El estándar C11 parece implicar que las declaraciones de iteración con expresiones de control constantes no deben optimizarse. Estoy tomando mi consejo de esta respuesta , que cita específicamente la sección 6.8.5 del borrador del estándar:
La implementación puede suponer que una declaración de iteración cuya expresión de control no es una expresión constante ... termina.
En esa respuesta, menciona que un ciclo como while(1) ;
no debería estar sujeto a optimización.
Entonces ... ¿por qué Clang / LLVM optimiza el ciclo a continuación (compilado con cc -O2 -std=c11 test.c -o test
)?
#include <stdio.h>
static void die() {
while(1)
;
}
int main() {
printf("begin\n");
die();
printf("unreachable\n");
}
En mi máquina, esto se imprime begin
, luego se bloquea con una instrucción ilegal (una ud2
trampa colocada después die()
). En godbolt , podemos ver que no se genera nada después de la llamada a puts
.
Ha sido una tarea sorprendentemente difícil hacer que Clang produzca un bucle infinito debajo -O2
, aunque podría probar repetidamente una volatile
variable, que implica una lectura de memoria que no quiero. Y si hago algo como esto:
#include <stdio.h>
static void die() {
while(1)
;
}
int main() {
printf("begin\n");
volatile int x = 1;
if(x)
die();
printf("unreachable\n");
}
... Clang imprime begin
seguido de unreachable
como si el bucle infinito nunca existiera.
¿Cómo se consigue que Clang genere un bucle infinito adecuado sin acceso a la memoria con las optimizaciones activadas?
fuente
exit()
y porque el código puede haber descubierto una situación en la que no puede garantizar que los efectos de la ejecución continua no sean peores que inútiles . Un bucle de salto a uno mismo es una forma bastante pésima de manejar tales situaciones, pero no obstante, puede ser la mejor manera de manejar una mala situación.Respuestas:
El estándar C11 dice esto, 6.8.5 / 6:
Las dos notas al pie no son normativas, pero proporcionan información útil:
En su caso,
while(1)
es una expresión constante cristalina, por lo que la implementación no puede suponer que termine. Tal implementación se rompería irremediablemente, ya que los bucles "para siempre" son una construcción de programación común.Sin embargo, lo que sucede con el "código inalcanzable" después del ciclo es, que yo sepa, no está bien definido. Sin embargo, el sonido metálico se comporta de manera muy extraña. Comparando el código de máquina con gcc (x86):
gcc 9.2
-O3 -std=c11 -pedantic-errors
clang 9.0.0
-O3 -std=c11 -pedantic-errors
gcc genera el bucle, clang simplemente corre hacia el bosque y sale con el error 255.
Me estoy inclinando hacia este comportamiento de clang no conforme. Porque intenté expandir tu ejemplo más de esta manera:
Agregué C11
_Noreturn
en un intento de ayudar al compilador más adelante. Debe quedar claro que esta función colgará, solo de esa palabra clave.setjmp
devolverá 0 en la primera ejecución, por lo que este programa simplemente debería estrellarsewhile(1)
y detenerse allí, solo imprimiendo "begin" (suponiendo que \ n flushes stdout). Esto sucede con gcc.Si el ciclo simplemente se eliminó, debería imprimir "comenzar" 2 veces y luego imprimir "inalcanzable". Sin embargo, en el sonido metálico ( godbolt ), imprime "comenzar" 1 vez y luego "inalcanzable" antes de devolver el código de salida 0. Eso es simplemente incorrecto, sin importar cómo lo coloque.
No puedo encontrar ningún caso para reclamar un comportamiento indefinido aquí, así que mi opinión es que esto es un error en el sonido metálico. En cualquier caso, este comportamiento hace que el sonido metálico sea 100% inútil para programas como sistemas embebidos, donde simplemente debe poder confiar en bucles eternos que cuelgan el programa (mientras espera un perro guardián, etc.).
fuente
6.8.5/6
tiene la forma de if (estos), entonces puede asumir (esto) . Eso no significa que si no (estos) no puede asumir (esto) . Es una especificación solo para cuando se cumplen las condiciones, no cuando no se cumplen, donde puede hacer lo que quiera dentro de los estándares. Y si no hay observables ...int z=3; int y=2; int x=1; printf("%d %d\n", x, z);
no haya nada2
en el ensamblaje, por lo que en el sentido inútil vacíox
no se asignó despuésy
sino despuész
debido a la optimización. Entonces, a partir de su última oración, seguimos las reglas regulares, asumimos que el tiempo se detuvo (porque no estábamos limitados mejor) y quedamos en la impresión final "inalcanzable". Ahora, optimizamos esa declaración inútil (porque no sabemos nada mejor).while(1);
lo mismo que unaint y = 2;
declaración en términos de qué semántica se nos permite optimizar, incluso si su lógica permanece en la fuente. Desde n1528 tuve la impresión de que pueden ser iguales, pero dado que las personas con más experiencia que yo están discutiendo a la inversa, y aparentemente es un error oficial, más allá de un debate filosófico sobre si la redacción de la norma es explícita , el argumento se vuelve discutible.Debe insertar una expresión que pueda causar un efecto secundario.
La solución más simple:
Enlace Godbolt
fuente
asm("")
está implícitamenteasm volatile("");
y, por lo tanto, la declaración asm debe ejecutarse tantas veces como lo hace en la máquina abstracta gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html . (Tenga en cuenta que es no seguro para sus efectos secundarios que incluyen cualquier memoria o registros, que hay asm extendido con un"memory"
clobber si desea leer o escritura de la memoria que el acceso que nunca de asm C. básico sólo es seguro para cosas comoasm("mfence")
ocli
.)Otras respuestas ya cubrieron formas de hacer que Clang emitiera el bucle infinito, con lenguaje ensamblador en línea u otros efectos secundarios. Solo quiero confirmar que este es realmente un error del compilador. Específicamente, es un error LLVM de larga data : aplica el concepto C ++ de "todos los bucles sin efectos secundarios deben terminar" a lenguajes donde no debería, como C.
Por ejemplo, el lenguaje de programación Rust también permite bucles infinitos y usa LLVM como back-end, y tiene este mismo problema.
A corto plazo, parece que LLVM continuará asumiendo que "todos los bucles sin efectos secundarios deben terminar". Para cualquier lenguaje que permita bucles infinitos, LLVM espera que el front-end inserte
llvm.sideeffect
códigos de operación en dichos bucles. Esto es lo que Rust planea hacer, por lo que Clang (al compilar código C) probablemente también tenga que hacerlo.fuente
sideeffect
operación (en 2017) y espera que los front-end inserten esa operación en los bucles a su discreción. LLVM tuvo que elegir algunos valores predeterminados para los bucles, y resultó elegir el que se alinea con el comportamiento de C ++, intencionalmente o no. Por supuesto, todavía queda algo de trabajo de optimización por hacer, como fusionarsideeffect
operaciones consecutivas en una sola. (Esto es lo que impide que el front-end Rust lo use). Entonces, sobre esa base, el error está en el front-end (clang) que no inserta el op en los bucles.sideeffect
operaciones al comienzo de cada función y no vio ninguna regresión de rendimiento en tiempo de ejecución. El único problema es una regresión de tiempo de compilación , aparentemente debido a la falta de fusión de operaciones consecutivas como mencioné en mi comentario anterior.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 laprint;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.En el explorador del compilador Godbolt, compilación Clang 9.0 -O3 como C (
-xc
) para x86-64:El mismo compilador con las mismas opciones compila un
main
que llamainfloop() { while(1); }
al mismo primeroputs
, pero luego deja de emitir instrucciones paramain
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
label: jmp label
bucle infinitoreturn 0
desdemain
.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 escribeswhile(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).fuente
-ffreestanding -fno-strict-aliasing
. Funciona bien con ARM y quizás con AVR heredado.Solo para que conste, Clang también se porta mal con
goto
:Produce el mismo resultado que en la pregunta, es decir:
Veo que no veo ninguna forma de leer esto según lo permitido en C11, que solo dice:
Como
goto
no es una "declaración de iteración" (listas de 6.8.5while
,do
yfor
) no se aplica nada sobre las indulgencias especiales de "terminación asumida", sin embargo, desea leerlas.El compilador de enlaces Godbolt de la pregunta original es x86-64 Clang 9.0.0 y las banderas
-g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c
Con otros como x86-64 GCC 9.2 obtienes el perfecto perfecto:
Banderas
-g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c
fuente
nasty: goto nasty
puede conformarse y no hacer girar las CPU hasta que intervenga el agotamiento de los recursos o del usuario.bar()
dentrofoo()
se procese como una llamada de__1foo
a__2bar
, de__2foo
a__3bar
, etc. y de__16foo
a__launch_nasal_demons
, lo que permitiría que todos los objetos automáticos se asignen estáticamente y convertiría lo que generalmente es un límite de "tiempo de ejecución" en un límite de traducción.Interpretaré al abogado del diablo y argumentaré que el estándar no prohíbe explícitamente que un compilador optimice un bucle infinito.
Analicemos esto. Se puede suponer que una declaración de iteración que satisface ciertos criterios termina:
Esto no dice nada acerca de lo que sucede si no se cumplen los criterios y asumir que un ciclo puede terminar incluso entonces no está explícitamente prohibido mientras se observen otras reglas del estándar.
do { } while(0)
owhile(0){}
son después de todas las declaraciones de iteración (bucles) que no satisfacen los criterios que permiten que un compilador asuma por capricho que terminan y, sin embargo, obviamente terminan.Pero, ¿puede el compilador simplemente optimizar
while(1){}
?5.1.2.3p4 dice:
Esto menciona expresiones, no declaraciones, por lo que no es 100% convincente, pero ciertamente permite llamadas como:
ser omitido Curiosamente, el sonido metálico lo omite, y gcc no .
fuente
while(1){}
hay una secuencia infinita de1
evaluaciones entrelazadas con{}
evaluaciones, pero en qué parte del estándar dice que esas evaluaciones deben tomar un tiempo distinto de cero ? El comportamiento de gcc es más útil, supongo, porque no necesitas trucos relacionados con el acceso a la memoria o trucos fuera del lenguaje. Pero no estoy convencido de que el estándar prohíba esta optimización en el sonido metálico. Siwhile(1){}
la intención es hacer no optimizable, el estándar debe ser explícito al respecto y el bucle infinito debe enumerarse como un efecto secundario observable en 5.1.2.3p2.1
condición como un cálculo de valor. El tiempo de ejecución no importa: lo que importa es lo quewhile(A){} B;
puede no optimizarse por completo, no optimizarseB;
ni volver a secuenciarseB; while(A){}
. Para citar la máquina abstracta C11, el énfasis es mío: "La presencia de un punto de secuencia entre la evaluación de las expresiones A y B implica que cada cálculo de valor y efecto secundario asociado con A se secuencia antes de cada cálculo de valor y efecto secundario asociado con B ". El valor deA
se usa claramente (por el bucle).He estado convencido de que esto es solo un viejo error. Dejo mis pruebas a continuación y, en particular, la referencia a la discusión en el comité estándar por algún razonamiento que tenía anteriormente.
Creo que este es un comportamiento indefinido (ver final), y Clang solo tiene una implementación. De hecho, GCC funciona como espera, optimizando solo la
unreachable
declaración de impresión pero dejando el bucle. De alguna manera, Clang toma decisiones de manera extraña al combinar la alineación y determinar qué puede hacer con el bucle.El comportamiento es muy extraño: elimina la impresión final, por lo que "ve" el bucle infinito, pero también se deshace del bucle.
Es aún peor por lo que puedo decir. Eliminando la línea obtenemos:
entonces se crea la función y se optimiza la llamada. Esto es aún más resistente de lo esperado:
da como resultado un ensamblaje muy no óptimo para la función, ¡pero la llamada a la función se optimiza nuevamente! Peor aún:
Hice un montón de otras pruebas agregando una variable local y aumentándola, pasando un puntero, usando un
goto
etc ... En este punto me rendiría. Si debes usar clanghace el trabajo. Apesta a la optimización (obviamente), y sale en la final redundante
printf
. Al menos el programa no se detiene. Tal vez GCC después de todo?Apéndice
Tras una discusión con David, doy por sentado que el estándar no dice "si la condición es constante, no puede suponer que el ciclo termina". Como tal, y concedido bajo el estándar no hay un comportamiento observable (como se define en el estándar), argumentaría solo por la consistencia: si un compilador está optimizando un ciclo porque supone que termina, no debería optimizar las siguientes declaraciones.
Heck n1528 tiene estos como comportamiento indefinido si lo leo bien. Específicamente
A partir de aquí, creo que solo puede convertirse en una discusión de lo que queremos (¿esperado?) En lugar de lo que está permitido.
fuente
Parece que este es un error en el compilador de Clang. Si no hay ninguna obligación de que la
die()
función sea estática, elimínelastatic
y hágaloinline
:Funciona como se esperaba cuando se compila con el compilador de Clang y también es portátil.
Explorador del compilador (godbolt.org) - clang 9.0.0
-O3 -std=c11 -pedantic-errors
fuente
static inline
?Lo siguiente parece funcionar para mí:
a godbolt
Explícitamente decirle a Clang que no optimice esa función hace que se emita un bucle infinito como se esperaba. Esperemos que haya una manera de deshabilitar selectivamente optimizaciones particulares en lugar de simplemente desactivarlas de esa manera. Sin embargo, Clang todavía se niega a emitir código para el segundo
printf
. Para forzarlo a hacer eso, tuve que modificar aún más el código internomain
para:Parece que tendrá que deshabilitar las optimizaciones para su función de bucle infinito, luego asegúrese de que su bucle infinito se llame condicionalmente. En el mundo real, este último es casi siempre el caso de todos modos.
fuente
printf
que se genere el segundo si el ciclo realmente dura para siempre, porque en ese caso el segundoprintf
es realmente inalcanzable y, por lo tanto, se puede eliminar. (El error de Clang está en detectar la inalcanzabilidad y luego eliminar el bucle de manera que se alcance el código inalcanzable).__attribute__ ((optimize(1)))
, pero clang lo ignora como no compatible: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.htmlUna implementación conforme puede, y muchas prácticas lo hacen, imponer límites arbitrarios sobre cuánto tiempo puede ejecutarse un programa o cuántas instrucciones ejecutará, y comportarse de manera arbitraria si se violan esos límites o, bajo la regla "como si" - si determina que inevitablemente serán violados. Siempre que una implementación pueda procesar con éxito al menos un programa que ejerza nominalmente todos los límites enumerados en N1570 5.2.4.1 sin alcanzar ningún límite de traducción, la existencia de límites, la medida en que están documentados y los efectos de excederlos son todos los problemas de calidad de implementación fuera de la jurisdicción de la norma
Creo que la intención del Estándar es bastante clara de que los compiladores no deberían asumir que un
while(1) {}
ciclo sin efectos secundarios nibreak
declaraciones terminará. Al contrario de lo que algunas personas podrían pensar, los autores de la Norma no estaban invitando a los escritores de compiladores a ser estúpidos u obtusos. Una implementación conforme podría ser útil para decidir terminar cualquier programa que, si no se interrumpe, ejecutaría más instrucciones sin efectos secundarios que los átomos en el universo, pero una implementación de calidad no debería realizar tal acción sobre la base de cualquier suposición sobre terminación, sino más bien sobre la base de que hacerlo podría ser útil y no sería (a diferencia del comportamiento de clang) peor que inútil.fuente
El bucle no tiene efectos secundarios, por lo que puede optimizarse. El ciclo es efectivamente un número infinito de iteraciones de cero unidades de trabajo. Esto no está definido en matemáticas y en lógica y el estándar no dice si una implementación puede completar un número infinito de cosas si cada cosa se puede hacer en tiempo cero. La interpretación de Clang es perfectamente razonable al tratar el infinito por cero como cero en lugar de infinito. El estándar no dice si un bucle infinito puede terminar o no si todo el trabajo en los bucles se completa.
El compilador puede optimizar todo lo que no sea un comportamiento observable como se define en el estándar. Eso incluye el tiempo de ejecución. No es necesario preservar el hecho de que el ciclo, si no está optimizado, tomaría una cantidad infinita de tiempo. Está permitido cambiar eso a un tiempo de ejecución mucho más corto; de hecho, ese es el punto de la mayoría de las optimizaciones. Tu ciclo fue optimizado.
Incluso si clang tradujo el código ingenuamente, podría imaginar una CPU optimizadora que pueda completar cada iteración en la mitad del tiempo que tomó la iteración anterior. Eso literalmente completaría el ciclo infinito en una cantidad de tiempo finita. ¿Tal CPU optimizadora viola el estándar? Parece bastante absurdo decir que una CPU optimizadora violaría el estándar si es demasiado buena para la optimización. Lo mismo es cierto de un compilador.
fuente
Lo siento si este no es absurdamente el caso, me topé con esta publicación y sé que, debido a mis años usando la distribución Gentoo Linux, si desea que el compilador no optimice su código, debe usar -O0 (Zero). Tenía curiosidad al respecto, compilé y ejecuté el código anterior, y el bucle se ejecuta indefinidamente. Compilado usando clang-9:
fuente
Un
while
bucle vacío no tiene ningún efecto secundario en el sistema.Por lo tanto, Clang lo elimina. Hay "mejores" formas de lograr el comportamiento deseado que lo obligan a ser más obvio de sus intenciones.
while(1);
es baaadd.fuente
abort()
oexit()
. Si surge una situación en la que una función determina que (tal vez como resultado de la corrupción de la memoria) la ejecución continuada sería peor que peligrosa, un comportamiento predeterminado común para las bibliotecas integradas es invocar una función que realiza unawhile(1);
. Puede ser útil que el compilador tenga opciones para sustituir un comportamiento más útil , pero cualquier escritor del compilador que no pueda descubrir cómo tratar una construcción tan simple como una barrera para la ejecución continua del programa es incapaz de confiar en optimizaciones complejas.