Otimizando um "tempo (1)"; em C ++ 0x

153

Atualizado, veja abaixo!

Ouvi e li que o C ++ 0x permite que um compilador imprima "Olá" para o seguinte trecho

#include <iostream>

int main() {
  while(1) 
    ;
  std::cout << "Hello" << std::endl;
}

Aparentemente, tem algo a ver com threads e recursos de otimização. Parece-me que isso pode surpreender muitas pessoas.

Alguém tem uma boa explicação de por que isso era necessário? Para referência, o rascunho mais recente do C ++ 0x diz em6.5/5

Um loop que, fora da instrução for-init no caso de uma instrução for,

  • não faz chamadas para funções de E / S da biblioteca e
  • não acessa ou modifica objetos voláteis e
  • não executa operações de sincronização (1.10) ou operações atômicas (Cláusula 29)

pode ser assumido pela implementação para terminar. [Nota: Pretende-se permitir transformações do compilador, como remoção de loops vazios, mesmo quando a terminação não puder ser comprovada. - nota final]

Editar:

Este artigo esclarecedor diz sobre o texto dos Padrões

Infelizmente, as palavras "comportamento indefinido" não são usadas. No entanto, sempre que o padrão diz "o compilador pode assumir P", está implícito que um programa que possui a propriedade not-P possui semântica indefinida.

Isso está correto e o compilador pode imprimir "Tchau" para o programa acima?


Há um tópico ainda mais perspicaz aqui , que trata de uma alteração análoga a C, iniciada pelo Guy que fez o artigo acima. Entre outros fatos úteis, eles apresentam uma solução que parece se aplicar também ao C ++ 0x ( atualização : isso não funcionará mais com o n3225 - veja abaixo!)

endless:
  goto endless;

Um compilador não pode otimizar isso, ao que parece, porque não é um loop, mas um salto. Outro cara resume a alteração proposta em C ++ 0x e C201X

Ao escrever um loop, o programador está afirmando quer que o loop faz algo com o comportamento visível (executa E / S, acessa objetos voláteis, ou sincronização executa ou operações atômicas), ou que, eventualmente, termina. Se eu violar essa suposição escrevendo um loop infinito sem efeitos colaterais, minto para o compilador e o comportamento do meu programa é indefinido. (Se eu tiver sorte, o compilador poderá me avisar.) A linguagem não fornece (não fornece mais?) Uma maneira de expressar um loop infinito sem comportamento visível.


Atualização em 3.1.2011 com n3225: o Comitê mudou o texto para 1.10 / 24 e diz

A implementação pode assumir que qualquer encadeamento acabará por executar um dos seguintes procedimentos:

  • terminar,
  • faça uma chamada para uma função de E / S da biblioteca,
  • acessar ou modificar um objeto volátil, ou
  • executar uma operação de sincronização ou uma operação atômica.

O gototruque não vai mais funcionar!

Johannes Schaub - litb
fonte
4
while(1) { MyMysteriousFunction(); }deve ser compilável independentemente, sem conhecer a definição dessa função misteriosa, certo? Então, como podemos determinar se ele faz chamadas para qualquer função de E / S da biblioteca? Em outras palavras: certamente que a primeira bala poderia ser redigida não faz chamadas para funções .
Daniel Earwicker
19
@ Daniel: Se tiver acesso à definição da função, poderá provar muitas coisas. Existe uma otimização interprocedural.
Potatoswatter
3
Agora, em C ++ 03, é um compilador permitido mudar int x = 1; for(int i = 0; i < 10; ++i) do_something(&i); x++;para for(int i = 0; i < 10; ++i) do_something(&i); int x = 2;? Ou possivelmente o contrário, com xa inicialização 2antes do loop. Ele pode dizer do_somethingque não se importa com o valor de x, portanto é uma otimização perfeitamente segura, se do_something não fizer com que o valor iseja alterado de forma que você termine em um loop infinito.
Dennis Zickefoose
4
Então, isso significa que main() { start_daemon_thread(); while(1) { sleep(1000); } }pode sair imediatamente, em vez de executar meu daemon em um encadeamento em segundo plano?
Gabe
2
"Este artigo perspicaz" pressupõe que um comportamento específico seja Comportamento indefinido apenas porque não há comportamento definido explícito. Essa é uma suposição incorreta. Em geral, quando o padrão deixa em aberto um número finito de comportamentos, uma implementação deve escolher qualquer um deles ( comportamento não especificado ). Isso não precisa ser determinístico. Se um loop de não fazer termina é sem dúvida uma opção booleana; ou faz ou não. Fazer outra coisa não é permitido.
MSalters 30/08/10

Respostas:

33

Alguém tem uma boa explicação de por que isso era necessário?

Sim, Hans Boehm fornece uma justificativa para isso no N1528: Por que comportamento indefinido para loops infinitos? , embora este seja o documento do WG14, a lógica se aplica também ao C ++ e o documento se refere ao WG14 e WG21:

Como o N1509 corretamente aponta, o rascunho atual basicamente oferece um comportamento indefinido a loops infinitos em 6.8.5p6. Uma questão importante para fazer isso é que ele permite que o código se mova através de um loop potencialmente não terminável. Por exemplo, suponha que tenhamos os seguintes loops, em que count e count2 são variáveis ​​globais (ou tiveram seu endereço obtido) ep é uma variável local, cujo endereço não foi utilizado:

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

Esses dois loops poderiam ser mesclados e substituídos pelo seguinte loop?

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

Sem a dispensação especial em 6.8.5p6 para loops infinitos, isso não seria permitido: se o primeiro loop não terminar porque q aponta para uma lista circular, o original nunca grava em count2. Assim, poderia ser executado em paralelo com outro encadeamento que acessa ou atualiza o count2. Isso não é mais seguro com a versão transformada, que acessa count2, apesar do loop infinito. Assim, a transformação potencialmente introduz uma corrida de dados.

Em casos como esse, é muito improvável que um compilador seja capaz de provar o encerramento do loop; teria que entender que q aponta para uma lista acíclica, que acredito estar além da capacidade da maioria dos compiladores convencionais, e geralmente impossível sem informações completas do programa.

As restrições impostas pelos loops não termináveis ​​são uma restrição à otimização dos loops finais, para os quais o compilador não pode provar a finalização, bem como à otimização dos loops realmente não finais. Os primeiros são muito mais comuns que os segundos e, muitas vezes, mais interessantes para otimizar.

Claramente, existem também for-loops com uma variável de loop inteiro na qual seria difícil para um compilador provar a terminação e, portanto, seria difícil para o compilador reestruturar loops sem 6.8.5p6. Mesmo algo como

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

ou

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

parece não trivial de lidar. (No primeiro caso, é necessária alguma teoria básica dos números para provar a terminação, no último caso, precisamos saber algo sobre os possíveis valores de j para fazê-lo. A quebra automática de números inteiros não assinados pode complicar ainda mais alguns desses raciocínios. )

Esse problema parece se aplicar a quase todas as transformações de reestruturação de loop, incluindo paralelismo de compilador e otimização de cache, as quais provavelmente ganharão importância e já são importantes para o código numérico. Parece provável que isso se transforme em um custo substancial pelo benefício de poder gravar loops infinitos da maneira mais natural possível, especialmente porque a maioria de nós raramente escreve loops infinitamente intencionais.

A principal diferença com C é que C11 fornece uma exceção para controlar expressões que são expressões constantes que difere de C ++ e torna seu exemplo específico bem definido em C11.

Shafik Yaghmour
fonte
1
Existem otimizações seguras e úteis que são facilitadas pela linguagem atual e que também não seriam facilitadas dizendo "Se o término de um loop depende do estado de qualquer objeto, o tempo necessário para executar o loop não é considerado um efeito colateral observável, mesmo que esse tempo seja infinito ". Dado o do { x = slowFunctionWithNoSideEffects(x);} while(x != 23);código de elevação após o loop, do qual não dependeria, xseria seguro e razoável, mas permitir que um compilador assuma x==23esse código parece mais perigoso do que útil.
Supercat
47

Para mim, a justificativa relevante é:

Isso visa permitir as transformações do compilador, como a remoção de loops vazios, mesmo quando a terminação não puder ser comprovada.

Presumivelmente, isso ocorre porque provar a terminação mecanicamente é difícil , e a incapacidade de provar a terminação dificulta os compiladores que poderiam fazer transformações úteis, como mover operações não dependentes de antes do loop para depois ou vice-versa, executando operações pós-loop em um thread enquanto o loop é executado em outro e assim por diante. Sem essas transformações, um loop pode bloquear todos os outros encadeamentos enquanto eles esperam que um encadeamento termine o referido loop. (Eu uso "thread" livremente para significar qualquer forma de processamento paralelo, incluindo fluxos de instruções VLIW separados.)

EDIT: Exemplo idiota:

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

Aqui, seria mais rápido para um thread fazer o complex_io_operationmesmo enquanto o outro está fazendo todos os cálculos complexos no loop. Mas sem a cláusula que você citou, o compilador precisa provar duas coisas antes de fazer a otimização: 1) que complex_io_operation()não depende dos resultados do loop e 2) que o loop será encerrado . A prova 1) é bem fácil, a prova 2) é o problema da parada. Com a cláusula, pode assumir que o loop termina e obter uma vitória de paralelização.

Também imagino que os projetistas consideraram que os casos em que ocorrem loops infinitos no código de produção são muito raros e geralmente são como loops controlados por eventos que acessam a E / S de alguma maneira. Como resultado, eles pessimizaram o caso raro (loops infinitos) em favor da otimização do caso mais comum (não-infinito, mas difícil de provar mecanicamente, loops não-infinitos).

No entanto, isso significa que loops infinitos usados ​​em exemplos de aprendizado sofrerão como resultado e aumentarão as pegadinhas no código para iniciantes. Não posso dizer que isso é uma coisa totalmente boa.

EDIT: com relação ao artigo perspicaz que você vincula agora, eu diria que "o compilador pode assumir X sobre o programa" é logicamente equivalente a "se o programa não satisfizer X, o comportamento será indefinido". Podemos mostrar o seguinte: suponha que exista um programa que não satisfaça a propriedade X. Onde seria definido o comportamento desse programa? O Padrão define apenas o comportamento assumindo que a propriedade X é verdadeira. Embora o Padrão não declare explicitamente o comportamento indefinido, ele o declarou indefinido por omissão.

Considere um argumento semelhante: "o compilador pode assumir que uma variável x é atribuída apenas no máximo uma vez entre os pontos de sequência" é equivalente a "atribuir x mais de uma vez entre os pontos de sequência é indefinida".

Philip Potter
fonte
"Provar 1) é bastante fácil" - na verdade, não se segue imediatamente das três condições para o compilador poder assumir a terminação do loop sob a cláusula que Johannes está perguntando? Eu acho que eles equivalem a "o loop não tem efeito observável, exceto talvez girando para sempre", e a cláusula garante que "girar para sempre" não seja um comportamento garantido para esses loops.
precisa
@ Steve: é fácil se o loop não terminar; mas se o loop terminar, pode ter um comportamento não trivial que afeta o processamento do complex_io_operation.
Philip Potter
Ops, sim, eu senti falta de que isso pode modificar locais / aliases / voláteis não voláteis / o que for usado na operação de IO. Então, você está certo: embora isso não ocorra necessariamente, há muitos casos em que os compiladores podem e provam que essa modificação não ocorre.
Steve Jessop
"Isso significa, no entanto, que infinitos loops usados ​​em exemplos de aprendizado sofrerão como resultado e aumentarão as pegadinhas no código para iniciantes. Não posso dizer que isso seja uma coisa totalmente boa." Apenas compilar com otimizações fora e ele ainda deve trabalhar
KitsuneYMG
1
@ supercat: O que você descreve é ​​o que acontecerá na prática, mas não é o que o rascunho da norma exige. Não podemos assumir que o compilador não saiba se um loop será encerrado. Se o compilador faz saber o loop não irá terminar, ele pode fazer o que gosta. O DS9K vai criar demons nasais para qualquer ciclo infinito com qualquer de I / O, etc (Por conseguinte, os resolve DS9K o problema da paragem.)
Philip Potter
15

Eu acho que a interpretação correta é a da sua edição: loops infinitos vazios são um comportamento indefinido.

Eu não diria que é um comportamento particularmente intuitivo, mas essa interpretação faz mais sentido do que a alternativa, que o compilador é arbitrariamente autorizado a ignorar loops infinitos sem chamar o UB.

Se loops infinitos forem UB, isso significa apenas que programas não termináveis ​​não são considerados significativos: de acordo com o C ++ 0x, eles não têm semântica.

Isso também faz certo sentido. Eles são um caso especial, onde vários efeitos colaterais simplesmente não ocorrem mais (por exemplo, nada é retornado main) e várias otimizações do compilador são dificultadas por preservar loops infinitos. Por exemplo, mover os cálculos pelo loop é perfeitamente válido se o loop não tiver efeitos colaterais, porque, eventualmente, o cálculo será realizado em qualquer caso. Mas se o loop nunca terminar, não poderemos reorganizar o código com segurança, pois podemos estar apenas alterando quais operações realmente são executadas antes que o programa seja interrompido. A menos que tratemos um programa suspenso como UB, isso é.

jalf
fonte
7
"Loops infinitos vazios são um comportamento indefinido"? Alan Turing imploraria para diferir, mas apenas quando ele acabou de girar em seu túmulo.
Donal Fellows
11
@Donal: Eu nunca disse nada sobre sua semântica em uma máquina de Turing. Estamos discutindo a semântica de um loop infinito sem efeitos colaterais em C ++ . E enquanto eu o leio, o C ++ 0x escolhe dizer que esses loops são indefinidos.
jalf
Loops infinitos vazios seriam bobos, e não haveria razão para ter regras especiais para eles. A regra é projetada para lidar com loops úteis de duração ilimitada (espero que não seja infinita), que calculam algo que será necessário no futuro, mas não imediatamente.
supercat 02/09/10
1
Isso significa que o C ++ 0x não é adequado para dispositivos incorporados? Quase todos os dispositivos incorporados não têm terminação e fazem seu trabalho dentro de uma grande quantidade de gordura while(1){...}. Eles até usam rotineiramente while(1);para invocar uma redefinição assistida por watchdog.
vsz
1
@ VSZ: a primeira forma é boa. Loops infinitos são perfeitamente bem definidos, desde que tenham algum tipo de comportamento observável. A segunda forma é mais complicada, mas posso pensar em duas maneiras muito fáceis: (1) um compilador direcionado a dispositivos incorporados pode optar por definir um comportamento mais rigoroso nesse caso, ou (2) você cria um corpo que chama alguma função de biblioteca fictícia . Desde que o compilador não saiba o que essa função faz, ele deve assumir que pode ter algum efeito colateral e, portanto, não pode mexer com o loop.
jalf
8

Eu acho que isso acontece ao longo deste tipo de pergunta , que faz referência a outro segmento . A otimização pode ocasionalmente remover loops vazios.

linuxuser27
fonte
3
Boa pergunta. Parece que esse cara teve exatamente o problema que este parágrafo permite que esse compilador cause. Na discussão vinculada de uma das respostas, está escrito que "Infelizmente, as palavras 'comportamento indefinido' não são usadas. No entanto, sempre que o padrão diz 'o compilador pode assumir P' ', está implícito que um programa que possui o a propriedade not-P possui semântica indefinida ". . Isso me surpreende. Isso significa que o meu exemplo de programa acima tem um comportamento indefinido e pode simplesmente falhar do nada?
Johannes Schaub - litb 28/08
@ Joanesburgo: o texto "pode ​​ser assumido" não ocorre em nenhum outro lugar do rascunho que tenho à mão, e "pode ​​assumir" ocorre apenas algumas vezes. Embora eu tenha verificado isso com uma função de pesquisa que não corresponde às quebras de linha, talvez eu tenha perdido algumas. Portanto, não tenho certeza de que a generalização do autor se justifique com as evidências, mas como matemático tenho que admitir a lógica do argumento, que, se o compilador assumir algo falso, em geral, poderá deduzir qualquer coisa ...
Steve Jessop
... Permitir uma contradição no raciocínio do compilador sobre o programa certamente indica muito fortemente a UB, pois, em particular, permite ao compilador, para qualquer X, deduzir que o programa é equivalente a X. Certamente, permitindo que o compilador deduza isso. permitindo que ele faça isso. Também concordo com o autor que, se o UB se destina, deve ser declarado explicitamente, e se não se destina, o texto da especificação está errado e deve ser corrigido (talvez pelo equivalente no idioma da especificação de ", o compilador pode substituir o loop com código que não tem efeito ", não tenho certeza).
Steve Jessop
@SteveJessop: O que você pensaria em dizer simplesmente que a execução de qualquer parte do código - incluindo loops infinitos - pode ser adiada até um momento em que algo que a parte do código afetasse afetaria um comportamento observável do programa, e que, para fins disso, regra, o tempo necessário para executar um pedaço de código - mesmo que infinito - não é um "efeito colateral observável". Se um compilador puder demonstrar que um loop não pode sair sem uma variável mantendo um determinado valor, a variável pode ser considerada como mantendo esse valor, mesmo que também seja possível mostrar que o loop não pôde sair com esse valor.
Supercat 13/03
@ supercat: como você declarou essa conclusão, não acho que isso melhore as coisas. Se o loop provavelmente nunca sair, para qualquer objeto Xe padrão de bits x, o compilador pode demonstrar que o loop não sai sem Xmanter o padrão de bits x. É vacuamente verdade. Assim, Xpoderia ser considerado para realizar qualquer padrão de bits, e isso é tão ruim quanto UB no sentido de que pelo mal Xe xele vai rapidamente causar algum. Então, acredito que você precisa ser mais preciso em suas normas. É difícil falar sobre o que acontece "no final de" um loop infinito e mostrá-lo equivalente a alguma operação finita.
21714 Steve Jobs (
8

O problema relevante é que o compilador pode reordenar códigos cujos efeitos colaterais não entrem em conflito. A ordem surpreendente de execução pode ocorrer mesmo que o compilador produza um código de máquina não-final para o loop infinito.

Eu acredito que esta é a abordagem correta. A especificação do idioma define maneiras de impor a ordem de execução. Se você deseja um loop infinito que não pode ser reordenado, escreva o seguinte:

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");
Daniel Newby
fonte
2
@ JohannesSchaub-litb: Se um loop - interminável ou não - não lê ou grava nenhuma variável volátil durante a execução, e não chama nenhuma função que possa fazê-lo, um compilador é livre para adiar qualquer parte do loop até o primeiro esforço para acessar algo calculado nele. Dado que unsigned int dummy; while(1){dummy++;} fprintf(stderror,"Hey\r\n"); fprintf(stderror,"Result was %u\r\n",dummy);o primeiro fprintfpode executar, mas o segundo não (o compilador pode mover a computação dummyentre os dois fprintf, mas não além do que imprime seu valor).
22714
1

Eu acho que a questão talvez possa ser melhor declarada, como "Se um código posterior não depender de um código anterior e o código anterior não tiver efeitos colaterais em nenhuma outra parte do sistema, a saída do compilador pode executar o trecho de código posterior antes, depois ou misturado à execução do anterior, mesmo que o primeiro contenha loops, sem levar em consideração quando ou se o código anterior seria realmente concluído.Por exemplo, o compilador pode reescrever:

void testfermat (int n)
{
  int a = 1, b = 1, c = 1;
  while (pow (a, n) + pow (b, n)! = pow (c, n))
  {
    se (b> a) a ++; caso contrário, se (c> b) {a = 1; b ++}; mais {a = 1; b = 1; c ++};
  }
  printf ("O resultado é");
  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))
    {
      se (b> a) a ++; caso contrário, se (c> b) {a = 1; b ++}; mais {a = 1; b = 1; c ++};
    }
    signal_other_thread_and_die ();
  }
  else // Segunda discussão
  {
    printf ("O resultado é");
    wait_for_other_thread ();
  }
  printf ("% d /% d /% d", a, b, c);
}

Geralmente não é irracional, embora eu possa me preocupar com isso:

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

se tornaria

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

Ao fazer com que uma CPU lide com os cálculos e outra com as atualizações da barra de progresso, a reescrita melhoraria a eficiência. Infelizmente, isso tornaria as atualizações da barra de progresso um pouco menos úteis do que deveriam.

supercat
fonte
Acho que o caso da barra de progresso não pôde ser separado, porque exibir uma barra de progresso é uma chamada de E / S da biblioteca. As otimizações não devem alterar o comportamento visível dessa maneira.
Philip Potter
@ Philip Potter: Se a rotina lenta tivesse efeitos colaterais, isso certamente seria verdade. No meu exemplo anterior, não teria sentido se não o fizesse, então eu mudei. Minha interpretação da especificação é que o sistema pode adiar a execução do código lento até que seus efeitos (exceto o tempo que leva para executar) se tornem visíveis, ou seja, a chamada show_result (). Se o código de barra de progresso utilizasse o total em execução, ou pelo menos pretendesse fazê-lo, isso o forçaria a sincronizar com o código lento.
supercat 03/09/10
1
Isso explica todas essas barras de progresso que vão rápido de 0 a 100 e depois pendurar para as idades;)
paulm
0

Não é decidível para o compilador para casos não triviais se é um loop infinito.

Em diferentes casos, pode acontecer que o seu otimizador atinja uma classe de complexidade melhor para o seu código (por exemplo, era O (n ^ 2) e você obtém O (n) ou O (1) após a otimização).

Portanto, incluir uma regra que não permita a remoção de um loop infinito no padrão C ++ tornaria muitas otimizações impossíveis. E a maioria das pessoas não quer isso. Eu acho que isso responde bastante à sua pergunta.


Outra coisa: nunca vi nenhum exemplo válido em que você precise de um loop infinito que não faça nada.

O único exemplo que ouvi foi sobre um truque feio que realmente deveria ser resolvido de outra forma: tratava-se de sistemas embarcados em que a única maneira de acionar uma redefinição era congelar o dispositivo para que o cão de guarda o reinicias automaticamente.

Se você conhece algum exemplo válido / bom em que precisa de um loop infinito que não faz nada, por favor me diga.

Albert
fonte
1
Exemplo de onde você pode querer um loop infinito: um sistema incorporado em que você não deseja dormir por motivos de desempenho e todo o código está interrompido por uma interrupção ou duas?
JCx
@JCx no Padrão C, as interrupções devem definir um sinalizador que o loop principal verifica, portanto, o loop principal teria um comportamento observável no caso de os sinalizadores serem configurados. A execução de código substancial em interrupções não é portátil.
MM
-1

Eu acho que vale ressaltar que os loops que seriam infinitos, exceto pelo fato de que eles interagem com outros threads por meio de variáveis ​​não voláteis e não sincronizadas, agora podem gerar comportamento incorreto com um novo compilador.

Em outras palavras, torne seus globais voláteis - assim como os argumentos passados ​​para esse loop via ponteiro / referência.

spraff
fonte
Se eles estão interagindo com outros threads, você não deve torná-los voláteis, torná-los atômicos ou protegê-los com um cadeado.
BCoates
1
Thjs é um péssimo conselho. volatileTorná- los não é necessário nem suficiente, e prejudica o desempenho drasticamente.
David Schwartz