He escrito el siguiente programa para jugar con std :: chrono por un momento:

#include <iostream>
#include <chrono>

int main(int argc, char** argv) {
    const long iterationCount = 10000;
    long count = 0;
    for(long i = 0; i < iterationCount; i++) {
        auto start = std::chrono::high_resolution_clock::now();
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        count++;count++;count++;count++;count++;count++;count++;count++;count++;count++;
        auto end = std::chrono::high_resolution_clock::now();

        auto timeTaken = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        std::cout << timeTaken << " " << std::endl;
    }
}

He compilado esto usando G ++ sin las optimizaciones del compilador habilitadas:

g++ chrono.cpp -o chrono

Posteriormente ejecuté este programa varias veces y obtuve un patrón interesante. Para las primeras 500-1000 iteraciones, el programa se ejecuta entre 7 y 8 veces más lento que el resto de las iteraciones.

Aquí hay un ejemplo de salida de este programa: https://pastebin.com/tUQsQEAQ

¿Qué causa esta discrepancia en tiempos de ejecución? Mi primera reacción fue el caché, pero ¿no se saturaría rápidamente?

En caso de que sea importante, mi sistema operativo es Ubuntu 18.04 y mi versión de g ++ es 7.3.0.

5
Bartvbl 9 sep. 2018 a las 02:22

5 respuestas

La mejor respuesta

Después de una implementación de microarquitectura, el tiempo definido y si la CPU puede encontrar el mismo margen de potencia térmica, la escala de frecuencia se activa para acelerar el reloj del núcleo exigente hasta un máximo (dentro de los límites del TDP).

La implementación de Intel se llama Turbo boost .

Si desactiva la escala de frecuencia en su sistema (por ejemplo, con sudo cpupower frequency-set --governor performance - cpupower está en el paquete cpupowerutils) el tiempo de cada iteración es más o menos el mismo.


El bucle en sí es muy fácil de predecir, puede esperar solo unas pocas predicciones erróneas si no solo una al final del control de bucle: el código dentro de la biblioteca de C ++ es más difícil de predecir, pero incluso con eso no tomará tanto tiempo (1000 iteraciones) para que la BPU se ponga al día con su código.
Por lo tanto, puede descartar la predicción errónea de la rama.

Más o menos lo mismo se aplica para el caché I (hay poco uso del caché D, a menos que la implementación de la biblioteca C ++ sea muy pesada en el uso variable).
El código debe ser lo suficientemente pequeño como para caber en el L1-I y, sobre todo, incluso en el DSB.
Las fallas de L1-I no requieren 1000 iteraciones para resolverse y si hubiera un conflicto de conjunto pesado en el caché que se mostraría como una desaceleración general que no desaparecerá después de 1000 iteraciones.

En términos generales, es un efecto conocido que el código se ejecuta más rápido desde la segunda iteración de una cadena de dependencia transportada por bucle porque la primera vez que la CPU llena los cachés (datos, instrucciones, TLB).
Eventualmente, puede disminuir la velocidad nuevamente si la CPU se queda sin recursos, por ejemplo, si hay mucha presión de puerto (por ejemplo, muchas instrucciones idénticas de latencia larga de puerto limitado) el RS puede llenar deteniendo el FE o si hay un mucha carga / tienda llenando el MOB / SB / LB o muchos saltos llenando el BOB.
Sin embargo, estos efectos se activan rápidamente hasta el punto de que dominan el tiempo de ejecución del código.
En este caso, la desaceleración ocurre muy tarde (en tiempos de CPU), lo que hace que sea deseable pensar en el proceso a pedido, como Turbo boost.

3
Margaret Bloom 11 sep. 2018 a las 06:25

Supongo que la resolución del reloj podría ser la razón. Está dentro del rango que puede estar por debajo de la resolución de su sistema. Considere esta cita de Linux documentación ( negrita por mí):

Desde Linux 2.6.21, Linux admite temporizadores de alta resolución (HRT), opcionalmente configurable a través de CONFIG_HIGH_RES_TIMERS. En un sistema que admite HRT, la precisión de las llamadas al sistema de reposo y temporizador es nula ya no está limitado por el jiffy, sino que puede ser tan preciso como el hardware lo permite (la precisión de microsegundos es típica de la moderna hardware ).

0
NoSenseEtAl 9 sep. 2018 a las 13:18

Como dicen otros, es casi seguro que experimente el efecto de la escala dinámica de frecuencia de la CPU.

Puedo reproducir tus resultados en mi máquina. Pero, si apago la escala dinámica de frecuencia de CPU con la utilidad cpufreq-set (para hacer que la CPU funcione a una frecuencia constante), el efecto que ves desaparece.

1
geza 10 sep. 2018 a las 21:14

No tengo una máquina física Linux para probar esto, pero ejecutándome en un cuadro de Windows 10 x64 (i7) obtuve resultados como ...

395
16592
395
395
395
790
395
790
395
395
395
790

Que coincide con el final de tu rastro. En Windows, el valor 395 parece estar bloqueado en fase con el contador del reloj, por lo que el período de tiempo es 395, 790 o un número realmente grande (por ejemplo, 116592). El número realmente grande se vería como un cambio de contexto, donde nuestro programa no se está ejecutando.

Su programa en mi máquina Windows no tuvo la desaceleración inicial.

Sin embargo, los resultados en general son muy similares a los resultados en el archivo pastebin.

Entonces, la pregunta de por qué el bucle lleva más tiempo desde el principio. Podemos ver que no es un cambio de contexto, ya que parecen ser significativamente más largos (18k). Por lo tanto, debe ser que el procesador es más lento al hacer su trabajo al comienzo del programa. Causas de esa lentitud, puede ser que otros núcleos en la CPU estén limpiando la memoria caché para nuestro núcleo, o usando la memoria caché compartida para la CPU.

Validaría esto agregando una suspensión inicial para que el sistema se estabilice en el momento del lanzamiento, ejecutando el ciclo solo después de que la máquina se haya asentado.

0
mksteve 10 sep. 2018 a las 19:48

En la mayoría de las máquinas, cuando comienza su prueba, la CPU generalmente estará en un estado de menor potencia, ejecutando una frecuencia más baja, pero a medida que la prueba continúe ejecutándose, la CPU aumentará su velocidad a la frecuencia máxima.

Entonces, un patrón común es ver tiempos lentos durante los primeros milisegundos, después de lo cual el tiempo de ejecución disminuye (a medida que la CPU se acelera) y finalmente se estabiliza.

0
BeeOnRope 10 sep. 2018 a las 21:03