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

131

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 ud2trampa 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 volatilevariable, 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 beginseguido de unreachablecomo 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?

nneonneo
fuente
3
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Bhargav Rao
2
No hay una solución portátil que no implique un efecto secundario. Si no desea un acceso a la memoria, su mejor esperanza sería registrar caracteres volátiles sin firmar; pero el registro desaparece en C ++ 17.
Scott M
25
Tal vez esto no esté dentro del alcance de la pregunta, pero tengo curiosidad por qué quieres hacer esto. Seguramente hay alguna otra forma de lograr tu tarea real. ¿O es esto solo de naturaleza académica?
Cruncher
1
@Cruncher: los efectos de cualquier intento particular de ejecutar un programa pueden ser útiles, esencialmente inútiles o sustancialmente peores que inútiles. Una ejecución que da como resultado que un programa se atasque en un bucle sin fin puede ser inútil, pero aún así es preferible a otros comportamientos que un compilador podría sustituir.
supercat
66
@Cruncher: Debido a que el código podría estar ejecutándose en un contexto independiente donde no existe un concepto 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.
supercat

Respuestas:

77

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

Una declaración de iteración cuya expresión controladora no es una expresión constante, 156) que no realiza operaciones de entrada / salida, no accede a objetos volátiles y no realiza operaciones de sincronización u operaciones atómicas en su cuerpo, expresión de control o (en el caso de un para declaración) su expresión-3, puede ser asumida 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 diferente 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 pueda 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 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

.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 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:

#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 _Noreturnen un intento de ayudar al compilador más adelante. Debe quedar claro que esta función colgará, solo de esa palabra clave.

setjmpdevolverá 0 en la primera ejecución, por lo que este programa simplemente debería estrellarse while(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.).

Lundin
fuente
15
No estoy de acuerdo con "esta es una expresión constante cristalina, por lo que la implementación no puede suponer que termine" . Esto realmente entra en la abogacía de lenguaje quisquilloso, pero 6.8.5/6tiene 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 ...
kabanus
77
@kabanus La parte citada es un caso especial. Si no (el caso especial), evalúe y secuencia el código como lo haría normalmente. Si continúa leyendo el mismo capítulo, la expresión de control se evalúa según lo especificado para cada declaración de iteración ("según lo especificado por la semántica") con la excepción del caso especial citado. Sigue las mismas reglas que la evaluación de cualquier cálculo de valor, que está secuenciado y bien definido.
Lundin
2
Estoy de acuerdo, pero no se sorprendería de que int z=3; int y=2; int x=1; printf("%d %d\n", x, z);no haya nada 2en el ensamblaje, por lo que en el sentido inútil vacío xno se asignó después ysino después zdebido 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).
kabanus
2
@MSalters Se eliminó uno de mis comentarios, pero gracias por los comentarios, y estoy de acuerdo. Lo que dijo mi comentario es que creo que este es el corazón del debate: es while(1);lo mismo que una int 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.
kabanus
2
"Tal implementación se rompería irremediablemente, ya que los bucles 'para siempre' son una construcción de programación común". - Entiendo el sentimiento, pero el argumento es defectuoso porque podría aplicarse de manera idéntica a C ++, sin embargo, un compilador de C ++ que optimizó este bucle no estaría roto sino conforme.
Konrad Rudolph el
52

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

La solución más simple:

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

Enlace Godbolt

P__J__
fuente
21
Sin embargo, no explica por qué está actuando el ruido metálico.
Lundin
44
Sin embargo, solo decir "es un error en el sonido metálico" es suficiente. Sin embargo, me gustaría probar algunas cosas aquí primero, antes de gritar "error".
Lundin
3
@Lundin No sé si es un error. La norma no es técnicamente precisa en este caso
P__J__
44
Afortunadamente, GCC es de código abierto y puedo escribir un compilador que optimice su ejemplo. Y podría hacerlo para cualquier ejemplo que se te ocurra, ahora y en el futuro.
Thomas Weller
3
@ThomasWeller: los desarrolladores de GCC no aceptarían un parche que optimice este ciclo; violaría el comportamiento documentado = garantizado. Vea mi comentario anterior: asm("")está implícitamente asm 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 como asm("mfence")o cli.)
Peter Cordes
50

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.sideeffectcó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.

Arnavion
fuente
55
Nada como el olor de un error que tiene más de una década ... con múltiples correcciones y parches propuestos ... pero aún no se ha solucionado.
Ian Kemp
44
@IanKemp: Para que ellos arreglen el error ahora requeriría reconocer que han tardado diez años en solucionarlo. Es mejor mantener la esperanza de que la Norma cambie para justificar su comportamiento. Por supuesto, incluso si el estándar cambiara, eso aún no justificaría su comportamiento, excepto a los ojos de las personas que considerarían el cambio al Estándar como una indicación de que el mandato de comportamiento anterior del Estándar era un defecto que debería corregirse retroactivamente.
supercat
44
Se ha "arreglado" en el sentido de que LLVM agregó la sideeffectoperació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 fusionar sideeffectoperaciones 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.
Arnavion
@Arnavion: ¿Hay alguna forma de indicar que las operaciones pueden diferirse a menos que se utilicen los resultados o hasta que se usen los resultados, pero que si los datos provocan que un programa se repita sin cesar, intentar continuar con las dependencias de datos anteriores hará que el programa sea peor que inútil ? Tener que agregar efectos secundarios falsos que evitarían las optimizaciones útiles anteriores para evitar que el optimizador haga un programa peor que inútil no parece una receta para la eficiencia.
supercat
Esa discusión probablemente pertenece a las listas de correo LLVM / clang. FWIW el compromiso LLVM que agregó la operación también enseñó varios pases de optimización al respecto. Además, Rust experimentó insertando sideeffectoperaciones 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.
Arnavion
32

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=gnu11vs. -std=gnu99no 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 asmdeclaració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 mainque llama infloop() { while(1); }al mismo primero puts, pero luego deja de emitir instrucciones para maindespué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 labelbucle infinito
  • o (si aceptamos que se puede eliminar el bucle infinito) emitir otra llamada para imprimir la segunda cadena, y luego return 0desde 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 hltinstrucción existía, IDK si ahorraba una cantidad significativa de energía hasta que las CPU comenzaron a tener estados inactivos de baja potencia).

Peter Cordes
fuente
1
Por curiosidad, ¿alguien está usando realmente el sonido metálico para sistemas integrados? Nunca lo he visto y trabajo exclusivamente con incrustado. gcc solo "recientemente" (hace 10 años) ingresó al mercado integrado y lo uso con escepticismo, preferiblemente con bajas optimizaciones y siempre con -ffreestanding -fno-strict-aliasing. Funciona bien con ARM y quizás con AVR heredado.
Lundin
1
@Lundin: IDK sobre incrustado, pero sí, la gente construye núcleos con clang, al menos a veces Linux. Presumiblemente también Darwin para MacOS.
Peter Cordes
2
bugs.llvm.org/show_bug.cgi?id=965 este error parece relevante, pero no estoy seguro de que sea lo que estamos viendo aquí.
bracco23
1
@lundin: estoy bastante seguro de que usamos GCC (y muchos otros kits de herramientas) para el trabajo integrado a lo largo de los años 90, con RTOS como VxWorks y PSOS. No entiendo por qué dice que GCC solo ingresó al mercado integrado recientemente.
Jeff Learman
1
@JeffLearman ¿Se convirtió en la corriente principal recientemente, entonces? De todos modos, el fiasco de alias estricto de gcc solo ocurrió después de la introducción de C99, y las versiones más nuevas de él ya no parecen volverse locas al encontrar violaciones de alias estrictas. Aún así, me mantengo escéptico cada vez que lo uso. En cuanto al sonido metálico, la última versión está evidentemente completamente rota cuando se trata de bucles eternos, por lo que no se puede usar para sistemas integrados.
Lundin
14

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 gotodeclaración provoca un salto incondicional a la declaración prefijada por la etiqueta nombrada en la función de cierre.

Como gotono es una "declaración de iteración" (listas de 6.8.5 while, doy for) 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:

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

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

jonathanjo
fuente
Una implementación conforme podría tener un límite de traducción no documentado en el tiempo de ejecución o ciclos de la CPU que podría causar un comportamiento arbitrario si se excede, o si las entradas de un programa hacen que sea inevitable exceder el límite. Tales cosas son un problema de calidad de implementación, fuera de la jurisdicción de la norma. Parecería extraño que los mantenedores de clang fueran tan insistentes en su derecho a producir una implementación de baja calidad, pero el Estándar lo permite.
supercat
2
@supercat gracias por comentar ... ¿por qué exceder un límite de traducción haría otra cosa que no pasar la fase de traducción y negarse a ejecutar? Además: " 5.1.1.3 Diagnóstico Una implementación conforme producirá ... mensaje de diagnóstico ... si una unidad de traducción o unidad de traducción de preprocesamiento contiene una violación de cualquier regla o restricción de sintaxis ...". No puedo ver cómo puede comportarse un comportamiento erróneo en la fase de ejecución.
Jonathan
El estándar sería completamente imposible de implementar si los límites de implementación tuvieran que resolverse en el momento de la construcción, ya que uno podría escribir un programa estrictamente conforme que requeriría más bytes de pila que átomos en el universo. No está claro si las limitaciones de tiempo de ejecución deben agruparse con "límites de traducción", pero tal concesión es claramente necesaria, y no hay otra categoría en la que se pueda poner.
supercat
1
Estaba respondiendo a tu comentario sobre los "límites de traducción". Por supuesto, también hay límites de ejecución, confieso que no entiendo por qué sugieres que deberían agruparse con límites de traducción o por qué dices que es necesario. Simplemente no veo ninguna razón para decir que nasty: goto nastypuede conformarse y no hacer girar las CPU hasta que intervenga el agotamiento de los recursos o del usuario.
Jonathan
1
La Norma no hace referencia a los "límites de ejecución" que pude encontrar. Cosas como el anidamiento de llamadas a funciones generalmente se manejan mediante la asignación de la pila, pero una implementación conforme que limite las llamadas a funciones a una profundidad de 16 podría generar 16 copias de cada función y hacer que una llamada a bar()dentro foo()se procese como una llamada de __1fooa __2bar, de __2fooa __3bar, etc. y de __16fooa__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.
supercat
5

Interpretaré al 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 controladora no es una expresión constante, 156) que no realiza operaciones de entrada / salida, no accede a objetos volátiles y no realiza operaciones de sincronización u operaciones atómicas en su cuerpo, expresión de control o (en el caso de un para declaración) su expresión-3, puede ser asumida 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 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)o while(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:

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 los 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();
}

ser omitido Curiosamente, el sonido metálico lo omite, y gcc no .

PSkocik
fuente
"Esto no dice nada sobre lo que sucede si no se cumplen los criterios" Pero sí, 6.8.5.1 La declaración while: "La evaluación de la expresión de control tiene lugar antes de cada ejecución del cuerpo del bucle". Eso es. Este es un cálculo de valor (de una expresión constante), cae bajo la regla de la máquina abstracta 5.1.2.3 que define el término evaluación: "La evaluación de una expresión en general incluye tanto los cálculos de valor como el inicio de los efectos secundarios". Y de acuerdo con el mismo capítulo, todas esas evaluaciones se secuencian y evalúan según lo especificado por la semántica.
Lundin
1
@Lundin Entonces, ¿ while(1){}hay una secuencia infinita de 1evaluaciones 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. Si while(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.
PSkocik
1
Creo que se especifica, si trata la 1condición como un cálculo de valor. El tiempo de ejecución no importa: lo que importa es lo que while(A){} B;puede no optimizarse por completo, no optimizarse B;ni volver a secuenciarse B; 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 de Ase usa claramente (por el bucle).
Lundin
2
+1 Aunque me parece que "la ejecución se cuelga indefinidamente sin ningún resultado" es un "efecto secundario" en cualquier definición de "efecto secundario" que tiene sentido y es útil más allá del estándar en el vacío, esto ayuda a explicar la mentalidad desde la que puede tener sentido para alguien.
mtraceur
1
Cerca de "optimizar un bucle infinito" : no está del todo claro si "eso" se refiere al estándar o al compilador, ¿quizás reformular? Dado "aunque probablemente debería" y no "aunque probablemente no debería" , es probablemente el estándar al que se refiere "eso" .
Peter Mortensen
2

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 unreachabledeclaració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:

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"

entonces 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 gotoetc ... En este punto me rendiría. Si debes usar clang

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

hace 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

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

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.

kabanus
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Bhargav Rao
Re "eliminar todo error" : ¿Te refieres a " error viejo " ?
Peter Mortensen
@PeterMortensen "ole" estaría bien conmigo también.
kabanus
2

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ínela staticy hágalo 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 de 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"
HS
fuente
¿Qué hay de static inline?
SS Anne
1

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");
}

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 interno mainpara:

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

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.

bta
fuente
1
No es necesario printfque se genere el segundo si el ciclo realmente dura para siempre, porque en ese caso el segundo printfes 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).
nneonneo
Documentos de GCC __attribute__ ((optimize(1))), pero clang lo ignora como no compatible: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes
0

Una 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 ni breakdeclaraciones 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.

Super gato
fuente
-2

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.

David Schwartz
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Samuel Liew
44
A juzgar por la experiencia que tiene (por su perfil), solo puedo concluir que esta publicación está escrita de mala fe solo para defender al compilador. Estás discutiendo seriamente que algo que lleva una cantidad infinita de tiempo puede optimizarse para ejecutarse en la mitad del tiempo. Eso es ridículo en todos los niveles y lo sabes.
tubería
@pipe: Creo que los mantenedores de clang y gcc esperan que una versión futura de la Norma haga permisible el comportamiento de sus compiladores, y los mantenedores de esos compiladores podrán fingir que tal cambio fue simplemente una corrección de un defecto de larga data. en el estándar. Así es como han tratado las garantías de secuencia inicial común de C89, por ejemplo.
supercat
@SSAnne: Hmm ... no creo que sea suficiente para bloquear algunas de las inferencias poco sólidas gcc y clang de los resultados de las comparaciones de igualdad de punteros.
supercat
@supercat Hay <s> otros </s> toneladas.
SS Anne
-2

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:

cc -O0 -std=c11 test.c -o test
Fellipe Weno
fuente
1
El punto es hacer un bucle infinito con optimizaciones habilitadas.
SS Anne
-4

Un whilebucle 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.

Jameis famosos
fuente
66
En muchas construcciones incrustadas, no existe el concepto de abort()o exit(). 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 una while(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.
supercat
¿Hay alguna manera de que puedas ser más explícito de tus intenciones? el optimizador está ahí para optimizar su programa, y ​​eliminar bucles redundantes que no hacen nada ES una optimización. Esta es realmente una diferencia filosófica entre el pensamiento abstracto del mundo de las matemáticas y el mundo de la ingeniería más aplicada.
Famoso Jameis
La mayoría de los programas tienen un conjunto de acciones útiles que deben realizar cuando sea posible, y un conjunto de acciones peores que inútiles que nunca deben realizar bajo ninguna circunstancia. Muchos programas tienen un conjunto de comportamientos aceptables en cualquier caso particular, uno de los cuales, si el tiempo de ejecución no es observable, siempre sería "esperar algo arbitrario y luego realizar alguna acción del conjunto". Si todas las acciones que no sean esperar están en el conjunto de acciones peores que inútiles, no habría un número de segundos N para los cuales "esperar para siempre" sería notablemente diferente de ...
supercat
... "espere N + 1 segundos y luego realice alguna otra acción", por lo que no sería observable el conjunto de acciones tolerables que no sean esperar. Por otro lado, si un código elimina alguna acción intolerable del conjunto de acciones posibles, y una de esas acciones se realiza de todos modos , eso debería considerarse observable. Desafortunadamente, las reglas de lenguaje C y C ++ usan la palabra "asumir" de una manera extraña a diferencia de cualquier otro campo de lógica o esfuerzo humano que pueda identificar.
supercat
1
@FamousJame está bien, pero Clang no solo elimina el bucle, sino que analiza estáticamente todo como inalcanzable y emite una instrucción no válida. Eso no es lo que espera si simplemente "eliminó" el bucle.
nneonneo