Supongamos que tengo varias declaraciones que quiero ejecutar en un orden fijo. Quiero usar g ++ con el nivel de optimización 2, por lo que algunas declaraciones podrían reordenarse. ¿Qué herramientas tiene uno para hacer cumplir un determinado orden de declaraciones?
Considere el siguiente ejemplo.
using Clock = std::chrono::high_resolution_clock;
auto t1 = Clock::now(); // Statement 1
foo(); // Statement 2
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
En este ejemplo, es importante que las instrucciones 1-3 se ejecuten en el orden indicado. Sin embargo, ¿no puede el compilador pensar que la declaración 2 es independiente de 1 y 3 y ejecutar el código de la siguiente manera?
using Clock=std::chrono::high_resolution_clock;
foo(); // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
c++
c++11
operator-precedence
S2108887
fuente
fuente
__sync_synchronize()
ser de alguna ayuda?foo
tarda en ejecutarse, que el compilador puede ignorar al reordenar, al igual que se le permite ignorar la observación de un hilo diferente.Respuestas:
Me gustaría intentar proporcionar una respuesta algo más completa después de que esto se discutiera con el comité de estándares de C ++. Además de ser miembro del comité de C ++, también soy desarrollador de los compiladores LLVM y Clang.
Fundamentalmente, no hay forma de usar una barrera o alguna operación en la secuencia para lograr estas transformaciones. El problema fundamental es que la semántica operativa de algo como una suma de números enteros es totalmente conocida en la implementación. Puede simularlos, sabe que no pueden ser observados por los programas correctos y siempre es libre de moverlos.
Podríamos intentar prevenir esto, pero tendría resultados extremadamente negativos y finalmente fracasaría.
Primero, la única forma de evitar esto en el compilador es decirle que todas estas operaciones básicas son observables. El problema es que esto evitaría la inmensa mayoría de las optimizaciones del compilador. Dentro del compilador, esencialmente no tenemos buenos mecanismos para modelar que el tiempo es observable, pero nada más. Ni siquiera tenemos un buen modelo de qué operaciones llevan tiempo . Como ejemplo, ¿lleva tiempo convertir un entero sin signo de 32 bits en un entero sin signo de 64 bits? No lleva tiempo en x86-64, pero en otras arquitecturas toma un tiempo distinto de cero. Aquí no hay una respuesta genéricamente correcta.
Pero incluso si logramos evitar con algunos actos heroicos que el compilador reordene estas operaciones, no hay garantía de que esto sea suficiente. Considere una forma válida y conforme de ejecutar su programa C ++ en una máquina x86: DynamoRIO. Este es un sistema que evalúa dinámicamente el código máquina del programa. Una cosa que puede hacer son optimizaciones en línea, e incluso es capaz de ejecutar especulativamente toda la gama de instrucciones aritméticas básicas fuera del tiempo. Y este comportamiento no es exclusivo de los evaluadores dinámicos, la CPU x86 real también especulará (un número mucho menor de) instrucciones y las reordenará dinámicamente.
La comprensión esencial es que el hecho de que la aritmética no sea observable (incluso a nivel de tiempo) es algo que impregna las capas de la computadora. Es cierto para el compilador, el tiempo de ejecución y, a menudo, incluso el hardware. Obligarlo a ser observable restringiría drásticamente el compilador, pero también restringiría drásticamente el hardware.
Pero todo esto no debería hacer que pierda la esperanza. Cuando desee cronometrar la ejecución de operaciones matemáticas básicas, tenemos técnicas bien estudiadas que funcionan de manera confiable. Por lo general, se utilizan al realizar microevaluaciones comparativas . Di una charla sobre esto en CppCon2015: https://youtu.be/nXaxk27zwlk
Las técnicas que se muestran allí también las proporcionan varias bibliotecas de microevaluaciones como las de Google: https://github.com/google/benchmark#preventing-optimization
La clave de estas técnicas es centrarse en los datos. Usted hace que la entrada del cálculo sea opaca para el optimizador y el resultado del cálculo opaco para el optimizador. Una vez que haya hecho eso, puede cronometrarlo de manera confiable. Veamos una versión realista del ejemplo de la pregunta original, pero con la definición de
foo
completamente visible para la implementación. También extraje una versión (no portátil) deDoNotOptimize
la biblioteca de Google Benchmark que puede encontrar aquí: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208Aquí nos aseguramos de que los datos de entrada y los datos de salida estén marcados como no optimizables alrededor del cálculo
foo
, y solo alrededor de esos marcadores se calculan los tiempos. Debido a que está utilizando datos para ajustar el cálculo, se garantiza que se mantendrá entre los dos tiempos y, sin embargo, se permite optimizar el cálculo en sí. El ensamblado x86-64 resultante generado por una compilación reciente de Clang / LLVM es:Aquí puede ver al compilador optimizando la llamada para
foo(input)
reducirla a una sola instrucciónaddl %eax, %eax
, pero sin moverla fuera del tiempo o eliminarla por completo a pesar de la entrada constante.Espero que esto ayude, y el comité de estándares de C ++ está estudiando la posibilidad de estandarizar API similares a
DoNotOptimize
aquí.fuente
Clock::now()
se reordenen en relación con foo ()? ¿Tiene el optimizador que asumir esoDoNotOptimize
yClock::now()
tener acceso y podría modificar algún estado global común que a su vez los vincularía a la entrada y salida? ¿O confía en algunas limitaciones actuales de la implementación del optimizador?DoNotOptimize
en este ejemplo es un evento sintéticamente "observable". Es como si imprimiera teóricamente una salida visible en algún terminal con la representación de la entrada. Dado que leer el reloj también es observable (está observando el paso del tiempo), no se pueden reordenar sin cambiar el comportamiento observable del programa.foo
función está realizando algunas operaciones como leer desde un socket que puede estar bloqueado por un tiempo, ¿esto cuenta como una operación observable? Y dadoread
que no es una operación "totalmente conocida" (¿verdad?), ¿El código se mantendrá en orden?Resumen:
Parece que no hay una forma garantizada de evitar el reordenamiento, pero mientras no se habilite la optimización de tiempo de enlace / programa completo, ubicar la función llamada en una unidad de compilación separada parece una apuesta bastante buena . (Al menos con GCC, aunque la lógica sugeriría que esto también es probable con otros compiladores). Esto tiene el costo de la llamada a la función: el código en línea está por definición en la misma unidad de compilación y está abierto a reordenamiento.
Respuesta original:
GCC reordena las llamadas bajo la optimización -O2:
GCC 5.3.0:
g++ -S --std=c++11 -O0 fred.cpp
:Pero:
g++ -S --std=c++11 -O2 fred.cpp
:Ahora, con foo () como función externa:
g++ -S --std=c++11 -O2 fred.cpp
:PERO, si esto está vinculado con -flto (optimización de tiempo de enlace):
fuente
La reordenación puede realizarla el compilador o el procesador.
La mayoría de los compiladores ofrecen un método específico de la plataforma para evitar el reordenamiento de las instrucciones de lectura y escritura. En gcc, esto es
( Más información aquí )
Tenga en cuenta que esto solo evita indirectamente las operaciones de reordenación, siempre que dependan de las lecturas / escrituras.
En la práctica , todavía no he visto un sistema en el que la llamada al sistema
Clock::now()
tenga el mismo efecto que una barrera de este tipo. Puede inspeccionar el ensamblaje resultante para estar seguro.Sin embargo, no es raro que la función bajo prueba sea evaluada durante el tiempo de compilación. Para imponer una ejecución "realista", es posible que deba derivar la entrada
foo()
de E / S o unavolatile
lectura.Otra opción sería deshabilitar la inserción para
foo()
; nuevamente, esto es específico del compilador y generalmente no es portátil, pero tendría el mismo efecto.En gcc, esto sería
__attribute__ ((noinline))
@Ruslan plantea una cuestión fundamental: ¿Cuán realista es esta medida?
El tiempo de ejecución se ve afectado por muchos factores: uno es el hardware real en el que estamos ejecutando, el otro es el acceso concurrente a recursos compartidos como caché, memoria, disco y núcleos de CPU.
Entonces, lo que solemos hacer para obtener tiempos comparables : asegurarnos de que sean reproducibles con un margen de error bajo. Esto los hace algo artificiales.
El rendimiento de ejecución de "caché caliente" frente a "caché fría" puede diferir fácilmente en un orden de magnitud, pero en realidad, será algo intermedio (¿"tibio"?)
fuente
asm
afecta el tiempo de ejecución de las declaraciones entre llamadas de temporizador: el código después del golpe de memoria tiene que recargar todas las variables de la memoria.El lenguaje C ++ define lo que es observable de varias formas.
Si
foo()
no hace nada observable, entonces se puede eliminar por completo. Sifoo()
solo hace un cálculo que almacena valores en estado "local" (ya sea en la pila o en un objeto en algún lugar), y el compilador puede probar que ningún puntero derivado de forma segura puede ingresar alClock::now()
código, entonces no hay consecuencias observables para moviendo lasClock::now()
llamadas.Si
foo()
interactuado con un archivo o la pantalla, y el compilador no puede probar queClock::now()
lo hace no interactúan con el archivo o la pantalla, a continuación, la reordenación no se puede hacer, debido a la interacción con un archivo o pantalla es la conducta observable.Si bien puede usar trucos específicos del compilador para forzar que el código no se mueva (como el ensamblaje en línea), otro enfoque es intentar burlar a su compilador.
Cree una biblioteca cargada dinámicamente. Cárguelo antes que el código en cuestión.
Esa biblioteca expone una cosa:
y lo envuelve así:
que empaqueta un lambda nular y usa la biblioteca dinámica para ejecutarlo en un contexto que el compilador no puede entender.
Dentro de la biblioteca dinámica, hacemos:
que es bastante simple.
Ahora, para reordenar las llamadas a
execute
, debe comprender la biblioteca dinámica, lo que no puede hacer mientras compila el código de prueba.Todavía puede eliminar
foo()
mensajes de correo electrónico con cero efectos secundarios, pero ganas algunos y pierdes otros.fuente
volatile
acceso ficticio o una llamada a un código externo.No, no puede. Según el estándar C ++ [intro.execution]:
Una expresión completa es básicamente una declaración terminada con un punto y coma. Como puede ver, la regla anterior estipula que las declaraciones deben ejecutarse en orden. Es dentro de las declaraciones donde el compilador tiene más rienda suelta (es decir, bajo alguna circunstancia se le permite evaluar expresiones que componen una declaración en órdenes que no sean de izquierda a derecha o cualquier otra cosa específica).
Tenga en cuenta que aquí no se cumplen las condiciones para que se aplique la regla como si. No es razonable pensar que cualquier compilador podría demostrar que reordenar las llamadas para obtener la hora del sistema no afectaría el comportamiento observable del programa. Si hubiera una circunstancia en la que dos llamadas para obtener el tiempo pudieran reordenarse sin cambiar el comportamiento observado, sería extremadamente ineficiente producir un compilador que analice un programa con suficiente comprensión para poder inferir esto con certeza.
fuente
No.
A veces, por la regla "como si", las declaraciones pueden reordenarse. Esto no se debe a que sean lógicamente independientes entre sí, sino a que esa independencia permite que se produzca tal reordenamiento sin cambiar la semántica del programa.
Mover una llamada al sistema que obtiene la hora actual, obviamente, no satisface esa condición. Un compilador que lo hace a sabiendas o sin saberlo no cumple con las normas y es realmente tonto.
En general, no esperaría que ninguna expresión que resulte en una llamada al sistema sea "cuestionada" incluso por un compilador de optimización agresiva. Simplemente no sabe lo suficiente sobre lo que hace esa llamada al sistema.
fuente
int x = 0; clock(); x = y*2; clock();
no hay formas definidas para que elclock()
código interactúe con el estado dex
. Bajo el estándar C ++, no tiene que saber quéclock()
es lo que hace, podría examinar la pila (y notar cuándo ocurre el cálculo), pero ese no es el problema de C ++ .t2
y el segundot1
, sería no conforme y tonto si se usan esos valores, lo que esta respuesta pierde es que un compilador conforme a veces puede reordenar otro código a través de una llamada al sistema. En este caso, siempre que sepa lo quefoo()
hace (por ejemplo, porque lo ha incluido) y, por lo tanto, (en términos generales) es una función pura, entonces puede moverlo.y*y
antes de la llamada al sistema, solo por diversión. Tampoco hay garantía de que la implementación real no use el resultado de este cálculo especulativo más adelante en cualquier punto quex
se use, por lo tanto, no hace nada entre las llamadas aclock()
. Lo mismo ocurre con cualquier función en líneafoo
, siempre que no tenga efectos secundarios y no pueda depender del estado que pueda ser alteradoclock()
.noinline
función + caja negra de ensamblaje en línea + dependencias de datos completasEsto se basa en https://stackoverflow.com/a/38025837/895245 pero como no vi ninguna justificación clara de por qué
::now()
no se puede reordenar allí, preferiría ser paranoico y ponerlo dentro de una función noinline junto con el asm.De esta manera, estoy bastante seguro de que el reordenamiento no puede ocurrir, ya que
noinline
"vincula" el::now
y la dependencia de datos.main.cpp
GitHub aguas arriba .
Compilar y ejecutar:
El único inconveniente menor de este método es que agregamos una
callq
instrucción adicional sobre uninline
método.objdump -CD
muestra quemain
contiene:entonces vemos que
foo
estaba alineado, peroget_clock
no lo estaba y lo rodeamos.get_clock
en sí mismo, sin embargo, es extremadamente eficiente, y consiste en una instrucción optimizada de llamada de una sola hoja que ni siquiera toca la pila:Dado que la precisión del reloj es en sí misma limitada, creo que es poco probable que pueda notar los efectos de sincronización de uno adicional
jmpq
. Tenga en cuenta quecall
se requiere uno independientemente de que::now()
esté en una biblioteca compartida.Llamar
::now()
desde ensamblado en línea con una dependencia de datosEsta sería la solución más eficiente posible, superando incluso el extra
jmpq
mencionado anteriormente.Desafortunadamente, esto es extremadamente difícil de hacer correctamente, como se muestra en: Llamar a printf en ASM en línea extendido
Sin embargo, si su medición del tiempo se puede realizar directamente en el ensamblaje en línea sin una llamada, entonces se puede utilizar esta técnica. Este es el caso, por ejemplo, de las instrucciones de instrumentación mágica gem5 , RDTSC x86 (no estoy seguro de si esto ya es representativo) y posiblemente otros contadores de rendimiento.
Temas relacionados:
Probado con GCC 8.3.0, Ubuntu 19.04.
fuente
"+m"
, usar"+r"
es una forma mucho más eficiente de hacer que el compilador materialice un valor y luego asuma que la variable ha cambiado.