c++ - tiempo - ¿Por qué este delay-loop comienza a correr más rápido después de varias iteraciones sin dormir?




system delay c++ (2)

Después de 26 iteraciones, Linux aumenta la CPU hasta la velocidad máxima del reloj ya que su proceso usa su segmento de tiempo completo un par de veces seguidas.

Si verificara con contadores de rendimiento en lugar de tiempo de reloj de pared, vería que los ciclos de reloj de núcleo por ciclo de retardo se mantuvieron constantes, confirmando que es solo un efecto de DVFS (que todas las CPU modernas usan para funcionar con más energía) frecuencia y voltaje eficientes la mayor parte del tiempo).

Si probó en un Skylake con soporte de kernel para el nuevo modo de administración de energía (donde el hardware toma el control total de la velocidad del reloj) , la aceleración ocurriría mucho más rápido.

Si lo deja funcionando durante un tiempo en una CPU Intel con Turbo , probablemente verá que el tiempo por iteración aumenta nuevamente ligeramente una vez que los límites térmicos requieran que la velocidad del reloj se reduzca a la frecuencia máxima sostenida.

La introducción de un usleep evita que el regulador de frecuencia de la CPU de Linux usleep la velocidad del reloj, porque el proceso no genera una carga del 100% incluso a la frecuencia mínima. (Es decir, la heurística del núcleo decide que la CPU se está ejecutando lo suficientemente rápido para la carga de trabajo que se ejecuta en él).

comentarios sobre otras teorías :

re: La teoría de David de que un posible cambio de contexto de usleep podría contaminar los cachés : esa no es una mala idea en general, pero no ayuda a explicar este código.

La contaminación de la caché / TLB no es importante para este experimento . Básicamente, no hay nada dentro de la ventana de tiempo que toque la memoria que no sea el final de la pila. La mayor parte del tiempo se gasta en un pequeño bucle (1 línea de caché de instrucciones) que solo toca un int de memoria de pila. ¡Cualquier posible contaminación de caché durante el usleep es una pequeña fracción del tiempo para este código (el código real será diferente)!

En más detalle para x86:

La llamada a clock() sí misma puede fallar en la memoria caché, pero una falta de memoria caché de búsqueda de código retrasa la medición del tiempo de inicio, en lugar de ser parte de lo que se mide. La segunda llamada a clock() casi nunca se retrasará, ya que aún debería estar caliente en caché.

La función de run puede estar en una línea de caché diferente de main (dado que gcc marca main como "frío", por lo que se optimiza menos y se coloca con otras funciones / datos en frío). Podemos esperar uno o dos errores de caché de instrucciones . Sin embargo, probablemente todavía estén en la misma página de 4k, por lo que main habrá desencadenado la posible falla de TLB antes de ingresar a la región cronometrada del programa.

gcc -O0 compilará el código del OP para algo como esto (explorador del compilador Godbolt) : manteniendo el contador de bucles en la memoria de la pila.

El bucle vacío mantiene el contador del bucle en la memoria de la pila, por lo que en una CPU Intel x86 típica, el bucle se ejecuta en una iteración por ~ 6 ciclos en la CPU IvyBridge del OP, gracias a la latencia de reenvío de la tienda que es parte de add con un destino de memoria ( leer-modificar-escribir). 100k iterations * 6 cycles/iteration son 600k ciclos, que dominan la contribución de, como máximo, un par de errores de caché (~ 200 ciclos cada uno para errores de recuperación de código que evitan que se emitan más instrucciones hasta que se resuelvan).

La ejecución fuera de orden y el reenvío de la tienda deberían ocultar la pérdida potencial de caché al acceder a la pila (como parte de la instrucción de call ).

Incluso si el contador de bucles se mantuvo en un registro, 100k ciclos es mucho.

https://code.i-harness.com

Considerar:

#include <time.h>
#include <unistd.h>
#include <iostream>
using namespace std;

const int times = 1000;
const int N = 100000;

void run() {
  for (int j = 0; j < N; j++) {
  }
}

int main() {
  clock_t main_start = clock();
  for (int i = 0; i < times; i++) {
    clock_t start = clock();
    run();
    cout << "cost: " << (clock() - start) / 1000.0 << " ms." << endl;
    //usleep(1000);
  }
  cout << "total cost: " << (clock() - main_start) / 1000.0 << " ms." << endl;
}

Aquí está el código de ejemplo. En las primeras 26 iteraciones del ciclo de temporización, la función de run cuesta aproximadamente 0.4 ms, pero luego el costo se reduce a 0.2 ms.

Cuando el usleep está usleep , el delay-loop toma 0.4 ms para todas las ejecuciones, sin acelerar nunca. ¿Por qué?

El código se compila con g++ -O0 (sin optimización), por lo que el ciclo de retraso no está optimizado. Se ejecuta en la CPU Intel (R) Core (TM) i3-3220 a 3.30 GHz, con 3.13.0-32-genérico Ubuntu 14.04.1 LTS (Trusty Tahr).


Una llamada a usleep puede o no resultar en un cambio de contexto. Si lo hace, tomará más tiempo que si no lo hace.







benchmarking