Optimizando un "while(1);" en C++0x


Actualizado, ver a continuación!

He oído 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. Me parece que esto puede sorprender a mucha gente.

¿Alguien tiene una buena explicación de por qué esto era necesario permitir? Como referencia, el borrador más reciente de C++0x dice en 6.5/5

Un bucle que, fuera de la for-init-statement en el caso de una for statement,

  • no hace 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 aplicación puede suponer que terminará. [Nota: Esto está destinado a permitir la transfor del compilador- maciones, tales como la eliminación de bucles vacíos, incluso cuando la terminación no puede ser probada. - nota final ]

Editar:

Este artículo perspicaz dice sobre ese texto de Estándares

Desafortunadamente, las palabras "comportamiento indefinido" no se usan. Sin embargo, cada vez que el estándar dice "el compilador puede asumir P", se implica que un programa que tiene la propiedad not-P tiene semántica indefinida.

¿Es correcto, y se permite al compilador imprimir "Bye" para el programa anterior?


Hay un aún más perspicaz hilo aquí, que se trata de un cambio análogo a C, comenzó por el Tipo hecho el artículo enlazado anterior. Entre otros datos útiles, presentan una solución que parece aplicarse también a C++0x (Update: Esto ya no funcionará con n3225-ver más abajo!)

endless:
  goto endless;

Parece que a un compilador no se le permite optimizar eso porque no es un bucle, sino un salto. Otro individuo resume el cambio propuesto en C++0x y C201X

Escribiendo un bucle, el el programador está afirmando ya sea que el loop hace algo con comportamiento visible (realiza E / S, accesos objetos volátiles, o realiza sincronización u operaciones atómicas), o que finalmente termina. Si violo esa suposición al escribir un bucle infinito sin efectos secundarios, estoy mintiendo a la compilador, y el comportamiento de mi programa es indefinido. (Si tengo suerte, el compilador podría advertirme al respecto.) El idioma no proporciona (ya no proporciona?) un manera de expresar un bucle infinito sin comportamiento visible.


Actualización del 3.1.2011 con n3225: El Comité trasladó el texto a 1.10/24 y dice

La implementación puede asumir que cualquier subproceso eventualmente hará uno de los siguientes:

  • terminar,
  • hacer una llamada a una función de E/S de biblioteca,
  • acceder o modificar un objeto volátil, o
  • realizar una operación de sincronización o una operación atómica.

El truco goto ya no funcionará!

Author: Johannes Schaub - litb, 2010-08-29

8 answers

¿Alguien tiene una buena explicación de por qué esto era necesario permitir?

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 se refiere tanto al WG14 como al WG21:

Como N1509 señala correctamente, el borrador actual esencialmente da comportamiento indefinido a bucles infinitos en 6.8. 5p6. Un problema importante para hacerlo es que permite que el código se mueva a través de un potencial bucle sin terminación. Por ejemplo, supongamos que tenemos los siguientes bucles, donde count y count2 son variables globales (o han tenido su dirección tomado), y p es una variable local, cuya dirección no se ha tomado:

for (p = q; p != 0; p = p -> next) {
    ++count;
}
for (p = q; p != 0; p = p -> next) {
    ++count2;
}

¿Podrían estos dos bucles ser fusionados y reemplazados por el siguiente bucle?

for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}

Sin la dispensación especial en 6.8. 5p6 para bucles infinitos, esto sería desautorizado: Si el primer bucle no lo hace terminar porque q apunta a una lista circular, el original nunca escribe a count2. Así se podría ejecutar en paralelo con otro subproceso que acceda o actualizaciones count2. Esto ya no es seguro con la versión transformada lo que hace acceso a count2 a pesar del bucle infinito. Así el la transformación potencialmente introduce una carrera de datos.

En casos como este, es muy poco probable que un compilador pueda para probar la terminación del bucle; tendría que entender que q punto a una lista acíclica, que creo que está más allá de la capacidad de la mayoría compiladores convencionales, y a menudo imposible sin todo el programa información.

Las restricciones impuestas por los bucles no terminales son una restricción de la optimización de los bucles de terminación para los que el compilador no puede probar la terminación, así como en la optimización de la realidad bucles no terminantes. Los primeros son mucho más comunes que los último, y a menudo más interesante para optimizar.

Claramente también hay bucles for con una variable de bucle entero en que sería difícil para un compilador probar la terminación, y por lo tanto, sería difícil para el compilador reestructurar los bucles sin 6.8. 5p6. Incluso algo como

for (i = 1; i != 15; i += 2)

O

for (i = 1; i <= 10; i += j)

Parece no trivial de manejar. (En el primer caso, algunos números básicos la teoría es necesaria para probar la terminación, en el último caso, necesitamos saber algo sobre los posibles valores de j para hacerlo. Wrap-around para enteros sin signo puede complicar algo de este razonamiento aún más.)

Este problema parece aplicarse a casi todas las reestructuraciones de bucles transformaciones, incluyendo la paralelización del compilador y transformaciones de optimización de caché, las cuales probablemente ganarán en importancia, y ya son a menudo importantes para el código numérico. Esto parece probable que se convierta en un costo sustancial para el beneficio de ser capaz de escribir loops infinitos de la manera más natural posible, especialmente porque la mayoría de nosotros rara vez escribimos bucles infinitos intencionalmente.

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.

 24
Author: Shafik Yaghmour,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:46:39

Para mí, la justificación relevante es:

Esto está destinado a permitir transfor - maciones del compilador, tales como la eliminación de bucles vacíos, incluso cuando la terminación no puede ser probada.

Presumiblemente, esto se debe a que probar la terminación mecánicamente es difícil, y la incapacidad de probar la terminación dificulta a los compiladores que de otra manera podrían hacer transformaciones útiles, como mover operaciones no dependientes desde antes del bucle a después o viceversa, operaciones post-bucle en un hilo mientras que el bucle se ejecuta en otro, y así sucesivamente. Sin estas transformaciones, un bucle podría bloquear todos los demás subprocesos mientras esperan que el único subproceso termine dicho bucle. (Uso "hilo" libremente para significar cualquier forma de procesamiento paralelo, incluyendo flujos de instrucción VLIW separados.)

EDITAR: Ejemplo tonto:

while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);
}
complex_io_operation();
cout << "Results:" << endl;
cout << x << endl;

Aquí, sería más rápido para un hilo hacer el complex_io_operation mientras que el otro está haciendo todos los cálculos complejos en el bucle. Pero sin la cláusula que has citado, el compilador tiene que probar dos cosas antes de poder hacer la optimización: 1) que complex_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 asumir 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 son por lo general, cosas como bucles impulsados por eventos que acceden a E/S de alguna manera. Como resultado, han pesimizado el caso raro (bucles infinitos) a favor de optimizar el caso más común (bucles no infinitos, pero difíciles de probar mecánicamente no infinitos).

Sin embargo, significa que los bucles infinitos utilizados en ejemplos de aprendizaje sufrirán como resultado, y aumentarán las gotchas en el código de principiante. No puedo decir que esto sea del todo bueno.

EDITAR: con respecto al artículo perspicaz ahora enlace, yo 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 a lo sumo una vez entre puntos de secuencia" es equivalente a "asignar a x más de una vez entre puntos de secuencia es indefinido".

 47
Author: Philip Potter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-28 22:40:28

Creo que la interpretación correcta es la de su edición: los bucles infinitos vacíos son un comportamiento indefinido.

No diría que es un comportamiento particularmente intuitivo, pero esta interpretación tiene más sentido que la alternativa, que al compilador se le permite arbitrariamente ignorar bucles infinitos sin invocar UB.

Si los bucles infinitos son UB, solo significa que los programas no terminantes no se consideran significativos: de acuerdo con C++0x, tienen no semántica.

Eso también tiene cierto sentido. Son un caso especial, donde una serie de efectos secundarios simplemente ya no ocurren (por ejemplo, nunca se devuelve nada de 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 bucle es perfectamente válido si el bucle no tiene efectos secundarios, porque eventualmente, el cálculo se realizará en cualquier caso. Pero si el bucle nunca termina, no podemos reorganiza de forma segura el código a través de él, porque podríamos simplemente cambiar qué operaciones realmente se ejecutan antes de que el programa se cuelgue. A menos que tratemos un programa colgante como UB, eso es.

 14
Author: jalf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-29 04:44:44

Creo que esto está en la línea de este tipo de pregunta , que hace referencia a otro hilo . La optimización puede ocasionalmente eliminar bucles vacíos.

 8
Author: linuxuser27,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:02:11

El problema relevante es que al compilador se le permite reordenar 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 máquina no terminante para el bucle infinito.

Creo que este es el enfoque correcto. La especificación de lenguaje define formas de hacer cumplir el orden de ejecución. Si desea un bucle infinito que no se puede reordenar, escriba esto:

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");
 8
Author: Daniel Newby,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-29 05:20:17

Creo que el problema podría ser mejor dicho, ya que "Si un fragmento de código posterior no depende de un fragmento de código anterior, y el fragmento de código anterior no tiene efectos secundarios en ninguna otra parte del sistema, la salida del compilador puede ejecutar el fragmento de código posterior antes, después o entremezclado con la ejecución del primero, incluso si el primero contiene bucles, sin tener en cuenta cuándo o si el código anterior realmente completaría . Por ejemplo, el compilador podría reescribir:

void testfermat(int n)
{
  int a=1,b=1,c=1;
  while(pow(a,n)+pow(b,n) != pow(c,n))
  {
    if (b > a) a++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++};
  }
  printf("The result is ");
  printf("%d/%d/%d", a,b,c);
}

Como

void testfermat(int n)
{
  if (fork_is_first_thread())
  {
    int a=1,b=1,c=1;
    while(pow(a,n)+pow(b,n) != pow(c,n))
    {
      if (b > a) a++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++};
    }
    signal_other_thread_and_die();
  }
  else // Second thread
  {
    printf("The result is ");
    wait_for_other_thread();
  }
  printf("%d/%d/%d", a,b,c);
}

Generalmente no es irrazonable, aunque podría preocuparme de que:

  int total=0;
  for (i=0; num_reps > i; i++)
  {
    update_progress_bar(i);
    total+=do_something_slow_with_no_side_effects(i);
  }
  show_result(total);

Se convertiría en

  int total=0;
  if (fork_is_first_thread())
  {
    for (i=0; num_reps > i; i++)
      total+=do_something_slow_with_no_side_effects(i);
    signal_other_thread_and_die();
  }
  else
  {
    for (i=0; num_reps > i; i++)
      update_progress_bar(i);
    wait_for_other_thread();
  }
  show_result(total);

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, haría que las actualizaciones de la barra de progreso fueran menos útiles de lo que deberían ser.

 1
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 15:18:42

No es decidible para el compilador para casos no triviales si es un bucle infinito en absoluto.

En diferentes casos, puede suceder que su optimizador alcance una clase de complejidad mejor para su código (por ejemplo, era O(n^2) y obtiene O(n) u O(1) después de la optimización).

Por lo tanto, incluir tal regla que no permite eliminar un bucle infinito en el estándar de C++ haría que muchas optimizaciones fueran imposibles. Y la mayoría de la gente no quiere esto. Creo que esto responde bastante a su pregunta.


Otra cosa: Nunca he visto ningún ejemplo válido donde se necesita un bucle infinito que no hace nada.

El único ejemplo del que he oído hablar fue un truco feo que realmente debería resolverse de otra manera: Se trataba de sistemas embebidos donde la única manera de desencadenar un reinicio era congelar el dispositivo para que el perro guardián lo reinicie automáticamente.

Si conoces algún ejemplo válido/bueno en el que necesites un bucle infinito que no haga nada, por favor dímelo.

 0
Author: Albert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-03 18:36:04

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 producir un comportamiento incorrecto con un nuevo compilador.

En otras palabras, haga que sus globales sean volátiles well así como los argumentos pasados a dicho bucle a través del puntero/referencia.

 0
Author: spraff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-08-25 10:13:21