Este es un error de Clang
... cuando se alinea una función que contiene un bucle infinito. El comportamiento es diferente cuando while(1);
aparece directamente en main, lo que me huele a buggy.
Consulte la respuesta de @ Arnavion para obtener un resumen y enlaces. El resto de esta respuesta se escribió antes de tener la confirmación de que se trataba de un error, y mucho menos de un error conocido.
Para responder la pregunta del título: ¿Cómo hago un bucle vacío infinito que no se optimizará? ? -
cree die()
una macro, no una función , para evitar este error en Clang 3.9 y versiones posteriores. (Las versiones anteriores de Clang mantienen el bucle o emiten unacall
versión no en línea de la función con el bucle infinito). Eso parece ser seguro incluso si la print;while(1);print;
función se alinea en su llamador ( Godbolt ). -std=gnu11
vs. -std=gnu99
no cambia nada.
Si solo le importa GNU C, P__J __ 's__asm__("");
dentro del bucle también funciona, y no debería dañar la optimización de ningún código circundante para los compiladores que lo entiendan. Las declaraciones asm de GNU C Basic son implícitamentevolatile
, por lo que esto cuenta como un efecto secundario visible que tiene que "ejecutarse" tantas veces como lo haría en la máquina abstracta de C. (Y sí, Clang implementa el dialecto GNU de C, como lo documenta el manual de GCC).
Algunas personas han argumentado que podría ser legal optimizar un bucle infinito vacío. No estoy de acuerdo 1 , pero incluso si aceptamos que, no puedo también ser legal para Clang para asumir declaraciones tras el bucle son inalcanzables, y dejó caer la ejecución fuera de la final de la función a la siguiente función, o en la basura que decodifica como instrucciones aleatorias.
(Eso sería compatible con los estándares para Clang ++ (pero aún no es muy útil); los bucles infinitos sin efectos secundarios son UB en C ++, pero no C.
Es while (1); el comportamiento indefinido en C? UB permite que el compilador emita básicamente cualquier cosa para el código en una ruta de ejecución que definitivamente encontrará UB. Una asm
declaración en el bucle evitaría esta UB para C ++. Pero en la práctica, la compilación de Clang como C ++ no elimina los bucles vacíos infinitos de expresión constante, excepto cuando está en línea, igual que cuando compilando como C.)
La inserción manual while(1);
cambia la forma en que Clang lo compila: bucle infinito presente en asm. Esto es lo que esperaríamos de un POV de abogados de reglas.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
En el explorador del compilador Godbolt, compilación Clang 9.0 -O3 como C ( -xc
) para x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
El mismo compilador con las mismas opciones compila un main
que llama infloop() { while(1); }
al mismo primero puts
, pero luego deja de emitir instrucciones para main
después de ese punto. Entonces, como dije, la ejecución simplemente cae del final de la función, en cualquier función que esté a continuación (pero con la pila desalineada para la entrada de la función, por lo que ni siquiera es una llamada final válida).
Las opciones válidas serían
- emitir un
label: jmp label
bucle infinito
- o (si aceptamos que se puede eliminar el bucle infinito) emitir otra llamada para imprimir la segunda cadena, y luego
return 0
desde main
.
El bloqueo o la continuación de otro modo sin imprimir "inalcanzable" claramente no está bien para una implementación de C11, a menos que haya UB que no haya notado.
Nota al pie 1:
Para el registro, estoy de acuerdo con la respuesta de @ Lundin que cita el estándar de evidencia de que C11 no permite la suposición de terminación para bucles infinitos de expresión constante, incluso cuando están vacíos (sin E / S, volátil, sincronización u otro efectos secundarios visibles).
Este es el conjunto de condiciones que permitirían compilar un bucle en un bucle asm vacío para una CPU normal. (Incluso si el cuerpo no estaba vacío en la fuente, las asignaciones a las variables no pueden ser visibles para otros hilos o manejadores de señales sin UB de carrera de datos mientras se ejecuta el bucle. Por lo tanto, una implementación conforme podría eliminar dichos cuerpos de bucle si lo desea Entonces, eso deja la pregunta de si el bucle en sí puede eliminarse. ISO C11 dice explícitamente que no).
Dado que C11 destaca ese caso como uno en el que la implementación no puede suponer que el ciclo termina (y que no es UB), parece claro que pretenden que el ciclo esté presente en tiempo de ejecución. Una implementación que apunta a CPU con un modelo de ejecución que no puede hacer una cantidad infinita de trabajo en tiempo finito no tiene justificación para eliminar un bucle infinito constante vacío. O incluso en general, la redacción exacta se refiere a si se puede "asumir que terminan" o no. Si un ciclo no puede terminar, eso significa que el código posterior no es accesible, sin importar los argumentos que haga sobre matemáticas e infinitos y cuánto tiempo lleva hacer una cantidad infinita de trabajo en una máquina hipotética.
Además de eso, Clang no es simplemente un DeathStation 9000 compatible con ISO C, está destinado a ser útil para la programación de sistemas de bajo nivel del mundo real, incluidos los núcleos y las cosas integradas. Entonces, ya sea que acepte o no argumentos sobre C11 que permite la eliminación de while(1);
, no tiene sentido que Clang quiera hacer eso realmente. Si escribes while(1);
, eso probablemente no fue un accidente. La eliminación de bucles que terminan infinitos por accidente (con expresiones de control de variables de tiempo de ejecución) puede ser útil, y tiene sentido que los compiladores hagan eso.
Es raro que solo quieras girar hasta la próxima interrupción, pero si escribes eso en C definitivamente eso es lo que esperas que suceda. (Y lo que sucede en GCC y Clang, excepto Clang cuando el bucle infinito está dentro de una función de contenedor).
Por ejemplo, en un núcleo de sistema operativo primitivo, cuando el planificador no tiene tareas para ejecutar, puede ejecutar la tarea inactiva. Una primera implementación de eso podría ser while(1);
.
O para hardware sin ninguna función inactiva de ahorro de energía, esa podría ser la única implementación. (Hasta principios de la década de 2000, creo que no era raro en x86. Aunque la hlt
instrucción existía, IDK si ahorraba una cantidad significativa de energía hasta que las CPU comenzaron a tener estados inactivos de baja potencia).