Como sei se o compilador quebrou meu código e o que devo fazer se fosse o compilador?

14

De vez em quando, o código C ++ não funciona quando compilado com algum nível de otimização. Pode ser um compilador que está fazendo otimização que quebra o código ou pode conter código contendo comportamento indefinido que permite ao compilador fazer o que sente.

Suponha que eu tenha algum código que seja quebrado quando compilado apenas com um nível de otimização mais alto. Como sei se é o código ou o compilador e o que faço se for o compilador?

dente afiado
fonte
43
Provavelmente é você.
Littleadv
9
@littleadv, mesmo as versões recentes do gcc e msvc estão cheias de bugs, então eu não teria tanta certeza.
SK-logic
3
Você tem todos os avisos ativados?
@ Thorbjørn Ravn Andersen: Sim, eu os tenho habilitados.
Sharptooth 26/09/11
3
FWIW: 1) Eu tento não fazer nada complicado que possa tentar atrapalhar o compilador, 2) o único lugar em que os sinalizadores de otimização são importantes (por velocidade) é no código em que o contador do programa gasta uma fração significativa de seu tempo. A menos que você esteja escrevendo loops de CPU apertados, em muitos aplicativos o PC passa essencialmente o tempo todo nas bibliotecas ou nas E / S. Nesse tipo de aplicativo, as opções / O não ajudam em nada.
precisa saber é o seguinte

Respostas:

19

Eu diria que é uma aposta segura que, na grande maioria dos casos, é o seu código, não o compilador, que está quebrado. E mesmo no caso extraordinário em que é o compilador, você provavelmente está usando algum recurso de linguagem obscura de uma maneira incomum, para a qual o compilador específico não está preparado; em outras palavras, você provavelmente poderia alterar seu código para ser mais idiomático e evitar o ponto fraco do compilador.

De qualquer forma, se você puder provar que encontrou um bug do compilador (com base na especificação da linguagem), relate-o aos desenvolvedores do compilador, para que eles possam corrigi-lo por algum tempo.

Péter Török
fonte
@ SK-logic, bastante justo, não tenho estatísticas para apoiá-lo. É baseado em minha própria experiência, e admito que raramente estiquei os limites do idioma e / ou do compilador - outros podem fazer isso com mais frequência.
Péter Török
(1) @ SK-Logic: Acabei de encontrar um bug do compilador C ++, mesmo código, experimentado em um compilador e funciona, experimentado em outro que ele corrige.
Sep
8
@umlcat: provavelmente era o seu código, dependendo do comportamento não especificado; em um compilador, ele corresponde às suas expectativas; em outro, não. isso não significa que está quebrado.
Javier
@ Mitch Melton, você já usou LTO?
SK-logic
1
Eu concordo com o Crashworks, ao falar sobre consoles de jogos. Não é incomum encontrar erros esotéricos de compilador nessa situação específica. Se você está mirando em PCs normais, usando um compilador muito usado, é muito improvável que você encontre um bug do compilador que ninguém viu antes.
Trevor Powell
14

Como sempre, como em qualquer outro bug: realize um experimento controlado. Limite a área suspeita, desative as otimizações para todo o resto e comece a variar as otimizações aplicadas a esse pedaço de código. Depois de obter 100% de reprodutibilidade, comece a variar seu código, introduzindo coisas que podem quebrar certas otimizações (por exemplo, introduza um possível alias de ponteiro, insira chamadas externas com possíveis efeitos colaterais etc.). Observar o código do assembly em um depurador também pode ajudar.

SK-logic
fonte
pode ajudar com o que? Se for um bug do compilador - e daí?
Littleadv
2
@littleadv, se for um bug do compilador, você pode tentar corrigi-lo (ou apenas relatá-lo adequadamente, com todos os detalhes) ou pode descobrir como evitá-lo no futuro, se estiver fadado a continuar usando isso versão do seu compilador por um tempo. Se for algo com seu próprio código, um dos inúmeros problemas de fronteira com C ++, esse tipo de análise também ajuda a corrigir um bug e evitar esse tipo no futuro.
SK-logic,
Então, como eu disse na minha resposta - além de relatar, não há muita diferença no tratamento, independentemente de quem é a culpa.
Littleadv
3
@littleadv, sem entender a natureza de um problema, é provável que você o enfrente de novo e de novo. E geralmente existe a possibilidade de corrigir um compilador por conta própria. E, sim, não é "improvável" encontrar um bug em um compilador C ++, infelizmente.
SK-logic
10

Examine o código do assembly que resultou e veja se ele faz o que sua fonte está pedindo. Lembre-se de que as chances são muito altas de que seja realmente o seu código culpado de alguma maneira não óbvia.

Loren Pechtel
fonte
1
Esta é realmente a única resposta para esta pergunta. O trabalho dos compiladores, nesse caso, é levá-lo do C ++ à linguagem assembly. Você acha que é o compilador ... verifique o trabalho dos compiladores. É simples assim.
old_timer
7

Em mais de 30 anos de programação, o número de erros genuínos do compilador (geração de código) que encontrei ainda é apenas ~ 10. O número dos meus próprios erros (e de outras pessoas) que encontrei e corrigi no mesmo período provavelmente é > 10.000. Minha "regra de ouro" é que a probabilidade de qualquer bug devido ao compilador é <0,001.

Paul R
fonte
1
Você é sortudo. Minha média é de cerca de 1 bug muito ruim por mês, e pequenos problemas de fronteira são muito mais frequentes. E quanto maior o nível de otimização que você estiver usando, maiores serão as chances de erro do compilador. Se você estiver tentando usar -O3 e LTO, teria muita sorte de não encontrar alguns deles em nenhum momento. E eu só conto aqui os erros nas versões de lançamento - como desenvolvedor de compiladores, estou enfrentando muito mais problemas desse tipo no meu trabalho, mas isso não conta. Eu só sei como é fácil estragar um compilador.
SK-logic,
2
25 anos e também vi muitos. Os compiladores estão piorando a cada ano.
old_timer
5

Comecei a escrever um comentário e depois decidi que era muito longo e direto ao ponto.

Eu diria que é o seu código que está quebrado. No caso improvável de você descobrir um bug no compilador - você deve denunciá-lo aos desenvolvedores do compilador, mas é aí que a diferença termina.

A solução é identificar a construção incorreta e refatorá-la para que ela faça a mesma lógica de maneira diferente. Isso provavelmente resolveria o problema, se o bug está do seu lado ou no compilador.

littleadv
fonte
5
  1. Releia seu código completamente. Verifique se você não está fazendo coisas com efeitos colaterais em ASSERTs ou em outras instruções específicas de depuração (ou configuração mais geral). Lembre-se também de que, em uma depuração, a memória é inicializada de maneira diferente - valores indicativos do ponteiro que você pode verificar aqui: Depuração - Representações de alocação de memória . Ao executar a partir do Visual Studio, você quase sempre usa o Debug Heap (mesmo no modo de lançamento), a menos que especifique explicitamente com uma variável de ambiente que não é isso que você deseja.
  2. Verifique sua compilação. É comum obter problemas com compilações complexas em outros lugares que não o compilador real - as dependências geralmente são as culpadas. Eu sei que "você tentou reconstruir completamente" é quase tão irritante quanto uma resposta "você tentou reinstalar o Windows", mas geralmente ajuda. Tente: a) Reinicializando. b) Excluindo todos os seus arquivos intermediários e de saída manualmente e reconstruindo.
  3. Examine seu código para verificar possíveis locais em que você possa estar chamando um comportamento indefinido. Se você trabalha em C ++ há algum tempo, saberá que há alguns pontos em que você pensa "Não tenho certeza absoluta de que posso assumir que ..." - pesquise no Google ou pergunte aqui sobre esse particular tipo de código para ver se é um comportamento indefinido ou não.
  4. Se isso ainda não parece ser o caso, gere uma saída pré-processada para o arquivo que está causando os problemas. Uma expansão inesperada de macro pode causar todo tipo de diversão (lembro-me de quando um colega decidiu que uma macro com o nome H seria uma boa idéia ...). Examine a saída pré-processada para alterações inesperadas entre as configurações do seu projeto.
  5. Último recurso - agora você realmente está na área de erros do compilador - observe a saída da montagem. Isso pode levar algumas escavações e brigas apenas para entender o que a montagem está realmente fazendo, mas na verdade é bastante informativa. Você pode usar as habilidades adquiridas aqui para avaliar também as micro otimizações, para que nem tudo se perca.
Joris Timmermans
fonte
+1 para "comportamento indefinido". Eu fui mordido por aquele. Escreveu algum código que dependia do int + intestouro exatamente como se fosse compilado em uma instrução ADD de hardware. Funcionou muito bem quando compilado com uma versão mais antiga do GCC, mas não quando compilado com o compilador mais recente. Aparentemente, as pessoas legais do GCC decidiram que, como o resultado de um estouro de número inteiro é indefinido, seu otimizador poderia operar com a suposição de que isso nunca acontece. Otimizou uma ramificação importante diretamente do código.
Solomon Slow
2

Se você deseja saber se é o seu código ou o compilador, é necessário conhecer perfeitamente a especificação do C ++.

Se a dúvida persistir, é necessário conhecer perfeitamente a montagem x86.

Se você não está disposto a aprender tanto com perfeição, é quase certamente um comportamento indefinido que o seu compilador resolve de maneira diferente, dependendo do nível de otimização.

mouviciel
fonte
(+1) @mouviciel: Também depende se o recurso é suportado pelo compilador, mesmo que esteja na especificação. Eu tenho um bug estranho com o gcc. Declaro que uma "estrutura c simples" com um "ponteiro de função" é permitida na especificação, mas funciona em algumas situações e não em outra.
umlcat
1

Obter um erro de compilação no código padrão ou um erro interno de compilação é mais provável do que os otimizadores estarem errados. Mas eu ouvi falar de compiladores que otimizam loops incorretamente esquecendo alguns efeitos colaterais que um método causa.

Não tenho sugestões sobre como saber se é você ou o compilador. Você pode tentar outro compilador.

Um dia eu queria saber se era o meu código ou não, e alguém me sugeriu um valgrind. Passei os 5 ou 10 minutos para executar meu programa com ele (acho que valgrind --leak-check=yes myprog arg1 arg2fiz, mas joguei com outras opções) e imediatamente me mostrou UMA linha que é executada em um caso específico que era o problema. Então, meu aplicativo funcionou sem problemas desde então, sem falhas estranhas, erros ou comportamento estranho. valgrind ou outra ferramenta como essa é uma boa maneira de saber se é o seu código.

Nota lateral: Uma vez me perguntei por que o desempenho do meu aplicativo foi ruim. Acontece que todos os meus problemas de desempenho estavam em uma linha também. Eu escrevi for(int i=0; i<strlen(sz); ++i) {. O sz foi de alguns mb. Por alguma razão, o compilador executava strlen todas as vezes, mesmo após a otimização. Uma linha pode ser um grande negócio. De performances a falhas


fonte
1

Uma situação cada vez mais comum é que os compiladores quebram o código escrito para dialetos de C que suportam comportamentos não exigidos pelo Padrão e permitem que o código direcionado a esses dialetos seja mais eficiente do que o código estritamente conforme. Nesse caso, seria injusto descrever como código "quebrado", que seria 100% confiável em compiladores que implementaram o dialeto de destino, ou descrever como "quebrado" o compilador que processa um dialeto que não suporta a semântica necessária . Em vez disso, os problemas decorrem simplesmente do fato de que o idioma processado pelos compiladores modernos com otimizações ativadas é diferente dos dialetos que costumavam ser populares (e ainda são processados ​​por muitos compiladores com as otimizações desativadas ou por alguns até com as otimizações ativadas).

Por exemplo, muito código é escrito para dialetos que reconhecem como legítimos vários padrões de alias de ponteiro não exigidos pela interpretação do Padrão do gcc, e faz uso desses padrões para permitir que uma tradução direta do código seja mais legível e eficiente do que seria possível sob a interpretação da Norma C por gcc. Esse código pode não ser compatível com o gcc, mas isso não implica que ele esteja quebrado. Simplesmente depende de extensões que o gcc suporta apenas com otimizações desativadas.

supercat
fonte
Bem, com certeza não há nada errado em codificar as extensões X + padrão Y e Z, desde que isso ofereça vantagens significativas, você sabe que fez isso e o documentou completamente. Infelizmente, as três condições normalmente não são atendidas e, portanto, é justo dizer que o código está quebrado.
Deduplicator
@Duplicator: os compiladores C89 foram promovidos como compatíveis com os seus antecessores, e também com o C99, etc. Embora o C89 não imponha requisitos a comportamentos previamente definidos em algumas plataformas, mas em outras, a compatibilidade ascendente sugere que os compiladores C89 para plataformas que consideravam comportamentos definidos deveriam continuar a fazê-lo; a justificativa para a promoção de tipos curtos não assinados em sinais sugeriria que os autores do Padrão esperassem que os compiladores se comportassem dessa maneira, independentemente de o Padrão o ter exigido. Mais ...
supercat
... interpretação estrita das regras de aliasing jogaria compatibilidade ascendente pela janela e tornaria muitos tipos de código impraticáveis, mas alguns pequenos ajustes (por exemplo, identificar alguns padrões nos quais o aliasing de tipo cruzado deveria ser esperado e, portanto, permitido) resolveria ambos os problemas . Todo o objetivo declarado da regra era evitar exigir que os compiladores fizessem suposições de pseudônimo "pessimista", mas, dado "float x", deve-se a presunção de que "foo ((int *) e x)" possa modificar x mesmo que "foo" não 't de escrita para qualquer ponteiros do tipo' float *" ou 'char *' ser considerado 'pessimista' ou 'óbvio'?
supercat
0

Isole o ponto problemático e compare o comportamento observado com o que deve acontecer de acordo com as especificações da linguagem. Definitivamente não é fácil, mas é isso que você precisa fazer para saber (e não apenas assumir ).

Eu provavelmente não seria tão meticuloso. Em vez disso, gostaria de perguntar ao fórum de suporte / lista de discussão do fabricante do compilador. Se é realmente um bug no compilador, eles podem corrigi-lo. Provavelmente seria o meu código de qualquer maneira. Por exemplo, as especificações de idioma relacionadas à visibilidade da memória no encadeamento podem ser bastante contra-intuitivas e podem se tornar aparentes apenas ao usar alguns sinalizadores de otimização específicos, em algum hardware específico (!). Algum comportamento pode ser indefinido pela especificação, portanto, ele pode funcionar com alguns compiladores / alguns sinalizadores e não com outros, etc.

Joonas Pulakka
fonte
0

Provavelmente, seu código tem um comportamento indefinido (como outros explicaram, é muito mais provável que você tenha bugs no código do que no compilador, mesmo se os compiladores C ++ forem tão complexos que eles possuem bugs; até a especificação C ++ possui bugs de design) . E o UB pode estar aqui, mesmo que o executável compilado funcione (por azar).

Portanto, você deve ler o blog de Lattner ' O que todo programador C deve saber sobre comportamento indefinido (a maioria se aplica também ao C ++ 11).

A ferramenta valgrind e as -fsanitize= opções recentes de instrumentação para o GCC (ou Clang / LLVM ) também devem ser úteis. E, é claro, habilite todos os avisos:g++ -Wall -Wextra

Basile Starynkevitch
fonte