Ao escrever uma ftol
função otimizada , encontrei um comportamento muito estranho GCC 4.6.1
. Deixe-me mostrar o código primeiro (para maior clareza, marquei as diferenças):
fast_trunc_one, C:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent; /* diff */
} else {
r = mantissa >> exponent; /* diff */
}
return (r ^ -sign) + sign; /* diff */
}
fast_trunc_two, C:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent) ^ -sign; /* diff */
} else {
r = (mantissa >> exponent) ^ -sign; /* diff */
}
return r + sign; /* diff */
}
Parece o mesmo, certo? Bem, o GCC discorda. Depois de compilar com gcc -O3 -S -Wall -o test.s test.c
isso, a saída do assembly:
fast_trunc_one, gerado:
_fast_trunc_one:
LFB0:
.cfi_startproc
movl 4(%esp), %eax
movl $150, %ecx
movl %eax, %edx
andl $8388607, %edx
sarl $23, %eax
orl $8388608, %edx
andl $255, %eax
subl %eax, %ecx
movl %edx, %eax
sarl %cl, %eax
testl %ecx, %ecx
js L5
rep
ret
.p2align 4,,7
L5:
negl %ecx
movl %edx, %eax
sall %cl, %eax
ret
.cfi_endproc
fast_trunc_two, gerado:
_fast_trunc_two:
LFB1:
.cfi_startproc
pushl %ebx
.cfi_def_cfa_offset 8
.cfi_offset 3, -8
movl 8(%esp), %eax
movl $150, %ecx
movl %eax, %ebx
movl %eax, %edx
sarl $23, %ebx
andl $8388607, %edx
andl $255, %ebx
orl $8388608, %edx
andl $-2147483648, %eax
subl %ebx, %ecx
js L9
sarl %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_remember_state
.cfi_def_cfa_offset 4
.cfi_restore 3
ret
.p2align 4,,7
L9:
.cfi_restore_state
negl %ecx
sall %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_restore 3
.cfi_def_cfa_offset 4
ret
.cfi_endproc
Essa é uma diferença extrema . Na verdade, isso também aparece no perfil, fast_trunc_one
é cerca de 30% mais rápido que fast_trunc_two
. Agora minha pergunta: o que está causando isso?
-S -O3 -da -fdump-tree-all
. Isso criará muitos instantâneos da representação intermediária. Caminhe por eles (eles estão numerados) lado a lado e você poderá encontrar a otimização ausente no primeiro caso.int
paraunsigned int
e veja se a diferença desaparece.(r + shifted) ^ sign
não é a mesma quer + (shifted ^ sign)
. Eu acho que isso está confundindo o otimizador? FWIW, MSVC 2010 (16.00.40219.01) produz listagens quase idênticas entre si: gist.github.com/2430454Respostas:
Atualizado para sincronizar com a edição do OP
Ao mexer no código, consegui ver como o GCC otimiza o primeiro caso.
Antes de entendermos por que eles são tão diferentes, primeiro precisamos entender como o GCC otimiza
fast_trunc_one()
.Acredite ou não,
fast_trunc_one()
está sendo otimizado para isso:Isso produz exatamente a mesma montagem que os
fast_trunc_one()
nomes dos registros originais e tudo mais.Observe que não há
xor
s na montagem parafast_trunc_one()
. Foi isso que me deu.Como assim?
Passo 1:
sign = -sign
Primeiro, vamos dar uma olhada na
sign
variável. Desdesign = i & 0x80000000;
, existem apenas dois valores possíveis quesign
podem ser utilizados:sign = 0
sign = 0x80000000
Agora reconheça isso em ambos os casos
sign == -sign
,. Portanto, quando eu altero o código original para isso:Produz exatamente a mesma montagem que o original
fast_trunc_one()
. Vou poupar a montagem, mas é idêntica - registre nomes e tudo.Etapa 2: Redução matemática:
x + (y ^ x) = y
sign
pode assumir apenas um dos dois valores,0
ou0x80000000
.x = 0
, em seguida,x + (y ^ x) = y
, trivial é válido.0x80000000
é o mesmo. Ele vira o bit do sinal. Portanto,x + (y ^ x) = y
também vale quandox = 0x80000000
.Portanto,
x + (y ^ x)
reduz paray
. E o código simplifica para isso:Novamente, isso compila exatamente o mesmo assembly - registre nomes e tudo.
Esta versão acima finalmente se reduz a isso:
que é exatamente o que o GCC gera na montagem.
Então, por que o compilador não otimiza
fast_trunc_two()
para a mesma coisa?A parte principal
fast_trunc_one()
é ax + (y ^ x) = y
otimização.fast_trunc_two()
nox + (y ^ x)
expressão está sendo dividido entre o ramo.Suspeito que isso seja suficiente para confundir o GCC para não fazer essa otimização. (Seria necessário içar
^ -sign
o ramo e fundi-lo com or + sign
no final.)Por exemplo, isso produz o mesmo conjunto que
fast_trunc_one()
:fonte
Essa é a natureza dos compiladores. Supondo que eles seguirão o caminho mais rápido ou melhor, é bastante falso. Qualquer pessoa que implique que você não precisa fazer nada no seu código para otimizar, porque "compiladores modernos" preenchem o espaço em branco, fazem o melhor trabalho, criam o código mais rápido etc. Na verdade, vi o gcc piorar de 3.x para 4.x no braço, pelo menos. O 4.x pode ter atingido o 3.x nesse ponto, mas desde o início produziu um código mais lento. Com a prática, você pode aprender a escrever seu código para que o compilador não precise trabalhar tanto e, como resultado, produz resultados mais consistentes e esperados.
O erro aqui são suas expectativas sobre o que será produzido, não o que realmente foi produzido. Se você deseja que o compilador gere a mesma saída, alimente a mesma entrada. Não é matematicamente o mesmo, não é o mesmo, mas na verdade é o mesmo, sem caminhos diferentes, sem operações de compartilhamento ou distribuição de uma versão para a outra. Este é um bom exercício para entender como escrever seu código e ver o que os compiladores fazem com ele. Não cometa o erro de supor que, porque uma versão do gcc para um destino de processador em um dia produziu um certo resultado, que é uma regra para todos os compiladores e todo o código. Você precisa usar muitos compiladores e muitos destinos para ter uma ideia do que está acontecendo.
O gcc é bastante desagradável, convido você a olhar por trás da cortina, olhar as entranhas do gcc, tentar adicionar um alvo ou modificar você mesmo. Mal é mantida unida por fita adesiva e fiação. Uma linha extra de código adicionada ou removida em locais críticos e desmoronando. O fato de ter produzido código utilizável é algo para se agradar, em vez de se preocupar com o motivo de não atender a outras expectativas.
você olhou para quais versões diferentes do gcc produzem? 3.xe 4.x em particular 4.5 vs 4.6 vs 4.7, etc? e para diferentes processadores de destino, x86, arm, mips, etc ou diferentes tipos de x86, se esse for o compilador nativo que você usa, 32 bits versus 64 bits, etc.? E então llvm (clang) para diferentes alvos?
O Mystical fez um excelente trabalho no processo de reflexão necessário para solucionar o problema de analisar / otimizar o código, esperando que um compilador desenvolva algo que não seja esperado de nenhum "compilador moderno".
Sem entrar nas propriedades matemáticas, o código deste formulário
vai levar o compilador para A: implementá-lo dessa forma, execute o if-then-else e, em seguida, convergir no código comum para concluir e retornar. ou B: salve um ramo, pois esse é o final da função. Também não se preocupe em usar ou salvar r.
Então você pode entrar como Mystical apontou que a variável de sinal desaparece todos juntos para o código conforme escrito. Eu não esperaria que o compilador visse a variável de sinal desaparecer, então você deveria ter feito isso sozinho e não forçado o compilador a tentar descobrir isso.
Esta é uma oportunidade perfeita para descobrir o código fonte do gcc. Parece que você encontrou um caso em que o otimizador viu uma coisa em um caso e outra em outro caso. Depois, dê o próximo passo e veja se você não consegue que o gcc veja esse caso. Toda otimização existe porque algum indivíduo ou grupo reconheceu a otimização e a colocou intencionalmente lá. Para que essa otimização esteja lá e funcione toda vez que alguém tiver que colocá-la lá (e depois testá-la e mantê-la no futuro).
Definitivamente, não assuma que menos código é mais rápido e mais lento, é muito fácil criar e encontrar exemplos de que isso não é verdade. Na maioria das vezes, pode ser o caso de menos código ser mais rápido que mais código. Como demonstrei desde o início, você pode criar mais código para salvar ramificações nesse caso ou em loop, etc., e fazer com que o resultado líquido seja um código mais rápido.
O resultado final é que você alimentou uma fonte diferente do compilador e esperou os mesmos resultados. O problema não é a saída do compilador, mas as expectativas do usuário. É bastante fácil demonstrar para um compilador e processador específico, a adição de uma linha de código que torna toda uma função muito mais lenta. Por exemplo, por que alterar a = b + 2; para a = b + c + 2; causa _fill_in_the_blank_compiler_name_ gerar código radicalmente diferente e mais lento? A resposta, obviamente, sendo o compilador, recebeu um código diferente na entrada, portanto é perfeitamente válido que o compilador gere uma saída diferente. (Melhor ainda é quando você troca duas linhas de código não relacionadas e faz com que a saída mude drasticamente). Não há relação esperada entre a complexidade e o tamanho da entrada com a complexidade e o tamanho da saída.
Produziu algo entre 60-100 linhas de montador. Desenrolou o loop. Não contei as linhas, se você pensar bem, tem que adicionar, copiar o resultado da entrada para a chamada de função, fazer a chamada de função, três operações no mínimo. portanto, dependendo do alvo que provavelmente tenha 60 instruções, pelo menos, 80 se quatro por loop, 100 se cinco por loop, etc.
fonte
O Mysticial já deu uma ótima explicação, mas pensei em acrescentar, FWIW, que não há realmente nada fundamental sobre por que um compilador faria a otimização para um e não para o outro.
O
clang
compilador do LLVM , por exemplo, fornece o mesmo código para ambas as funções (exceto o nome da função), fornecendo:Esse código não é tão curto quanto a primeira versão do gcc do OP, mas não tão longo quanto a segunda.
O código de outro compilador (que não vou citar), compilando para x86_64, produz isso para as duas funções:
o que é fascinante, pois calcula os dois lados do
if
e depois usa um movimento condicional no final para escolher o caminho certo.O compilador Open64 produz o seguinte:
e código semelhante, mas não idêntico
fast_trunc_two
.De qualquer forma, quando se trata de otimização, é uma loteria - é o que é ... nem sempre é fácil saber por que o código é compilado de alguma maneira específica.
fonte
icc
. Eu só tenho a variante de 32 bits, mas produz código muito semelhante a isso.