Por que o GCC não otimiza a * a * a * a * a * a para (a * a * a) * (a * a * a)?

2120

Estou fazendo alguma otimização numérica em um aplicativo científico. Uma coisa que notei é que o GCC otimiza a chamada pow(a,2), compilando-a a*a, mas a chamada pow(a,6)não é otimizada e na verdade chama a função de biblioteca pow, o que diminui bastante o desempenho. (Por outro lado, o compilador Intel C ++ , executável icc, eliminará a chamada da biblioteca pow(a,6).)

O que eu estou curioso é que quando eu substituído pow(a,6)com a*a*a*a*a*autilizando GCC 4.5.1 e opções " -O3 -lm -funroll-loops -msse4", ele usa 5 mulsdinstruções:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

enquanto se eu escrever (a*a*a)*(a*a*a), ele produzirá

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

que reduz o número de instruções de multiplicação para 3. icctem um comportamento semelhante.

Por que os compiladores não reconhecem esse truque de otimização?

xis
fonte
13
O que significa "reconhecer pow (a, 6)"?
Varun Madiath
659
Hum ... você sabe que a a a a a e (a a a) * (a a * a) não são os mesmos com números de ponto flutuante, não é? Você terá que usar -funsafe-math ou -ffast-math ou algo para isso.
Damon
106
Sugiro que você leia "O que todo cientista da computação deve saber sobre aritmética de ponto flutuante", de David Goldberg: download.oracle.com/docs/cd/E19957-01/806-3568/…, após o qual você terá uma compreensão mais completa o poço de alcatrão em que você acabou de entrar!
Phil Armstrong
189
Uma pergunta perfeitamente razoável. Há 20 anos, fiz a mesma pergunta geral e, esmagando esse gargalo único, reduzi o tempo de execução de uma simulação de Monte Carlo de 21 horas para 7 horas. O código no loop interno foi executado 13 trilhões de vezes no processo, mas conseguiu a simulação em uma janela durante a noite. (ver resposta abaixo)
23
Talvez jogue (a*a)*(a*a)*(a*a)na mistura também. Mesmo número de multiplicações, mas provavelmente mais preciso.
Rok Kralj

Respostas:

2738

Porque a matemática de ponto flutuante não é associativa . A maneira como você agrupa os operandos na multiplicação de ponto flutuante afeta a precisão numérica da resposta.

Como resultado, a maioria dos compiladores é muito conservadora ao reordenar os cálculos de ponto flutuante, a menos que eles possam ter certeza de que a resposta permanecerá a mesma ou que você diga a eles que não se importa com a precisão numérica. Por exemplo: a -fassociative-mathopção do gcc, que permite ao gcc reassociar operações de ponto flutuante, ou mesmo a -ffast-mathopção que permite compensações ainda mais agressivas de precisão em relação à velocidade.

Lambdageek
fonte
10
Sim. Com -ffast-math, ele está fazendo essa otimização. Boa ideia! Porém, como nosso código diz respeito a mais precisão do que velocidade, talvez seja melhor não aprová-lo.
xis
19
O IIRC C99 permite que o compilador faça otimizações de FP "inseguras", mas o GCC (em algo que não seja o x87) faz uma tentativa razoável de seguir a IEEE 754 - não são "limites de erro"; existe apenas uma resposta correta .
tc.
14
Os detalhes de implementação de pownão estão aqui nem ali; esta resposta nem faz referência pow.
Stephen Canon
14
@nedR: O padrão da ICC é permitir a associação novamente. Se você deseja obter um comportamento em conformidade com o padrão, é necessário definir -fp-model precisecom o ICC. clange gccpadrão para conformidade estrita, reassociação.
Stephen Canon
49
@ xis, não é exatamente isso que -fassociative-mathseria impreciso; é apenas isso a*a*a*a*a*ae (a*a*a)*(a*a*a)são diferentes. Não se trata de precisão; trata-se de conformidade com padrões e resultados estritamente repetíveis, por exemplo, os mesmos resultados em qualquer compilador. Os números de ponto flutuante já não são exatos. Raramente é inapropriado compilar com -fassociative-math.
Paul Draper
652

Lambdageek indica corretamente que, como a associatividade não é válida para números de ponto flutuante, a "otimização" dea*a*a*a*a*apara(a*a*a)*(a*a*a)pode alterar o valor. É por isso que não é permitido pelo C99 (a menos que seja especificamente permitido pelo usuário, via sinalizador do compilador ou pragma). Geralmente, a suposição é de que o programador escreveu o que fez por uma razão, e o compilador deve respeitar isso. Se você quiser(a*a*a)*(a*a*a), escreva isso.

Isso pode ser uma dor de escrever; por que o compilador não pode fazer [o que você considera] a coisa certa quando você usa pow(a,6)? Porque seria a coisa errada a fazer. Em uma plataforma com uma boa biblioteca de matemática, pow(a,6)é significativamente mais preciso que um a*a*a*a*a*aou outro (a*a*a)*(a*a*a). Apenas para fornecer alguns dados, fiz um pequeno experimento no meu Mac Pro, medindo o pior erro na avaliação de um ^ 6 para todos os números flutuantes de precisão única entre [1,2):

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

Usar em powvez de uma árvore de multiplicação reduz o erro vinculado por um fator de 4 . Os compiladores não devem (e geralmente não fazem) "otimizações" que aumentam o erro, a menos que sejam licenciadas pelo usuário (por exemplo, via -ffast-math).

Observe que o GCC fornece __builtin_powi(x,n)uma alternativa para pow( ), o que deve gerar uma árvore de multiplicação em linha. Use isso se desejar trocar a precisão pelo desempenho, mas não deseja ativar a matemática rápida.

Stephen Canon
fonte
29
Observe também que o Visual C ++ fornece uma versão 'aprimorada' de pow (). Ao ligar _set_SSE2_enable(<flag>)com flag=1, ele usará o SSE2, se possível. Isso reduz a precisão um pouco, mas melhora as velocidades (em alguns casos). MSDN: _set_SSE2_enable () e pow ()
TkTech
18
@TkTech: Qualquer precisão reduzida é devida à implementação da Microsoft, não ao tamanho dos registros usados. É possível fornecer um arredondamento correto pow usando apenas registros de 32 bits, se o escritor da biblioteca estiver muito motivado. Existem powimplementações baseadas em SSE que são mais precisas do que a maioria das implementações baseadas em x87 e também implementações que compensam alguma precisão por velocidade.
Stephen Canon
9
@TkTech: Claro, eu só queria deixar claro que a redução na precisão se deve às escolhas feitas pelos escritores da biblioteca, não intrínsecas ao uso do SSE.
Stephen Canon
7
Estou interessado em saber o que você usou como o "padrão ouro" aqui para calcular erros relativos - normalmente eu esperava que fosse a*a*a*a*a*a, mas esse aparentemente não é o caso! :)
j_random_hacker
8
@j_random_hacker: desde que eu estava comparando resultados de precisão única, a precisão dupla é suficiente para um padrão-ouro - o erro de um a a a a calculado em dobro é * muito menor que o erro de qualquer um dos cálculos de precisão única.
Stephen Canon
168

Um outro caso semelhante: a maioria dos compiladores não vai optimizar a + b + c + da (a + b) + (c + d)(esta é uma optimização desde a segunda expressão pode ser melhor pipeline) e avaliá-la como determinado (isto é, quanto (((a + b) + c) + d)). Isso também ocorre por causa dos casos de canto:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

Isso gera 1.000000e-05 0.000000e+00

sanjoyd
fonte
10
Isto não é exatamente o mesmo. Alterar a ordem das multiplicações / divisões (excluindo a divisão por 0) é mais seguro do que alterar a ordem da soma / subtração. Na minha humilde opinião, o compilador deve tentar associar mults./divs. porque isso reduz o número total de operações e, além do ganho de desempenho, também há um ganho de precisão.
CoffeDeveloper 07/07/14
4
@DarioOO: Não é mais seguro. Multiplicar e dividir são o mesmo que adição e subtração do expoente, e alterar a ordem pode facilmente fazer com que os temporários excedam o intervalo possível do expoente. (Não é exatamente o mesmo, porque o expoente não sofre perda de precisão ... mas a representação ainda é bastante limitado, e reordenação pode levar a valores irrepresentável)
Ben Voigt
8
Eu acho que você está perdendo algum histórico de cálculo. Multiplicar e dividir 2 números introduz a mesma quantidade de erro. Embora a subtração / adição de 2 números possa introduzir um erro maior, especialmente quando os 2 números são diferentes em ordem de grandeza, portanto, é mais seguro reorganizar a mul / dividir do que sub / add porque introduz uma pequena alteração no erro final.
CoffeDeveloper 5/03/15
8
@DarioOO: o risco é diferente com mul / div: a reordenação faz uma alteração insignificante no resultado final ou o expoente transborda em algum momento (onde não teria antes) e o resultado é massivamente diferente (potencialmente + inf ou 0)
Peter Cordes
@GameDeveloper A imposição de um ganho de precisão de maneiras imprevisíveis é extremamente problemática.
curiousguy
80

O Fortran (projetado para computação científica) possui um operador de energia embutido e, até onde eu sei, os compiladores do Fortran geralmente otimizam o aumento para potências inteiras de maneira semelhante à que você descreve. Infelizmente, o C / C ++ não possui um operador de energia, apenas a função de biblioteca pow(). Isso não impede que os compiladores inteligentes tratem powespecialmente e o computem de maneira mais rápida em casos especiais, mas parece que eles fazem isso com menos frequência ...

Alguns anos atrás, eu estava tentando torná-lo mais conveniente para calcular potências inteiras da maneira ideal, e criei o seguinte. É C ++, não C, e ainda depende do compilador ser um pouco inteligente sobre como otimizar / incorporar coisas. De qualquer forma, espero que você ache útil na prática:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

Esclarecimento para os curiosos: isso não encontra a maneira ideal de calcular potências, mas como encontrar a solução ideal é um problema completo de NP e vale a pena fazer para potências pequenas de qualquer maneira (ao contrário de usar pow), não há razão para se preocupar com o detalhe.

Então apenas use-o como power<6>(a).

Isso facilita a digitação de poderes (não é necessário especificar 6 as com parênteses) e permite que você tenha esse tipo de otimização sem -ffast-mathter algo dependente da precisão, como a soma compensada (um exemplo em que a ordem das operações é essencial) .

Você provavelmente também pode esquecer que este é C ++ e apenas usá-lo no programa C (se compilar com um compilador C ++).

Espero que isso possa ser útil.

EDITAR:

Isto é o que recebo do meu compilador:

Para a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

Para (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

Para power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
Szabolcs
fonte
36
Encontrar a árvore de energia ideal pode ser difícil, mas como é interessante apenas para pequenas potências, a resposta óbvia é pré-calculá-la uma vez (Knuth fornece uma tabela de até 100) e usar essa tabela codificada (é o que o gcc faz internamente para o powi) .
Marc Glisse
7
Nos processadores modernos, a velocidade é limitada pela latência. Por exemplo, o resultado de uma multiplicação pode estar disponível após cinco ciclos. Nessa situação, encontrar a maneira mais rápida de criar energia pode ser mais complicado.
gnasher729
3
Você também pode tentar encontrar a árvore de poder que fornece o limite superior mais baixo para o erro de arredondamento relativo ou o menor erro médio de arredondamento relativo.
gnasher729
1
O Boost também tem suporte para isso, por exemplo, boost :: math :: pow <6> (n); Eu acho que até tenta reduzir o número de multiplicações extraindo fatores comuns.
gast128
Observe que o último é equivalente a (a ** 2) ** 3
minmaxavg
62

GCC realmente optimizar a*a*a*a*a*aa (a*a*a)*(a*a*a)quando um é um número inteiro. Eu tentei com este comando:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Existem muitas bandeiras do gcc, mas nada sofisticado. Eles significam: Leia de stdin; use nível de otimização de O2; lista de linguagem assembly de saída em vez de um binário; a listagem deve usar a sintaxe da linguagem de montagem Intel; a entrada está no idioma C (geralmente o idioma é deduzido da extensão do arquivo de entrada, mas não há extensão de arquivo ao ler no stdin); e escreva para stdout.

Aqui está a parte importante da saída. Eu o anotei com alguns comentários indicando o que está acontecendo na linguagem assembly:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

Estou usando o sistema GCC no Linux Mint 16 Petra, um derivado do Ubuntu. Aqui está a versão do gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

Como outros pôsteres observaram, essa opção não é possível no ponto flutuante, porque a aritmética do ponto flutuante não é associativa.

picomancer
fonte
12
Isso é legal para multiplicação de números inteiros porque o excesso de complemento de dois é um comportamento indefinido. Se houver um estouro, isso acontecerá em algum lugar, independentemente das operações de reordenação. Portanto, expressões sem excesso avaliam o mesmo, expressões com excesso de comportamento indefinido; portanto, é aceitável que o compilador altere o ponto em que o excesso ocorre. O gcc também faz isso unsigned int.
Peter Cordes
51

Como um número de ponto flutuante de 32 bits - como 1.024 - não é 1.024. Em um computador, 1.024 é um intervalo: de (1.024-e) a (1.024 + e), em que "e" representa um erro. Algumas pessoas não conseguem perceber isso e também acreditam que * em * a significa multiplicação de números de precisão arbitrária sem que haja erros associados a esses números. A razão pela qual algumas pessoas não percebem isso talvez seja o cálculo matemático que exercitavam nas escolas de ensino fundamental: trabalhando apenas com números ideais sem erros, e acreditando que não há problema em simplesmente ignorar "e" durante a multiplicação. Eles não veem o "e" implícito em "float a = 1.2", "a * a * a" e códigos C similares.

Caso a maioria dos programadores reconheça (e consiga executar) a idéia de que a expressão C a * a * a * a * a * a não esteja realmente trabalhando com números ideais, o compilador GCC estará livre para otimizar "a * a * a * a * a * a "digamos" t = (a * a); t * t * t "que requer um número menor de multiplicações. Infelizmente, porém, o compilador GCC não sabe se o programador que está escrevendo o código pensa que "a" é um número com ou sem erro. E assim o GCC fará apenas a aparência do código-fonte - porque é isso que o GCC vê com seu "olho nu".

... depois que você souber que tipo de programador você é, poderá usar a opção "-ffast-math" para informar ao GCC que "Ei, GCC, eu sei o que estou fazendo!". Isso permitirá que o GCC converta um * a * a * a * a * a em um pedaço de texto diferente - parece diferente de um * a * a * a * a * a - mas ainda calcula um número dentro do intervalo de erro de a * a * a * a * a * a. Tudo bem, já que você já sabe que está trabalhando com intervalos, e não com números ideais.


fonte
52
Os números de ponto flutuante são exatos. Eles não são necessariamente exatamente o que você esperava. Além disso, a técnica com epsilon é uma aproximação de como lidar com as coisas na realidade, porque o verdadeiro erro esperado é relativo à escala da mantissa, ou seja, você normalmente tem cerca de 1 LSB de saída, mas isso pode aumentar com todas as operações executadas se você não for cuidadoso, consulte um analista numérico antes de fazer algo não trivial com ponto flutuante. Use uma biblioteca adequada, se puder.
Donal Fellows
3
@DonalFellows: O padrão IEEE exige que os cálculos de ponto flutuante produzam o resultado que corresponda com maior precisão ao resultado, se os operandos de origem fossem valores exatos, mas isso não significa que eles realmente representem valores exatos. Em muitos casos, é mais útil considerar 0,1f como sendo (1.677.722 +/- 0,5) / 16.777.216, que deve ser exibido com o número de dígitos decimais implícito nessa incerteza, do que considerá-lo como quantidade exata (1.677.722 +/- 0,5) / 16.777.216 (que deve ser exibido com 24 dígitos decimais).
Supercat #
23
@supercat: IEEE-754 é bastante clara sobre o ponto que os dados de ponto flutuante fazer representar valores exatos; as seções 3.2 - 3.4 são as seções relevantes. Obviamente, você pode optar por interpretá-las de outra forma, assim como pode interpretar int x = 3como significando x3 +/- 0,5.
Stephen Canon
7
@ supercat: Concordo inteiramente, mas isso não significa que Distancenão seja exatamente igual ao seu valor numérico; significa que o valor numérico é apenas uma aproximação de alguma quantidade física sendo modelada.
Stephen Canon
10
Para análise numérica, seu cérebro agradecerá se você interpretar números de ponto flutuante não como intervalos, mas como valores exatos (que, por acaso, não são exatamente os valores desejados). Por exemplo, se x é algo em torno de 4,5 com um erro menor que 0,1 e você calcula (x + 1) - x, a interpretação "intervalo" deixa você com um intervalo de 0,8 a 1,2, enquanto a interpretação "valor exato" indica você o resultado será 1 com um erro de no máximo 2 ^ (- 50) em dupla precisão.
gnasher729
34

Nenhum pôster mencionou ainda a contração de expressões flutuantes (norma ISO C, 6.5p8 e 7.12.2). Se o FP_CONTRACTpragma estiver definido como ON, o compilador poderá considerar uma expressão comoa*a*a*a*a*a como uma única operação, como se avaliada exatamente com um único arredondamento. Por exemplo, um compilador pode substituí-lo por uma função de energia interna que é mais rápida e precisa. Isso é particularmente interessante, pois o comportamento é parcialmente controlado pelo programador diretamente no código-fonte, enquanto as opções do compilador fornecidas pelo usuário final às vezes podem ser usadas incorretamente.

O estado padrão do FP_CONTRACTpragma é definido pela implementação, para que um compilador possa fazer essas otimizações por padrão. Portanto, o código portátil que precisa seguir estritamente as regras da IEEE 754 deve explicitamente configurá-lo para OFF.

Se um compilador não suporta esse pragma, ele deve ser conservador, evitando qualquer otimização, caso o desenvolvedor tenha escolhido configurá-lo OFF.

O GCC não suporta esse pragma, mas com as opções padrão, ele assume que é ON; portanto, para destinos com uma FMA de hardware, se alguém quiser impedir a transformação a*b+cem fma (a, b, c), precisará fornecer uma opção como -ffp-contract=off(definir explicitamente o pragma como OFF) ou -std=c99(dizer ao GCC para se adequar a alguns Versão padrão C, aqui C99, siga o parágrafo acima). No passado, a última opção não estava impedindo a transformação, o que significa que o GCC não estava em conformidade com este ponto: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845

vinc17
fonte
3
Às vezes, perguntas populares de longa duração mostram sua idade. Esta pergunta foi feita e respondida em 2011, quando o GCC poderia ser desculpado por não respeitar exatamente o padrão C99 então recente. Claro que agora é 2014, então o GCC… ahem.
Pascal Cuoq
Você não deveria responder perguntas de ponto flutuante relativamente recentes sem uma resposta aceita? tosse stackoverflow.com/questions/23703408 tosse
Pascal Cuoq
Acho ... perturbador que o gcc não implemente pragmas de ponto flutuante C99.
David Monniaux
1
Os pragmas do @DavidMonniaux são, por definição, opcionais para implementar.
Tim Seguine
2
@ TimSeguine Mas se um pragma não for implementado, seu valor padrão precisará ser o mais restritivo para a implementação. Suponho que era nisso que David estava pensando. Com o GCC, isso agora é corrigido para FP_CONTRACT se alguém usa um modo ISO C : ele ainda não implementa o pragma, mas, no modo ISO C, agora assume que o pragma está desativado.
precisa saber é
28

Como Lambdageek apontou, a multiplicação de flutuador não é associativa e você pode obter menos precisão, mas também quando obtém uma melhor precisão, pode argumentar contra a otimização, porque deseja uma aplicação determinística. Por exemplo, em cliente / servidor de simulação de jogo, em que todo cliente precisa simular o mesmo mundo em que você deseja que os cálculos de ponto flutuante sejam determinísticos.

Bjorn
fonte
3
@greggo Não, ainda é determinístico. Nenhuma aleatoriedade é adicionada em qualquer sentido da palavra.
Alice
9
@ Alice Parece bastante claro que Bjorn aqui está usando 'determinístico' no sentido do código, dando o mesmo resultado em diferentes plataformas e diferentes versões do compilador, etc (variáveis ​​externas que podem estar além do controle do programador) - em oposição à falta aleatoriedade numérica real em tempo de execução. Se você está apontando que esse não é um uso adequado da palavra, não vou discutir com isso.
greggo 8/09/14
5
@greggo Exceto mesmo na sua interpretação do que ele diz, ainda está errado; esse é o objetivo da IEEE 754, fornecer características idênticas para a maioria (se não todas) das operações entre plataformas. Agora, ele não fez nenhuma menção de plataformas ou versões de compilador, o que seria uma preocupação válida se você quiser que cada operação em cada servidor / cliente remoto seja idêntica ... mas isso não é óbvio em sua declaração. Uma palavra melhor pode ser "similarmente confiável" ou algo assim.
Alice
8
@ Alice, você está desperdiçando o tempo de todos, inclusive o seu, discutindo a semântica. Seu significado era claro.
Lanaru
11
@Lanaru Todo o ponto dos padrões é semântica; seu significado não era decididamente claro.
Alice
28

Funções de biblioteca como "pow" são geralmente criadas com cuidado para gerar o mínimo erro possível (no caso genérico). Isso geralmente é alcançado aproximando funções com splines (de acordo com o comentário de Pascal, a implementação mais comum parece estar usando o algoritmo Remez )

fundamentalmente a seguinte operação:

pow(x,y);

tem um erro inerente de aproximadamente a mesma magnitude que o erro em qualquer multiplicação ou divisão única .

Enquanto a seguinte operação:

float a=someValue;
float b=a*a*a*a*a*a;

tem um erro inerente que é maior que 5 vezes o erro de uma única multiplicação ou divisão (porque você está combinando 5 multiplicações).

O compilador deve ter muito cuidado com o tipo de otimização que está fazendo:

  1. se otimizar pow(a,6)a a*a*a*a*a*aque pode melhorar o desempenho, mas reduzir drasticamente a precisão de números de ponto flutuante.
  2. se otimizar a*a*a*a*a*a para pow(a,6)ele pode realmente reduzir a precisão, pois "a" era algum valor especial que permite a multiplicação sem erro (uma potência de 2 ou algum número inteiro pequeno)
  3. se otimizar pow(a,6)para (a*a*a)*(a*a*a)ou (a*a)*(a*a)*(a*a)ainda houver perda de precisão em comparação à powfunção.

Em geral, você sabe que para valores arbitrários de ponto flutuante "pow" tem melhor precisão do que qualquer função que você possa escrever, mas em alguns casos especiais várias multiplicações podem ter melhor precisão e desempenho, cabe ao desenvolvedor escolher o que é mais apropriado, eventualmente comentando o código para que ninguém mais "otimize" esse código.

A única coisa que faz sentido (opinião pessoal e, aparentemente, uma escolha no GCC que não inclua nenhum sinalizador de otimização ou compilador) a ser otimizado deve ser substituir "pow (a, 2)" por "a * a". Essa seria a única coisa sã que um fornecedor de compilador deve fazer.

CoffeDeveloper
fonte
7
os que recusam devem perceber que esta resposta está perfeitamente correta. Posso citar dezenas de fontes e documentação para apoiar minha resposta e provavelmente estou mais envolvido com a precisão do ponto flutuante do que qualquer downvoter. É perfeitamente razoável no StackOverflow adicionar informações ausentes que outras respostas não abrangem; portanto, seja educado e explique seus motivos.
CoffeDeveloper
1
Parece-me que a resposta de Stephen Canon cobre o que você tem a dizer. Você parece insistir em que as libms são implementadas com splines: elas geralmente usam redução de argumento (dependendo da função que está sendo implementada) mais um único polinômio cujos coeficientes foram obtidos por variantes mais ou menos sofisticadas do algoritmo Remez. A suavidade nos pontos de junção não é considerada um objetivo que vale a pena perseguir para as funções libm (se elas forem precisas o suficiente, elas serão automaticamente suaves de qualquer maneira, independentemente de quantas partes o domínio foi dividido).
Pascal Cuoq
A segunda metade da sua resposta perde completamente o ponto de que os compiladores devem produzir código que implementa o que o código fonte diz, ponto final. Você também usa a palavra "precisão" quando quer dizer "precisão".
Pascal Cuoq
Obrigado pela sua entrada, eu corrigi um pouco a resposta, algo novo ainda está presente nos últimos 2 linhas ^^
CoffeDeveloper
27

Eu não esperava que esse caso fosse otimizado. Não pode ser muito frequente que uma expressão contenha subexpressões que possam ser reagrupadas para remover operações inteiras. Eu esperaria que os escritores de compiladores investissem seu tempo em áreas que provavelmente resultariam em melhorias visíveis, em vez de cobrir um caso extremo raramente encontrado.

Fiquei surpreso ao saber das outras respostas que essa expressão poderia realmente ser otimizada com as opções apropriadas do compilador. A otimização é trivial, ou é um caso extremo de uma otimização muito mais comum, ou os escritores do compilador foram extremamente minuciosos.

Não há nada errado em fornecer dicas para o compilador, como você fez aqui. É uma parte normal e esperada do processo de micro-otimização reorganizar declarações e expressões para ver quais diferenças elas trarão.

Embora o compilador possa ser justificado ao considerar as duas expressões para fornecer resultados inconsistentes (sem as opções apropriadas), não há necessidade de você ficar vinculado por essa restrição. A diferença será incrivelmente pequena - tanto que, se a diferença importa para você, você não deve usar aritmética padrão de ponto flutuante em primeiro lugar.

Mark Ransom
fonte
17
Como observado por outro comentarista, isso é falso a ponto de ser absurdo; a diferença pode ser de metade a 10% do custo e, se for executada em um circuito fechado, isso se traduzirá em muitas instruções desperdiçadas para obter o que poderia ser uma quantidade insignificante de precisão adicional. Dizer que você não deve usar FP padrão quando estiver montando um monte é como dizer que você sempre deve usar um avião para atravessar o país; ignora muitas externalidades. Finalmente, essa NÃO é uma otimização incomum; análise de código morto e redução / refator de código são muito comuns.
Alice
21

Já existem algumas boas respostas para essa pergunta, mas, para fins de completude, eu gostaria de salientar que a seção aplicável do padrão C é 5.1.2.2.3 / 15 (que é a mesma que a seção 1.9 / 9 no Padrão C ++ 11). Esta seção afirma que os operadores só podem ser reagrupados se forem realmente associativos ou comutativos.

Rastaban
fonte
12

Na verdade, o gcc pode fazer essa otimização, mesmo para números de ponto flutuante. Por exemplo,

double foo(double a) {
  return a*a*a*a*a*a;
}

torna-se

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

com -O -funsafe-math-optimizations. Essa reordenação viola a IEEE-754, no entanto, portanto, requer a bandeira.

Inteiros assinados, como Peter Cordes apontou em um comentário, podem fazer essa otimização sem, -funsafe-math-optimizationsuma vez que ela é mantida exatamente quando não há excesso e, se houver excesso, você obtém um comportamento indefinido. Então você recebe

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

com apenas -O. Para números inteiros não assinados, é ainda mais fácil, pois eles trabalham com potências mod de 2 e, portanto, podem ser reordenados livremente, mesmo diante do estouro.

Charles
fonte
1
Link Godbolt com double, int e sem sinal. gcc e clang tanto optimizar todos os três o mesmo modo (com -ffast-math)
Pedro Cordes
@PeterCordes Thanks!
Charles