Quais, se houver, compiladores C ++ fazem otimização de recursão de cauda?

149

Parece-me que funcionaria perfeitamente bem na otimização da recursão de cauda em C e C ++, mas durante a depuração, nunca pareço ver uma pilha de quadros que indica essa otimização. Isso é bom, porque a pilha me diz o quão profunda é a recursão. No entanto, a otimização também seria agradável.

Algum compilador C ++ faz essa otimização? Por quê? Por que não?

Como instruo o compilador a fazer isso?

  • Para MSVC: /O2ou/Ox
  • Para o GCC: -O2ou-O3

Que tal verificar se o compilador fez isso em um determinado caso?

  • Para MSVC, habilite a saída PDB para rastrear o código e, em seguida, inspecione o código
  • Para o GCC ..?

Eu ainda aceitaria sugestões de como determinar se uma determinada função é otimizada assim pelo compilador (embora eu ache reconfortante que o Konrad me diga para assumi-la)

Sempre é possível verificar se o compilador faz isso fazendo uma recursão infinita e verificando se isso resulta em um loop infinito ou em um estouro de pilha (fiz isso com o GCC e descobri que -O2é suficiente), mas quero ser capaz de verificar uma determinada função que eu sei que terminará de qualquer maneira. Eu adoraria ter uma maneira fácil de verificar isso :)


Após alguns testes, descobri que os destruidores arruinam a possibilidade de fazer essa otimização. Às vezes, pode valer a pena alterar o escopo de determinadas variáveis ​​e temporários para garantir que elas fiquem fora do escopo antes do início da instrução de retorno.

Se qualquer destruidor precisar ser executado após a chamada final, a otimização da chamada final não poderá ser realizada.

Magnus Hoff
fonte

Respostas:

128

Todos os compiladores mainstream atuais realizam a otimização de chamada de cauda razoavelmente bem (e o fazem há mais de uma década), mesmo para chamadas recursivas mutuamente , como:

int bar(int, int);

int foo(int n, int acc) {
    return (n == 0) ? acc : bar(n - 1, acc + 2);
}

int bar(int n, int acc) {
    return (n == 0) ? acc : foo(n - 1, acc + 1);
}

Deixar o compilador fazer a otimização é simples: basta ativar a otimização para obter velocidade:

  • Para MSVC, use /O2ou /Ox.
  • Para GCC, Clang e ICC, use -O3

Uma maneira fácil de verificar se o compilador fez a otimização é executar uma chamada que de outra forma resultaria em um estouro de pilha - ou observando a saída do assembly.

Como uma nota histórica interessante, a otimização da chamada de cauda para C foi adicionada ao GCC no curso de uma tese de diploma por Mark Probst. A tese descreve algumas advertências interessantes na implementação. Vale a pena ler.

Konrad Rudolph
fonte
A ICC faria isso, acredito. Que eu saiba, a ICC produz o código mais rápido do mercado.
Paul Nathan
35
@Paul A questão é quanto da velocidade do código ICC é causada por otimizações algorítmicas, como otimizações de chamada de cauda, ​​e quanto é causada pelas otimizações de cache e microinstrução que somente a Intel, com seu conhecimento íntimo de seus próprios processadores, pode fazer.
Imagist 28/09/09
6
gccpossui uma opção mais estreita -foptimize-sibling-callspara "otimizar chamadas de irmãos e chamadas recursivas". Esta opção (de acordo com gcc(1)páginas de manual para versões 4.4, 4.7 e 4.8 alvejando várias plataformas) é habilitado em níveis -O2, -O3, -Os.
Foof
Além disso, a execução no modo DEBUG sem solicitar otimizações explicitamente NÃO fará QUALQUER otimização. Você pode ativar o PDB para EXE de modo de versão verdadeiro e tentar passar por isso, mas observe que a depuração no modo de versão tem suas complicações - variáveis ​​invisíveis / removidas, variáveis ​​mescladas, variáveis ​​ficando fora do escopo em escopo desconhecido / inesperado, variáveis ​​que nunca entram escopo e tornou-se constantes verdadeiras com endereços no nível da pilha e - bem - quadros de pilha mesclados ou ausentes. Geralmente, os quadros de pilha mesclados significam que o chamado é alinhado e os quadros ausentes / retrocedidos provavelmente atendem.
21815
21

O gcc 4.3.2 destaca completamente esta função ( atoi()implementação ruim / trivial ) main(). O nível de otimização é -O1. Percebo que, se eu brincar com ele (mesmo mudando de staticpara extern, a recursão da cauda desaparece muito rápido, então eu não dependeria disso para a correção do programa.

#include <stdio.h>
static int atoi(const char *str, int n)
{
    if (str == 0 || *str == 0)
        return n;
    return atoi(str+1, n*10 + *str-'0');
}
int main(int argc, char **argv)
{
    for (int i = 1; i != argc; ++i)
        printf("%s -> %d\n", argv[i], atoi(argv[i], 0));
    return 0;
}
Tom Barta
fonte
1
Você pode ativar a otimização do tempo do link e acho que até um externmétodo pode ser incorporado então.
Konrad Rudolph
5
Estranho. Acabei de testar o gcc 4.2.3 (x86, Slackware 12.1) e o gcc 4.6.2 (AMD64, Debian wheezy) e com-O1 não otimização inlining e sem recursão de cauda . Você tem que usar -O2para isso (bem, no 4.2.x, que é bastante antigo agora, ainda não será incorporado). BTW Também vale acrescentar que o gcc pode otimizar a recursão mesmo quando não é estritamente uma cauda (como fatorial sem acumulador).
Przemoc 15/01/12
16

Assim como o óbvio (os compiladores não fazem esse tipo de otimização, a menos que você solicite), há uma complexidade sobre a otimização de chamada de cauda em C ++: destruidores.

Dado algo como:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }

O compilador não pode (em geral) a chamada de cauda otimizar isso porque precisa chamar o destruidor cls após o retorno da chamada recursiva.

Às vezes, o compilador pode ver que o destruidor não tem efeitos colaterais visíveis externamente (portanto, isso pode ser feito mais cedo), mas geralmente não pode.

Uma forma particularmente comum disso é onde Funkyé realmente um std::vectorou similar.

Martin Bonner apoia Monica
fonte
Nao funciona para mim. Os sistemas me dizem que meu voto está bloqueado até que a resposta seja editada.
Hmuelner 15/08/16
Acabei de editar a resposta (parênteses removidas) e agora eu poderia desfazer meu voto negativo.
Hmuelner 15/08/16
11

A maioria dos compiladores não faz nenhum tipo de otimização em uma compilação de depuração.

Se estiver usando o VC, tente uma versão compilada com as informações do PDB ativadas - isso permitirá que você rastreie o aplicativo otimizado e, esperançosamente, deverá ver o que deseja. Observe, no entanto, que a depuração e o rastreamento de uma compilação otimizada o guiarão por todo o lado, e muitas vezes você não poderá inspecionar as variáveis ​​diretamente, pois elas acabam nos registros ou são totalmente otimizadas. É uma experiência "interessante" ...

Greg Whitfield
fonte
2
tente o gcc why -g -O3 e obtenha opimizações em uma compilação de depuração. xlC tem o mesmo comportamento.
G24l
Quando você diz "a maioria dos compiladores": quais coleções de compiladores você considera? Como apontado, há pelo menos dois compiladores que executam otimizações durante a compilação de depuração - e até onde eu sei, o VC também faz isso (exceto se você estiver ativando o modificar e continuar, talvez).
SkyKing
7

Como Greg menciona, os compiladores não farão isso no modo de depuração. Não há problema em compilações de depuração serem mais lentas do que uma compilação de prod, mas elas não devem travar com mais frequência: e se você depende de uma otimização de chamada de cauda, ​​elas podem fazer exatamente isso. Por esse motivo, geralmente é melhor reescrever a chamada final como um loop normal. :-(

0124816
fonte