Actualizado, ver abajo!
He escuchado y leído que C ++ 0x permite que un compilador imprima "Hola" para el siguiente fragmento
#include <iostream>
int main() {
while(1)
;
std::cout << "Hello" << std::endl;
}
Aparentemente tiene algo que ver con hilos y capacidades de optimización. Sin embargo, me parece que esto puede sorprender a muchas personas.
¿Alguien tiene una buena explicación de por qué esto era necesario permitir? Como referencia, el borrador más reciente de C ++ 0x dice en6.5/5
Un bucle que, fuera de la declaración for-init en el caso de una declaración for,
- no realiza llamadas a las funciones de E / S de la biblioteca, y
- no accede ni modifica objetos volátiles, y
- no realiza operaciones de sincronización (1.10) u operaciones atómicas (Cláusula 29)
la implementación puede suponer que termina. [Nota: 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. - nota final]
Editar:
Este artículo perspicaz dice sobre ese texto de Normas
Lamentablemente, las palabras "comportamiento indefinido" no se utilizan. Sin embargo, cada vez que el estándar dice "el compilador puede asumir P", está implícito que un programa que tiene la propiedad no-P tiene una semántica indefinida.
¿Es correcto y el compilador puede imprimir "Bye" para el programa anterior?
Aquí hay un hilo aún más perspicaz , que trata sobre un cambio análogo a C, iniciado por el tipo que hizo el artículo vinculado anterior. Entre otros datos útiles, presentan una solución que parece aplicarse también a C ++ 0x ( Actualización : esto ya no funcionará con n3225, ¡vea a continuación!)
endless:
goto endless;
Un compilador no puede optimizar eso, parece, porque no es un bucle, sino un salto. Otro tipo resume el cambio propuesto en C ++ 0x y C201X
Al escribir un bucle, el programador está afirmando ya sea que el bucle hace algo con el comportamiento visible (realiza de E / S, accesos objetos volátiles, o la sincronización realiza o operaciones atómicas), o que eventualmente termina. Si violo esa suposición escribiendo un bucle infinito sin efectos secundarios, le estoy mintiendo al compilador y el comportamiento de mi programa no está definido. (Si tengo suerte, el compilador podría advertirme al respecto). El lenguaje no proporciona (¿ya no proporciona?) Una forma de expresar un bucle infinito sin un comportamiento visible.
Actualización el 3.1.2011 con n3225: el Comité movió el texto al 1.10 / 24 y dijo
La implementación puede suponer que cualquier hilo eventualmente hará uno de los siguientes:
- Terminar,
- hacer una llamada a una función de E / S de la biblioteca,
- acceder o modificar un objeto volátil, o
- realizar una operación de sincronización o una operación atómica.
¡El goto
truco ya no funcionará!
fuente
while(1) { MyMysteriousFunction(); }
debe ser compilable independientemente sin conocer la definición de esa misteriosa función, ¿verdad? Entonces, ¿cómo podemos determinar si realiza llamadas a alguna función de E / S de la biblioteca? En otras palabras: seguramente esa primera viñeta podría estar redactada no hace llamadas a funciones .int x = 1; for(int i = 0; i < 10; ++i) do_something(&i); x++;
enfor(int i = 0; i < 10; ++i) do_something(&i); int x = 2;
? O posiblemente al revés, conx
ser inicializado a2
antes del bucle. Puede decir quedo_something
no le importa el valor dex
, por lo que es una optimización perfectamente segura, sido_something
no hace que el valor dei
cambie de modo que termine en un bucle infinito.main() { start_daemon_thread(); while(1) { sleep(1000); } }
podría salir inmediatamente en lugar de ejecutar mi demonio en un hilo de fondo?Respuestas:
Sí, Hans Boehm proporciona una justificación para esto en N1528: ¿Por qué un comportamiento indefinido para bucles infinitos? , aunque este es el documento WG14, la justificación se aplica también a C ++ y el documento hace referencia tanto a WG14 como a WG21:
La principal diferencia con C es que C11 proporciona una excepción para controlar expresiones que son expresiones constantes que difiere de C ++ y hace que su ejemplo específico esté bien definido en C11.
fuente
do { x = slowFunctionWithNoSideEffects(x);} while(x != 23);
código de elevación después del ciclo que no dependeríax
, parecería seguro y razonable, pero permitir que un compilador asumax==23
en dicho código parece más peligroso que útil.Para mí, la justificación relevante es:
Presumiblemente, esto se debe a que probar la terminación mecánicamente es difícil , y la imposibilidad de probar la terminación obstaculiza los compiladores que de otro modo podrían hacer transformaciones útiles, como mover operaciones no dependientes desde antes del ciclo hasta después o viceversa, realizar operaciones posteriores al ciclo en un hilo mientras el bucle se ejecuta en otro, y así sucesivamente. Sin estas transformaciones, un bucle podría bloquear todos los otros subprocesos mientras esperan que un subproceso termine dicho bucle. (Utilizo "hilo" libremente para referirme a cualquier forma de procesamiento paralelo, incluidas las secuencias de instrucciones VLIW separadas).
EDITAR: Ejemplo tonto:
Aquí, sería más rápido para un hilo hacer
complex_io_operation
mientras que el otro está haciendo todos los cálculos complejos en el bucle. Pero sin la cláusula que ha citado, el compilador debe probar dos cosas antes de poder realizar la optimización: 1) quecomplex_io_operation()
no depende de los resultados del bucle, y 2) que el bucle terminará . Probar 1) es bastante fácil, probar 2) es el problema de detención. Con la cláusula, puede suponer que el bucle termina y obtener una ganancia de paralelización.También imagino que los diseñadores consideraron que los casos en los que se producen bucles infinitos en el código de producción son muy raros y generalmente son como bucles controlados por eventos que acceden de alguna manera a las E / S. Como resultado, han pesimizado el caso raro (bucles infinitos) a favor de optimizar el caso más común (no infinito, pero difícil de probar mecánicamente bucles no infinitos).
Sin embargo, sí significa que los bucles infinitos utilizados en los ejemplos de aprendizaje sufrirán como resultado y aumentarán las trampas en el código de principiante. No puedo decir que esto sea algo completamente bueno.
EDITAR: con respecto al artículo perspicaz que ahora vincula, diría que "el compilador puede asumir X sobre el programa" es lógicamente equivalente a "si el programa no satisface X, el comportamiento es indefinido". Podemos mostrar esto de la siguiente manera: supongamos que existe un programa que no satisface la propiedad X. ¿Dónde se definiría el comportamiento de este programa? El estándar solo define el comportamiento asumiendo que la propiedad X es verdadera. Aunque el Estándar no declara explícitamente el comportamiento indefinido, lo ha declarado indefinido por omisión.
Considere un argumento similar: "el compilador puede asumir que una variable x solo se asigna como máximo una vez entre puntos de secuencia" es equivalente a "asignar a x más de una vez entre puntos de secuencia no está definido".
fuente
complex_io_operation
.Creo que la interpretación correcta es la de su edición: los bucles infinitos vacíos son comportamientos indefinidos.
No diría que es un comportamiento particularmente intuitivo, pero esta interpretación tiene más sentido que la alternativa, que el compilador puede ignorar arbitrariamente bucles infinitos sin invocar UB.
Si los bucles infinitos son UB, solo significa que los programas sin terminación no se consideran significativos: de acuerdo con C ++ 0x, tienen semántica.
Eso también tiene cierto sentido. Son un caso especial, donde ya no se producen una serie de efectos secundarios (por ejemplo, nunca se devuelve nada
main
), y una serie de optimizaciones del compilador se ven obstaculizadas por tener que preservar bucles infinitos. Por ejemplo, mover cálculos a través del ciclo es perfectamente válido si el ciclo no tiene efectos secundarios, porque eventualmente, el cálculo se realizará en cualquier caso. Pero si el ciclo nunca termina, no podemos reorganizar el código de manera segura, ya que podríamos estar cambiando qué operaciones se ejecutan antes de que el programa se cuelgue. A menos que tratemos un programa colgante como UB, eso es.fuente
while(1){...}
. Incluso usan habitualmentewhile(1);
para invocar un reinicio asistido por un perro guardián.Creo que esto está en la línea de este tipo de pregunta , que hace referencia a otro hilo . La optimización ocasionalmente puede eliminar bucles vacíos.
fuente
X
y patrón de bitsx
, el compilador puede demostrar que el bucle no sale sinX
mantener el patrón de bitsx
. Es vacuamente cierto. PorX
lo tanto, podría considerarse que tiene cualquier patrón de bits, y eso es tan malo como UB en el sentido de que por el errorX
yx
rápidamente causará algunos. Así que creo que debes ser más preciso en tu estándar. Es difícil hablar sobre lo que sucede "al final de" un ciclo infinito, y mostrarlo equivalente a una operación finita.El problema relevante es que el compilador puede reordenar el código cuyos efectos secundarios no entran en conflicto. El sorprendente orden de ejecución podría ocurrir incluso si el compilador produjera código de máquina sin terminación para el bucle infinito.
Creo que este es el enfoque correcto. La especificación del lenguaje define formas de imponer el orden de ejecución. Si desea un bucle infinito que no se puede reordenar, escriba esto:
fuente
unsigned int dummy; while(1){dummy++;} fprintf(stderror,"Hey\r\n"); fprintf(stderror,"Result was %u\r\n",dummy);
, el primerofprintf
podría ejecutarse, pero el segundo no (el compilador podría mover el cálculodummy
entre los dosfprintf
, pero no más allá del que imprime su valor).Creo que el problema tal vez podría plantearse mejor como: "Si un código posterior no depende de un código anterior y el código anterior no tiene efectos secundarios en ninguna otra parte del sistema, la salida del compilador puede ejecutar el último código antes, después o entremezclado con la ejecución del primero, incluso si el primero contiene bucles, independientemente de cuándo o si el código anterior realmente se completaría . Por ejemplo, el compilador podría reescribir:
como
En general, no es irrazonable, aunque me preocuparía que:
se convertiría
Al hacer que una CPU maneje los cálculos y otra maneje las actualizaciones de la barra de progreso, la reescritura mejoraría la eficiencia. Desafortunadamente, las actualizaciones de la barra de progreso serían menos útiles de lo que deberían ser.
fuente
No es decidible para el compilador para casos no triviales si es un bucle infinito.
En diferentes casos, puede suceder que su optimizador alcance una mejor clase de complejidad para su código (por ejemplo, fue O (n ^ 2) y obtendrá O (n) u O (1) después de la optimización).
Por lo tanto, incluir una regla que no permita eliminar un bucle infinito en el estándar C ++ haría imposible muchas optimizaciones. Y la mayoría de la gente no quiere esto. Creo que esto responde bastante a tu pregunta.
Otra cosa: nunca he visto ningún ejemplo válido en el que necesite un bucle infinito que no hace nada.
El único ejemplo que escuché fue un hack feo que realmente debería resolverse de otra manera: se trataba de sistemas embebidos donde la única forma de activar un reinicio era congelar el dispositivo para que el perro guardián lo reinicie automáticamente.
Si conoce algún ejemplo válido / bueno donde necesita un bucle infinito que no hace nada, por favor dígame.
fuente
Creo que vale la pena señalar que los bucles que serían infinitos, excepto por el hecho de que interactúan con otros hilos a través de variables no volátiles y no sincronizadas, ahora pueden generar un comportamiento incorrecto con un nuevo compilador.
En otras palabras, haga que sus globales sean volátiles, así como los argumentos pasados a ese bucle a través de un puntero / referencia.
fuente
volatile
no es ni necesaria ni suffiicent, y duele el rendimiento de forma espectacular.