Pregunta ¿Por qué este bucle de retardo comienza a ejecutarse más rápido después de varias iteraciones sin dormir?


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 sincronización, el run la función cuesta aproximadamente 0.4 ms, pero luego el costo se reduce a 0.2 ms.

Cuando el usleep no está comentado, el bucle de retardo requiere 0,4 ms para todas las ejecuciones, nunca se acelera. ¿Por qué?

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


70
2017-07-11 04:06


origen


Respuestas:


Después de 26 iteraciones, Linux rampa la CPU hasta la velocidad máxima de reloj ya que su proceso utiliza su completo porción de tiempo un par de veces seguidas

Si seleccionó los contadores de rendimiento en lugar de la hora del reloj de pared, verá que los ciclos del reloj central por ciclo de demora se mantuvieron constantes, lo que confirma que solo es un efecto de DVFS (que todas las CPU modernas usan para funcionar a una frecuencia y voltaje de mayor eficiencia energética la mayor parte del tiempo).

Si has probado en un Skylake con soporte de núcleo 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 sería mucho más rápida.

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


Presentando un usleep previene Gobernador de frecuencia de CPU de Linux de aumentar la velocidad del reloj, porque el proceso no está generando un 100% de carga incluso a una frecuencia mínima. (Es decir, la heurística del kernel decide que la CPU se está ejecutando lo suficientemente rápido para la carga de trabajo que se está ejecutando en ella).



comentarios sobre otras teorías:

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

La contaminación de caché / TLB no es importante en absoluto 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 uno int de la memoria de la pila. Cualquier posible contaminación de caché durante usleep es una pequeña fracción del tiempo para este código (¡el código real será diferente)!

Más detalladamente para x86:

La llamada a clock() en sí mismo podría fallar la caché, pero una falla de caché de captación 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á, porque aún debería estar caliente en la memoria caché.

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

gcc -O0 compilará el código OP para algo como esto (Godbolt compiler explorer): mantener el contador de bucle en la memoria en la pila.

El bucle vacío mantiene el contador de bucle en la memoria de la pila, por lo que en un típico CPU Intel x86 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 tienda que es parte de add con un destino de memoria (lectura-modificación-escritura). 100k iterations * 6 cycles/iteration es de 600k ciclos, que domina la contribución de como mucho un par de errores de caché (~ 200 ciclos cada uno para errores de búsqueda de código que impiden la emisión de más instrucciones hasta que se resuelven).

La ejecución fuera de orden y el reenvío de tiendas deberían ocultar en su mayor parte el posible error de caché al acceder a la pila (como parte del call instrucción).

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


121
2017-07-11 05:57



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


3
2017-07-11 04:12