Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

449
Views
¿Cómo hago un bucle vacío infinito que no se optimizará?

El estándar C11 parece implicar que las declaraciones de iteración con expresiones de control constantes no deben optimizarse. Sigo mi consejo de esta respuesta , que cita específicamente la sección 6.8.5 del proyecto de norma:

Una declaración de iteración cuya expresión de control no es una expresión constante... puede ser asumida por la implementación para terminar.

En esa respuesta menciona que un bucle como while(1) ; no debe ser objeto de 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 imprime begin , luego falla en una instrucción ilegal (una trampa ud2 colocada después de 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 genere un bucle infinito bajo -O2 , mientras que pude probar repetidamente una variable volatile , 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"); }

... Las impresiones de Clang begin seguidas de unreachable como si el ciclo infinito nunca hubiera existido.

¿Cómo consigues que Clang emita un bucle infinito adecuado sin acceso a la memoria con las optimizaciones activadas?

over 4 years ago · Santiago Trujillo
13 answers
Answer question

0

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é de estándares por algunos razonamientos que tuve 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 declaración de impresión unreachable pero dejando el bucle. De alguna manera, Clang extrañamente toma decisiones al combinar el revestimiento 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 incluso peor por lo que puedo decir. Eliminando el inline obtenemos:

 die: # @die .LBB0_1: # =>This Inner Loop Header: Depth=1 jmp .LBB0_1 main: # @main push rax mov edi, offset .Lstr call puts .Lstr: .asciz "begin"

por lo que se crea la función y se optimiza la llamada. Esto es aún más resistente de lo esperado:

 #include <stdio.h> void die(int x) { while(x); } int main() { printf("begin\n"); die(1); printf("unreachable\n"); }

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:

 void die(x) { while(x++); } int main() { printf("begin\n"); die(1); printf("unreachable\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 daría por vencido. Si debes usar clang

 static void die() { int volatile x = 1; while(x); }

hace el trabajo. Apesta en la optimización (obviamente) y deja en el printf final redundante. Al menos el programa no se detiene. ¿Quizás GCC después de todo?

Apéndice

Después de la discusión con David, cedo que el estándar no dice "si la condición es constante, no puede asumir que el ciclo termina". Como tal, y dado que según el estándar no hay un comportamiento observable (como se define en el estándar), argumentaría solo por coherencia: si un compilador está optimizando un bucle porque supone que termina, no debe optimizar las siguientes declaraciones.

Heck n1528 tiene estos como un comportamiento indefinido si lo leo bien. Específicamente

Un problema importante para hacerlo es que permite que el código se mueva a través de un bucle potencialmente sin terminación.

A partir de aquí, creo que solo puede convertirse en una discusión sobre lo que queremos (¿esperamos?) en lugar de lo que está permitido.

over 4 years ago · Santiago Trujillo Report

0

El bucle no tiene efectos secundarios, por lo que se puede optimizar. El ciclo es efectivamente un número infinito de iteraciones de cero unidades de trabajo. Esto no está definido en matemáticas ni en lógica, y el estándar no dice si se permite que una implementación complete una cantidad infinita 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 ha completado.

El compilador puede optimizar cualquier cosa 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 bucle, si no se optimiza, tardarí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 objetivo de la mayoría de las optimizaciones. Su ciclo fue optimizado.

Incluso si clang tradujo el código de manera ingenua, podría imaginar una CPU optimizada que puede 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 finita de tiempo. ¿Una CPU tan optimizada viola el estándar? Parece bastante absurdo decir que una CPU optimizada violaría el estándar si es demasiado buena optimizando. Lo mismo ocurre con un compilador.

over 4 years ago · Santiago Trujillo Report

0

Debe insertar una expresión que pueda causar un efecto secundario.

La solución más simple:

 static void die() { while(1) __asm(""); }

Enlace de Godbolt

over 4 years ago · Santiago Trujillo Report

0

El estándar C11 dice esto, 6.8.5/6:

Una declaración de iteración cuya expresión de control no es una expresión constante, 156) que no realiza operaciones de entrada/salida, no accede a objetos volátiles y no realiza operaciones atómicas o de sincronización en su cuerpo, expresión de control o (en el caso de un for declaración) su expresión-3, puede ser asumido por la implementación para terminar. 157)

Las dos notas al pie no son normativas pero proporcionan información útil:

156) Una expresión de control omitida se reemplaza por una constante distinta de cero, que es una expresión constante.

157) Esto está destinado a permitir transformaciones del compilador, como la eliminación de bucles vacíos, incluso cuando no se puede probar la terminación.

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, que yo sepa, no está bien definido. Sin embargo, clang sí se comporta de manera muy extraña. Comparando el código máquina con gcc (x86):

gcc 9.2 -O3 -std=c11 -pedantic-errors

 .LC0: .string "begin" main: sub rsp, 8 mov edi, OFFSET FLAT:.LC0 call puts .L2: jmp .L2

clang 9.0.0 -O3 -std=c11 -pedantic-errors

 main: # @main push rax mov edi, offset .Lstr call puts .Lstr: .asciz "begin"

gcc genera el ciclo, clang simplemente corre hacia el bosque y sale con el error 255.

Me inclino a que esto sea un comportamiento no conforme de clang. Porque traté de expandir tu ejemplo aún más de esta manera:

 #include <stdio.h> #include <setjmp.h> static _Noreturn void die() { while(1) ; } int main(void) { jmp_buf buf; _Bool first = !setjmp(buf); printf("begin\n"); if(first) { die(); longjmp(buf, 1); } printf("unreachable\n"); }

Agregué C11 _Noreturn en un intento de ayudar al compilador más adelante. Debe quedar claro que esta función colgará, solo con esa palabra clave.

setjmp devolverá 0 en la primera ejecución, por lo que este programa debería simplemente romper el while(1) y detenerse allí, solo imprimiendo "begin" (suponiendo que \n vacía stdout). Esto sucede con gcc.

Si el bucle simplemente se eliminó, debería imprimir "comenzar" 2 veces y luego imprimir "inaccesible". Sin embargo, al sonar ( Godbolt ), imprime "comenzar" 1 vez y luego "inalcanzable" antes de devolver el código de salida 0. Eso es simplemente incorrecto, no importa cómo lo pongas.

No puedo encontrar ningún caso para reclamar un comportamiento indefinido aquí, por lo que mi opinión es que se trata de un error en clang. En cualquier caso, este comportamiento hace que el sonido metálico sea 100% inútil para programas como sistemas integrados, donde simplemente debe poder confiar en bucles eternos que cuelgan el programa (mientras espera un perro guardián, etc.).

over 4 years ago · Santiago Trujillo Report

0

Haré el papel de abogado del diablo y argumentaré que el estándar no prohíbe explícitamente que un compilador optimice un bucle infinito.

Una declaración de iteración cuya expresión de control no es una expresión constante, 156) que no realiza operaciones de entrada/salida, no accede a objetos volátiles y no realiza operaciones atómicas o de sincronización en su cuerpo, expresión de control o (en el caso de un for declaración) su expresión-3, puede ser asumido por la implementación para terminar.157)

Analicemos esto. Se puede suponer que una declaración de iteración que satisface ciertos criterios termina:

 if (satisfiesCriteriaForTerminatingEh(a_loop)) if (whatever_reason_or_just_because_you_feel_like_it) assumeTerminates(a_loop);

Esto no dice nada sobre lo que sucede si no se cumplen los criterios y asumir que un bucle puede terminar incluso entonces no está prohibido explícitamente siempre que se observen otras reglas del estándar.

do { } while(0) o while(0){} están después de todas las declaraciones de iteración (bucles) que no satisfacen los criterios que permiten a un compilador simplemente asumir por capricho que terminan y, sin embargo, obviamente terminan.

Pero, ¿puede el compilador simplemente optimizar while(1){} out?

5.1.2.3p4 dice:

En la máquina abstracta, todas las expresiones se evalúan según lo especificado por la semántica. Una implementación real no necesita evaluar parte de una expresión si puede deducir que su valor no se usa y que no se producen efectos secundarios necesarios (incluidos los causados por llamar a una función o acceder a un objeto volátil).

Esto menciona expresiones, no declaraciones, por lo que no es 100% convincente, pero ciertamente permite llamadas como:

 void loop(void){ loop(); } int main() { loop(); }

para ser salteado. Curiosamente, clang lo omite y gcc no .

over 4 years ago · Santiago Trujillo Report

0

Este es un error de Clang

... cuando se inserta una función que contiene un ciclo infinito. El comportamiento es diferente cuando while(1); aparece directamente en main, lo que me huele muy mal.

Consulte la respuesta de @Arnavion para obtener un resumen y enlaces. El resto de esta respuesta se escribió antes de que tuviera la confirmación de que era un error, y mucho menos un error conocido.


Para responder a la pregunta del título: ¿Cómo hago un bucle vacío infinito que no se optimizará? ? -
haga que die() sea una macro, no una función , para evitar este error en Clang 3.9 y versiones posteriores. (Las versiones anteriores de Clang mantienen el ciclo o emiten una call a una versión no en línea de la función con el ciclo infinito). Eso parece ser seguro incluso si print;while(1);print; función en línea en su llamador ( Godbolt ). -std=gnu11 vs. -std=gnu99 no cambia nada.

Si solo te interesa GNU C, __asm __asm__(""); dentro del ciclo también funciona, y no debería perjudicar la optimización de ningún código circundante para los compiladores que lo entiendan. Las declaraciones de GNU C Basic asm son implícitamente volatile , 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 se documenta en el manual de GCC).


Algunas personas han argumentado que podría ser legal optimizar un ciclo infinito vacío. No estoy de acuerdo 1 , pero incluso si aceptamos eso, tampoco puede ser legal que Clang suponga que las declaraciones después del ciclo son inalcanzables y deje que la ejecución se caiga del final de la función a la siguiente función, o a 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 en C.
es mientras(1); 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 instrucción asm en el ciclo evitaría este 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 se inserta, al igual que cuando se compila como C).


Inserción manual while(1); cambia cómo Clang lo compila: bucle infinito presente en asm. Esto es lo que esperaríamos de un punto de vista de abogado 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 , Clang 9.0 -O3 compilando 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 a infloop() { while(1); } al mismo primero puts , pero luego simplemente deja de emitir instrucciones para main después de ese punto. Entonces, como dije, la ejecución simplemente cae al 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 una label: jmp label bucle infinito
  • o (si aceptamos que el bucle infinito se puede eliminar) emita otra llamada para imprimir la segunda cadena y luego return 0 desde main .

Bloquear o continuar 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 que conste, estoy de acuerdo con la respuesta de @Lundin que cita el estándar como 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átiles, sincronización u otros efectos secundarios visibles).

Este es el conjunto de condiciones que permitiría compilar un ciclo en un ciclo 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 subprocesos o controladores 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 quisiera a. Entonces eso deja la pregunta de si el bucle en sí se puede eliminar. ISO C11 dice explícitamente que no).

Dado que C11 destaca ese caso como uno en el que la implementación no puede asumir que el bucle termina (y que no es UB), parece claro que pretenden que el bucle esté presente en tiempo de ejecución. Una implementación que tiene como objetivo las CPU con un modelo de ejecución que no puede realizar una cantidad infinita de trabajo en un 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 "suponer que terminan" o no. Si un bucle no puede terminar, eso significa que no se puede acceder al código posterior, 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 los argumentos sobre C11 que permiten la eliminación de while(1); , no tiene sentido que Clang quiera hacer eso. Si escribe while(1); , eso probablemente no fue un accidente. La eliminación de bucles que terminan siendo infinitos por accidente (con expresiones de control de variables en tiempo de ejecución) puede ser útil, y tiene sentido que los compiladores lo hagan.

Es raro que desee girar hasta la próxima interrupción, pero si escribe eso en C, eso es definitivamente lo que espera que suceda. (Y qué sucede en GCC y Clang, excepto en Clang cuando el ciclo infinito está dentro de una función contenedora).

Por ejemplo, en un kernel de sistema operativo primitivo, cuando el programador 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 instrucción hlt existía, no sé si ahorró una cantidad significativa de energía hasta que las CPU comenzaron a tener estados inactivos de bajo consumo).

over 4 years ago · Santiago Trujillo Report

0

Lo siguiente parece funcionar para mí:

 #include <stdio.h> __attribute__ ((optnone)) static void die(void) { while (1) ; } int main(void) { printf("begin\n"); die(); printf("unreachable\n"); }

en Godbolt

Decirle explícitamente a Clang que no optimice esa función hace que se emita un bucle infinito como se esperaba. Con suerte, hay una manera de desactivar selectivamente optimizaciones particulares en lugar de simplemente desactivarlas todas así. Sin embargo, Clang todavía se niega a emitir código para el segundo printf . Para obligarlo a hacer eso, tuve que modificar aún más el código dentro de main para:

 volatile int x = 0; if (x == 0) die();

Parece que necesitará 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.

over 4 years ago · Santiago Trujillo Report

0

Otras respuestas ya cubrieron formas de hacer que Clang emita el ciclo infinito, con lenguaje ensamblador en línea u otros efectos secundarios. Solo quiero confirmar que esto fue realmente un error del compilador. Específicamente, fue un error de LLVM de larga data : aplicó el concepto de C ++ de "todos los bucles sin efectos secundarios deben terminar" a idiomas donde no debería, como C. El error finalmente se solucionó en LLVM 12.

Por ejemplo, el lenguaje de programación Rust también permite bucles infinitos y usa LLVM como backend, y tenía el mismo problema.

LLVM 12 agregó un atributo mustprogress que las interfaces pueden omitir para indicar cuándo las funciones no necesariamente regresan, y clang 12 se actualizó para tenerlo en cuenta. Puede ver que su ejemplo se compila correctamente con clang 12.0.0 mientras que no lo hizo con clang 11.0.1

over 4 years ago · Santiago Trujillo Report

0

Solo para que conste, Clang también se porta mal con goto :

 static void die() { nasty: goto nasty; } int main() { int x; printf("begin\n"); die(); printf("unreachable\n"); }

Produce el mismo resultado que en la pregunta, es decir:

 main: # @main push rax mov edi, offset .Lstr call puts .Lstr: .asciz "begin"

Veo que no veo ninguna forma de leer esto según lo permitido en C11, que solo dice:

6.8.6.1(2) Una sentencia goto provoca un salto incondicional a la sentencia precedida por la etiqueta con nombre en la función envolvente.

Como goto no es una "declaración de iteración" (6.8.5 enumera while , do y for ), no se aplica nada sobre las indulgencias especiales de "terminación asumida", como quiera leerlas.

Según la pregunta original, el compilador de enlaces Godbolt es x86-64 Clang 9.0.0 y los indicadores son -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 bastante bien:

 .LC0: .string "begin" main: sub rsp, 8 mov edi, OFFSET FLAT:.LC0 call puts .L2: jmp .L2

Indicadores: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c

over 4 years ago · Santiago Trujillo Report

0

Un ciclo while vacío no tiene efectos secundarios en el sistema.

Por lo tanto, Clang lo elimina. Hay formas "mejores" de lograr el comportamiento deseado que lo obligan a ser más obvio de sus intenciones.

while(1); es malo.

over 4 years ago · Santiago Trujillo Report

0

Una implementación conforme puede, y muchas prácticas lo hacen, imponer límites arbitrarios sobre cuánto tiempo puede ejecutar un programa o cuántas instrucciones ejecutaría, 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: los compiladores no deben asumir que un ciclo while(1) {} sin efectos secundarios ni declaraciones de break terminará. Al contrario de lo que algunas personas podrían pensar, los autores del Estándar no invitaban a los 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 átomos hay 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 sobre la base de que hacerlo podría ser útil y no sería (a diferencia del comportamiento de clang) peor que inútil.

over 4 years ago · Santiago Trujillo Report

0

Parece que se trata de un error en el compilador de Clang. Si no hay ninguna compulsión en la función die() para que sea una función estática, elimine la static y hágala en inline :

 #include <stdio.h> inline void die(void) { while(1) ; } int main(void) { printf("begin\n"); die(); printf("unreachable\n"); }

Funciona como se esperaba cuando se compila con el compilador Clang y también es portátil.

Explorador del compilador (godbolt.org) - clang 9.0.0 -O3 -std=c11 -pedantic-errors

 main: # @main push rax mov edi, offset .Lstr call puts .LBB0_1: # =>This Inner Loop Header: Depth=1 jmp .LBB0_1 .Lstr: .asciz "begin"
over 4 years ago · Santiago Trujillo Report

0

Lo siento si este absurdamente no es el caso, me topé con esta publicación y sé debido a mis años usando la distribución Gentoo Linux que si desea que el compilador no optimice su código, debe usar -O0 (Cero). Tenía curiosidad al respecto, compilé y ejecuté el código anterior, y el ciclo continúa indefinidamente. Compilado usando clang-9:

 cc -O0 -std=c11 test.c -o test
over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!