Por que um programa C/C++ geralmente tem a otimização desativada no modo de depuração?

StackOverflow https://stackoverflow.com/questions/69250

  •  09-06-2019
  •  | 
  •  

Pergunta

Na maioria dos ambientes C ou C++, há um modo "debug" e um modo de compilação "release".
Observando a diferença entre os dois, você descobre que o modo de depuração adiciona os símbolos de depuração (geralmente a opção -g em muitos compiladores), mas também desativa a maioria das otimizações.
No modo “release”, você geralmente tem todos os tipos de otimizações ativadas.
Por que a diferença?

Foi útil?

Solução

Sem qualquer otimização ativada, o fluxo através do seu código é linear.Se você estiver na linha 5 e em um único passo, você avança para a linha 6.Com a otimização ativada, você pode reordenar instruções, desenrolar loops e todos os tipos de otimizações.
Por exemplo:


void foo() {
1:  int i;
2:  for(i = 0; i < 2; )
3:    i++;
4:  return;

Neste exemplo, sem otimização, você poderia percorrer o código em uma única etapa e clicar nas linhas 1, 2, 3, 2, 3, 2, 4

Com a otimização ativada, você pode obter um caminho de execução semelhante a:2, 3, 3, 4 ou até mesmo apenas 4!(Afinal, a função não faz nada...)

Resumindo, depurar código com a otimização habilitada pode ser uma verdadeira dor de cabeça!Especialmente se você tiver funções grandes.

Observe que ativar a otimização altera o código!Em determinados ambientes (sistemas críticos de segurança), isso é inaceitável e o código que está sendo depurado deve ser o código enviado.É preciso depurar com otimização ativada nesse caso.

Embora o código otimizado e não otimizado deva ser "funcionalmente" equivalente, sob certas circunstâncias, o comportamento mudará.
Aqui está um exemplo simplista:

    int* ptr = 0xdeadbeef;  // some address to memory-mapped I/O device
    *ptr = 0;   // setup hardware device
    while(*ptr == 1) {    // loop until hardware device is done
       // do something
    }

Com a otimização desativada, isso é simples e você sabe o que esperar.No entanto, se você ativar a otimização, algumas coisas poderão acontecer:

  • O compilador pode otimizar o bloco while (iniciamos com 0, nunca será 1)
  • Em vez de acessar a memória, o acesso do ponteiro pode ser movido para um registro->Sem atualização de E/S
  • o acesso à memória pode ser armazenado em cache (não necessariamente relacionado à otimização do compilador)

Em todos esses casos, o comportamento seria drasticamente diferente e provavelmente errado.

Outras dicas

Outra diferença crucial entre depuração e liberação é como as variáveis ​​locais são armazenadas.Variáveis ​​​​conceitualmente locais são alocadas para armazenamento em um quadro de pilha de funções.O arquivo de símbolo gerado pelo compilador informa ao depurador o deslocamento da variável no quadro de pilha, para que o depurador possa mostrá-lo a você.O depurador espia o local da memória para fazer isso.

No entanto, isso significa que toda vez que uma variável local é alterada, o código gerado para aquela linha de origem precisa gravar o valor de volta no local correto na pilha.Isso é muito ineficiente devido à sobrecarga de memória.

Em uma versão build, o compilador pode atribuir uma variável local a um registro para uma parte de uma função.Em alguns casos, ele pode não atribuir nenhum armazenamento de pilha (quanto mais registros uma máquina tiver, mais fácil será fazer isso).

No entanto, o depurador não sabe como os registros são mapeados para variáveis ​​locais para um ponto específico no código (não tenho conhecimento de nenhum formato de símbolo que inclua essas informações), portanto, ele não pode mostrá-lo com precisão, pois não não sei onde ir procurá-lo.

Outra otimização seria o inlining de funções.Em compilações otimizadas, o compilador pode substituir uma chamada para foo() pelo código real para foo em qualquer lugar em que for usado porque a função é pequena o suficiente.No entanto, quando você tenta definir um ponto de interrupção em foo() o depurador quer saber o endereço das instruções para foo(), e não há mais uma resposta simples para isso - pode haver milhares de cópias do foo( ) bytes de código espalhados pelo seu programa.Uma compilação de depuração garantirá que haja algum lugar para você colocar o ponto de interrupção.

A otimização do código é um processo automatizado que melhora o desempenho do tempo de execução do código, preservando a semântica.Este processo pode remover resultados intermediários desnecessários para concluir uma avaliação de expressão ou função, mas que podem ser de seu interesse durante a depuração.Da mesma forma, as otimizações podem alterar o fluxo de controle aparente para que as coisas possam acontecer em uma ordem ligeiramente diferente daquela que aparece no código-fonte.Isso é feito para pular cálculos desnecessários ou redundantes.Essa reformulação do código pode atrapalhar o mapeamento entre os números de linha do código-fonte e os endereços do código-objeto, tornando difícil para um depurador seguir o fluxo de controle conforme você o escreve.

A depuração no modo não otimizado permite que você veja tudo o que escreveu conforme foi escrito, sem que o otimizador remova ou reordene as coisas.

Quando estiver satisfeito com o funcionamento correto do seu programa, você poderá ativar as otimizações para obter melhor desempenho.Embora os otimizadores sejam bastante confiáveis ​​atualmente, ainda é uma boa ideia construir um conjunto de testes de boa qualidade para garantir que seu programa seja executado de forma idêntica (do ponto de vista funcional, sem considerar o desempenho) tanto no modo otimizado quanto no não otimizado.

A expectativa é que a versão de depuração seja - depurada!Definir pontos de interrupção, etapas únicas enquanto observa variáveis, rastreamentos de pilha e tudo o mais que você faz em um depurador (IDE ou outro) faz sentido se cada linha de código-fonte não vazio e sem comentários corresponder a alguma instrução de código de máquina.

A maioria das otimizações altera a ordem dos códigos de máquina.O desenrolar do loop é um bom exemplo.Subexpressões comuns podem ser retiradas de loops.Com a otimização ativada, mesmo no nível mais simples, você pode estar tentando definir um ponto de interrupção em uma linha que, no nível do código de máquina, não existe.Às vezes você não pode monitorar uma variável local porque ela está sendo mantida em um registro da CPU, ou talvez até mesmo otimizada para deixar de existir!

Se você estiver depurando no nível de instrução e não no nível de origem, será muito mais fácil mapear instruções não otimizadas de volta à origem.Além disso, os compiladores ocasionalmente apresentam erros em seus otimizadores.

Na divisão Windows da Microsoft, todos os binários de lançamento são criados com símbolos de depuração e otimizações completas.Os símbolos são armazenados em arquivos PDB separados e não afetam o desempenho do código.Eles não são enviados com o produto, mas a maioria deles está disponível no Servidor de símbolos da Microsoft.

Outro problema com as otimizações são as funções inline, também no sentido de que você sempre as percorrerá em uma única etapa.

Com o GCC, com depuração e otimizações habilitadas juntas, se você não sabe o que esperar, pensará que o código está se comportando mal e reexecutando a mesma instrução várias vezes - isso aconteceu com alguns de meus colegas.Além disso, as informações de depuração fornecidas pelo GCC com otimizações tendem a ser de qualidade inferior do que poderiam, na verdade.

No entanto, em linguagens hospedadas por uma máquina virtual como Java, otimizações e depuração podem coexistir - mesmo durante a depuração, a compilação JIT para código nativo continua e apenas o código dos métodos depurados é convertido de forma transparente para uma versão não otimizada.

Gostaria de enfatizar que a otimização não deve alterar o comportamento do código, a menos que o otimizador usado tenha bugs ou o próprio código tenha bugs e dependa de semântica parcialmente indefinida;o último é mais comum na programação multithread ou quando o assembly inline também é usado.

O código com símbolos de depuração é maior, o que pode significar mais perdas de cache, ou seja,mais lento, o que pode ser um problema para o software do servidor.

Pelo menos no Linux (e não há razão para que o Windows seja diferente) as informações de depuração são empacotadas em uma seção separada do binário e não são carregadas durante a execução normal.Eles podem ser divididos em um arquivo diferente para ser usado para depuração.Além disso, em alguns compiladores (incluindo o Gcc, acho que também com o compilador C da Microsoft), as informações e otimizações de depuração podem ser habilitadas juntas.Caso contrário, obviamente o código será mais lento.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top