¿Por qué el compilador convirtió mi bucle condicional en un infinito?

Uno de los usuarios del compilador de Visual C ++ dio el siguiente ejemplo de código y preguntó por qué su ciclo con la condición se ejecuta sin fin, aunque en algún momento la condición debería detenerse y el ciclo debería terminar:

#include <windows.h> int x = 0, y = 1; int* ptr; DWORD CALLBACK ThreadProc(void*) { Sleep(1000); ptr = &y; return 0; } int main(int, char**) { ptr = &x; // starts out pointing to x DWORD id; HANDLE hThread = CreateThread(nullptr, 0, ThreadProc, 0, &id); // ,        ptr //     while (*ptr == 0) { } return 0; } 

Para aquellos que no están familiarizados con las características específicas de la plataforma Windows, aquí está el equivalente en C ++ puro:

 #include <chrono> #include <thread> int x = 0, y = 1; int* ptr = &x; void ThreadProc() { std::this_thread::sleep_for(std::chrono::seconds(1)); ptr = &y; } int main(int, char**) { ptr = &x; // starts out pointing to x std::thread thread(ThreadProc); // ,        ptr //     while (*ptr == 0) { } return 0; } 

A continuación, el usuario aportó su comprensión del programa:
El bucle condicional ha sido convertido en infinito por el compilador. Veo esto desde el código ensamblador generado, que una vez carga el valor del puntero ptr en el registro (al comienzo del bucle), y luego compara el valor de este registro con cero en cada iteración. Como la recarga del valor de ptr nunca vuelve a ocurrir, el ciclo nunca termina.

Entiendo que declarar ptr como "int volátil *" debería hacer que el compilador descarte optimizaciones y lea el valor de ptr en cada iteración del bucle, lo que solucionará el problema. Pero aún así me gustaría saber por qué el compilador no puede ser lo suficientemente inteligente como para hacer esas cosas automáticamente. Obviamente, la variable global utilizada en dos hilos diferentes se puede cambiar, lo que significa que no se puede almacenar en caché simplemente en el registro. ¿Por qué el compilador no puede generar inmediatamente el código correcto?


Antes de responder a esta pregunta, comencemos con una pequeña selección: "volátil int * ptr" no declara la variable ptr como un "puntero para el cual las optimizaciones están prohibidas". Este es un "puntero normal a una variable para la cual las optimizaciones están prohibidas". Lo que el autor de la pregunta anterior tenía en mente era declararlo como "int * voltile ptr".

Ahora volvamos a la pregunta principal. ¿Qué está pasando aquí?

Incluso una mirada rápida al código nos dirá que no hay variables como std :: atomic, ni el uso de std :: memory_order (ya sea explícito o implícito). Esto significa que cualquier intento de acceder a ptr o * ptr desde dos flujos diferentes conduce a un comportamiento indefinido. Intuitivamente, puede pensarlo de esta manera: “El compilador optimiza cada hilo como si se estuviera ejecutando solo en el programa. Los únicos puntos en los que el compilador DEBE pensar en acceder a datos de diferentes flujos están usando std :: atomic o std :: memory_order ".

Esto explica por qué el programa no se comportó como esperaba el programador. Desde el momento en que permite un comportamiento vago, no se puede garantizar absolutamente nada.

Pero bueno, pensemos en la segunda parte de su pregunta: ¿por qué el compilador no es lo suficientemente inteligente como para reconocer esta situación y apaga automáticamente la optimización al cargar el valor del puntero en el registro? Bueno, el compilador aplica automáticamente todo lo posible y no contrario al estándar de optimización. Sería extraño exigirle que pueda leer los pensamientos del programador y deshabilitar algunas optimizaciones que no contradicen el estándar, lo que, tal vez, según el programador debería cambiar la lógica del programa para mejor. “Oh, ¿qué pasa si este ciclo espera un cambio en el valor de una variable global en otro hilo, aunque no se haya anunciado explícitamente? ¡Lo tomaré cien veces para reducir la velocidad y estar listo para esta situación! " ¿Debería ser así? Apenas

Pero supongamos que agregamos una regla al compilador como "Si la optimización ha llevado a la aparición de un bucle infinito, entonces debe cancelarlo y recopilar el código sin optimización". O incluso así: "Cancelar sucesivamente optimizaciones individuales hasta que el resultado sea un bucle no infinito". Además de las sorprendentes sorpresas que esto traerá, ¿dará algún beneficio?

Sí, en este caso teórico no obtendremos un bucle infinito. Se interrumpirá si alguna otra secuencia escribe un valor distinto de cero en * ptr. También se interrumpirá si otro hilo escribe un valor distinto de cero en la variable x. No queda claro cuán profundamente debería funcionar el análisis de las dependencias para "captar" todos los casos que pueden afectar la situación. Dado que el compilador en realidad no inicia el programa creado y no analiza su comportamiento en tiempo de ejecución, la única salida es asumir que no se pueden optimizar las llamadas a variables globales, punteros y enlaces.

 int limit; void do_something() { ... if (value > limit) value = limit; //   limit ... for (i = 0; i < 10; i++) array[i] = limit; //   limit ... } 

Esto es completamente contrario al espíritu de C ++. El estándar de lenguaje dice que si modifica una variable y espera ver esta modificación en otro hilo, debe decir explícitamente esto: use una operación atómica u organice el acceso a la memoria (generalmente usando un objeto de sincronización).

Así que por favor haz eso.

Source: https://habr.com/ru/post/es424729/


All Articles