O nível de otimização -O3 é perigoso em g ++?

233

Ouvi de várias fontes (embora principalmente de um colega meu) que a compilação com um nível de otimização -O3em g ++ é de alguma forma 'perigosa' e deve ser evitada em geral, a menos que seja necessário.

Isso é verdade? Se sim, por quê? Devo apenas estar aderindo -O2?

Dunnie
fonte
38
Só é perigoso se você confiar em um comportamento indefinido. E mesmo assim eu ficaria surpreso se fosse o nível de otimização que atrapalhasse alguma coisa.
Seth Carnegie
5
O compilador ainda está restrito a produzir um programa que se comporte "como se" compilasse seu código exatamente. Eu não sei o que -O3é considerado particularmente buggy? Eu acho que talvez possa piorar o comportamento indefinido, pois pode fazer coisas estranhas e maravilhosas com base em certas suposições, mas isso seria culpa sua. Então, geralmente, eu diria que está tudo bem.
BoBTFish
5
É verdade que níveis mais altos de otimizações são mais propensos a erros do compilador. Eu já atingi alguns casos, mas em geral eles ainda são bem raros.
Mysticial
21
-O2é ativado -fstrict-aliasinge, se o seu código sobreviver a isso, provavelmente sobreviverá a outras otimizações, pois é uma que as pessoas erram repetidamente. Dito isto, -fpredictive-commoningé apenas dentro -O3e habilitar isso pode habilitar erros no seu código causados ​​por suposições incorretas sobre simultaneidade. A menos errado seu código seja, a otimização menos perigoso é ;-)
Steve Jessop
6
@PlasmaHH, eu não acho que "mais rigorosa" é uma boa descrição de -Ofast, ele desliga manuseio IEEE-compliant de Nans por exemplo
Jonathan Wakely

Respostas:

223

Nos primeiros dias do gcc (2.8 etc.) e nos tempos do egcs, e redhat 2.96 -O3 às vezes era bastante complicado. Mas isso foi há mais de uma década, e -O3 não é muito diferente de outros níveis de otimizações (em buggy).

No entanto, tende a revelar casos em que as pessoas confiam em comportamento indefinido, devido a confiar mais estritamente nas regras, e especialmente nos casos de canto, do (s) idioma (s).

Como observação pessoal, estou executando o software de produção no setor financeiro há muitos anos com o -O3 e ainda não encontrei um bug que não existiria se eu tivesse usado o -O2.

Pela demanda popular, aqui está uma adição:

-O3 e sinalizadores especialmente adicionais como -funroll-loops (não ativados por -O3) às vezes podem levar à geração de mais código de máquina. Sob certas circunstâncias (por exemplo, em uma CPU com cache de instrução L1 excepcionalmente pequeno), isso pode causar uma desaceleração devido a todo o código de, por exemplo, algum loop interno que agora não se encaixa mais no L1I. Geralmente, o gcc tenta bastante não gerar tanto código, mas como geralmente otimiza o caso genérico, isso pode acontecer. Opções especialmente propensas a isso (como desenrolar o loop) normalmente não são incluídas em -O3 e são marcadas de acordo na página de manual. Como tal, geralmente é uma boa ideia usar -O3 para gerar código rápido, e somente voltar a -O2 ou -Os (que tenta otimizar o tamanho do código) quando apropriado (por exemplo, quando um criador de perfil indica que L1I está ausente).

Se você quiser levar a otimização ao extremo, poderá ajustar o gcc via --param os custos associados a determinadas otimizações. Além disso, observe que o gcc agora tem a capacidade de colocar atributos em funções que controlam as configurações de otimização apenas para essas funções. Portanto, quando você encontrar um problema com -O3 em uma função (ou quiser experimentar sinalizadores especiais para essa função), você não precisa compilar o arquivo inteiro ou mesmo o projeto inteiro com o O2.

parece que é preciso ter cuidado ao usar -Ofast, que afirma:

-Ofast permite todas as otimizações de -O3. Ele também permite otimizações que não são válidas para todos os programas compatíveis com o padrão.

o que me leva a concluir que o -O3 deve ser totalmente compatível com os padrões.

PlasmaHH
fonte
2
Eu apenas uso algo como o oposto. Eu sempre uso -Os ou -O2 (às vezes o O2 gera um executável menor) .. após a criação de perfil, uso o O3 em partes do código que levam mais tempo de execução e que sozinhas podem fornecer até 20% mais velocidade.
CoffeDeveloper
3
Eu faço isso por velocidade. O3 na maioria das vezes torna as coisas mais lentas. Não sei exatamente por que, suspeito que polua o cache de instruções.
CoffeDeveloper
4
@DarioOO Eu sinto que implorar "inchaço do código" é uma coisa popular a se fazer, mas quase nunca o vejo apoiado em benchmarks. Depende muito da arquitetura, mas toda vez que vejo benchmarks publicados (por exemplo, phoronix.com/… ), o O3 é mais rápido na grande maioria dos casos. Eu vi a análise de perfil e cuidadosa necessária para provar que o inchaço do código era realmente um problema, e geralmente só acontece para pessoas que adotam modelos de maneira extrema.
Nir Friedman
1
@NirFriedman: Tende a ter um problema quando o modelo de custo embutido do compilador possui bugs ou quando você otimiza para um destino totalmente diferente do que você executa. Intrestingly isso se aplica a todos os níveis de otimização ...
PlasmaHH
1
@PlasmaHH: seria difícil corrigir o problema usando cmov para o caso geral. Normalmente, você não apenas classificou seus dados; portanto, quando o gcc está tentando decidir se uma ramificação é previsível ou não, a análise estática à procura de chamadas para std::sortfunções provavelmente não ajudará. Usar algo como stackoverflow.com/questions/109710/… ajudaria, ou talvez escreva a fonte para tirar proveito da classificação: varra até ver> = 128 e comece a somar. Quanto ao código inchado, sim, pretendo denunciá-lo. : P
Peter Cordes
42

Na minha experiência um tanto quadriculada, aplicar -O3a um programa inteiro quase sempre o torna mais lento (em relação a -O2), porque ativa o desenrolar e o alinhamento agressivos de loop que tornam o programa não mais adequado ao cache de instruções. Para programas maiores, isso também pode ser verdade em -O2relação a -Os!

O padrão de uso pretendido -O3é que, após criar um perfil do seu programa, você o aplica manualmente a um pequeno punhado de arquivos contendo loops internos críticos que realmente se beneficiam dessas trocas agressivas de espaço por velocidade. As versões mais recentes do GCC têm um modo de otimização guiado por perfil que pode (IIUC) aplicar seletivamente as -O3otimizações a funções importantes - automatizando efetivamente esse processo.

zwol
fonte
10
"quase sempre"? Faça "50-50", e teremos um acordo ;-).
No-Bugs Hare
12

A opção -O3 ativa otimizações mais caras, como embutimento de funções, além de todas as otimizações dos níveis mais baixos '-O2' e '-O1'. O nível de otimização '-O3' pode aumentar a velocidade do executável resultante, mas também pode aumentar seu tamanho. Sob algumas circunstâncias em que essas otimizações não são favoráveis, essa opção pode tornar o programa mais lento.

neel
fonte
3
Entendo que algumas "otimizações aparentes" podem tornar o programa mais lento, mas você tem uma fonte que alega que o GCC -O3 tornou o programa mais lento?
Pato Mooing
1
@MooingDuck: Embora eu não possa citar uma fonte, lembro-me de ter encontrado um caso assim com alguns processadores AMD mais antigos que tinham um cache L1I bastante pequeno (~ 10k instruções). Tenho certeza que o google tem mais para os interessados, mas especialmente opções como desenrolamento de loop não fazem parte do O3, e isso aumenta muito o tamanho. -Os é aquele para quando você deseja tornar o executável menor. Mesmo -O2 pode aumentar o tamanho do código. Uma boa ferramenta para brincar com o resultado de diferentes níveis de otimização é o gcc explorer.
PlasmaHH 18/07/12
@PlasmaHH: Na verdade, um tamanho pequeno de cache é algo que um compilador pode estragar, bom ponto. Esse é realmente um bom exemplo. Por favor, coloque-o na resposta.
Mooing Duck
1
O @PlasmaHH Pentium III tinha um cache de código de 16 KB. Na verdade, o K6 da AMD e acima tinham 32KB de cache de instruções. Os P4 começaram com cerca de 96 KB. Na verdade, o Core I7 possui um cache de código de 32 KB L1. Os decodificadores de instruções são fortes hoje em dia, então o seu L3 é bom o suficiente para recorrer a praticamente qualquer loop.
precisa saber é o seguinte
1
Você verá um enorme desempenho aumentar sempre que houver uma função chamada em um loop e pode eliminar significativamente a subexpressão comum e elevar o recálculo desnecessário da função antes do loop.
precisa saber é o seguinte
8

Sim, o O3 é mais problemático. Sou desenvolvedor de compiladores e identifiquei erros claros e óbvios do gcc causados ​​pelo O3, gerando instruções de montagem do SIMD com erros ao criar meu próprio software. Pelo que vi, a maioria dos softwares de produção vem com O2, o que significa que o O3 receberá menos atenção em testes e correções de erros.

Pense da seguinte maneira: o O3 adiciona mais transformações sobre o O2, o que adiciona mais transformações sobre o O1. Estatisticamente falando, mais transformações significam mais erros. Isso é verdade para qualquer compilador.

David Yeager
fonte
3

Recentemente, tive um problema ao usar a otimização com g++. O problema estava relacionado a uma placa PCI, onde os registradores (para comando e dados) eram representados por um endereço de memória. Meu driver mapeou o endereço físico para um ponteiro dentro do aplicativo e o entregou ao processo chamado, que funcionou assim:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

O cartão não funcionou como esperado. Quando eu vi a montagem entendi que o compilador só escreveu someCommand[ the last ]em pciMemory, omitindo todas as gravações anteriores.

Em conclusão: seja preciso e atento à otimização.

borisbn
fonte
38
Mas o ponto aqui é que seu programa simplesmente tem um comportamento indefinido; o otimizador não fez nada de errado. Em particular, você precisa declarar pciMemorycomo volatile.
Konrad Rudolph
11
Na verdade, não é UB, mas o compilador tem o direito de omitir todas as gravações, exceto a última, pciMemoryporque todas as outras gravações provavelmente não têm efeito. Para o otimizador, isso é incrível porque pode remover muitas instruções inúteis e demoradas.
Konrad Rudolph
4
Encontrei isso no padrão (após 10 anos ou mais))) - Uma declaração volátil pode ser usada para descrever um objeto correspondente a uma porta de entrada / saída mapeada na memória ou um objeto acessado por uma função de interrupção assíncrona. As ações nos objetos declarados não devem ser '' otimizadas '' por uma implementação ou reordenadas, exceto conforme permitido pelas regras para avaliação de expressões.
borisbn
2
@borisbn Um pouco fora de tópico, mas como você sabe que seu dispositivo tomou o comando antes de enviar um novo comando?
user877329
3
@ user877329 eu vi pela behaviuor do dispositivo, mas foi uma grande busca
borisbn