Por que Clang otimiza x * 1.0, mas NÃO x + 0.0?

125

Por que Clang otimiza o loop neste código

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

mas não o loop neste código?

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

(Marque C e C ++ porque gostaria de saber se a resposta é diferente para cada uma.)

user541686
fonte
2
Quais sinalizadores de otimização estão ativos no momento?
Iwillnotexist Idonotexist
1
@IwillnotexistIdonotexist: Acabei de usar -O3, não sei como verificar o que isso ativa.
user541686
2
Seria interessante ver o que acontece se você adicionar -ffast-math à linha de comando.
Plug
static double arr[N]não é permitido em C; constvariáveis não contam como expressões constantes nessa língua
MM
1
[Insira um comentário sarcástico sobre como C não é C ++, mesmo que você já o tenha chamado.] #
User253751

Respostas:

164

A norma IEEE 754-2008 para aritmética de ponto flutuante e a norma aritmética independente de idioma (LIA) ISO / IEC 10967, parte 1, respondem por que isso é assim.

IEEE 754 § 6.3 O bit de sinal

Quando uma entrada ou resultado é NaN, esse padrão não interpreta o sinal de um NaN. Observe, no entanto, que as operações nas cadeias de bits - copiar, negar, abs, copySign - especificam o bit de sinal de um resultado NaN, às vezes com base no bit de sinal de um operando NaN. O predicado lógico totalOrder também é afetado pelo bit de sinal de um operando NaN. Para todas as outras operações, este padrão não especifica o bit de sinal de um resultado NaN, mesmo quando há apenas uma entrada NaN ou quando o NaN é produzido a partir de uma operação inválida.

Quando nem as entradas nem o resultado são NaN, o sinal de um produto ou quociente é o OR exclusivo dos sinais dos operandos; o sinal de uma soma ou de uma diferença x - y considerada uma soma x + (−y) difere de no máximo um dos sinais dos adendos; e o sinal do resultado das conversões, a operação de quantização, as operações roundTo-Integral e roundToIntegralExact (consulte 5.3.1) é o sinal do primeiro ou único operando. Essas regras serão aplicadas mesmo quando operandos ou resultados forem zero ou infinitos.

Quando a soma de dois operandos com sinais opostos (ou a diferença de dois operandos com sinais semelhantes) é exatamente zero, o sinal dessa soma (ou diferença) deve ser +0 em todos os atributos de direção de arredondamento, exceto roundTowardNegative; sob esse atributo, o sinal de uma soma zero exata (ou diferença) deve ser -0. No entanto, x + x = x - (−x) mantém o mesmo sinal que x, mesmo quando x é zero.

O caso da adição

Sob o modo de arredondamento padrão (Round-a-Nearest, Ties-a-Even) , vemos que x+0.0produz x, exceto quando xé -0.0: Nesse caso, temos uma soma de dois operandos com sinais opostos cuja soma é zero, e §6.3 parágrafo 3 regras que essa adição produz +0.0.

Como +0.0não é bit a bit idêntico ao original -0.0, e esse -0.0é um valor legítimo que pode ocorrer como entrada, o compilador é obrigado a inserir o código que transformará potenciais zeros negativos em +0.0.

O resumo: No modo de arredondamento padrão, em x+0.0, sex

  • não é -0.0 , então xele próprio é um valor de saída aceitável.
  • é -0.0 , então o valor de saída deve ser +0.0 , que não é bit a bit idêntico a -0.0.

O caso da multiplicação

No modo de arredondamento padrão , esse problema não ocorre com x*1.0. Se x:

  • é um número (sub) normal, x*1.0 == xsempre.
  • é +/- infinity, então o resultado é +/- infinitydo mesmo sinal.
  • é NaN, então de acordo com

    IEEE 754 § 6.2.3 Propagação de NaN

    Uma operação que propaga um operando NaN para seu resultado e possui um único NaN como entrada deve produzir um NaN com a carga útil da entrada NaN, se representável no formato de destino.

    o que significa que o expoente e a mantissa (embora não seja o sinal) de NaN*1.0são recomendados para permanecer inalterado em relação à entrada NaN. O sinal não é especificado de acordo com §6.3p1 acima, mas uma implementação pode especificar que seja idêntico à fonte NaN.

  • é +/- 0.0, então o resultado é um 0com seu bit de sinal XORed com o bit de sinal de 1.0, de acordo com §6.3p2. Como o bit de sinal de 1.0é 0, o valor de saída é inalterado em relação à entrada. Assim, x*1.0 == xmesmo quando xé um zero (negativo).

O caso da subtração

No modo de arredondamento padrão , a subtração x-0.0também é não operacional, porque é equivalente a x + (-0.0). Se xé

  • é NaN , então, §6.3p1 e §6.2.3 se aplicam da mesma maneira que para adição e multiplicação.
  • é +/- infinity, então o resultado é+/- infinitydo mesmo sinal.
  • é um número (sub) normal, x-0.0 == xsempre.
  • é -0.0, então, em §6.3p2, temos " [...] o sinal de uma soma, ou de uma diferença x - y considerada uma soma x + (−y), difere de no máximo um dos sinais dos adendos; " Isso nos obriga a atribuir -0.0como resultado de (-0.0) + (-0.0), porque -0.0difere no sinal de nenhum dos adendos, enquanto +0.0difere no sinal de dois dos adendos, violando esta cláusula.
  • é +0.0, então, isso se reduz ao caso de adição (+0.0) + (-0.0)considerado acima em The Case of Addition , que por §6.3p3 está decidido a dar +0.0.

Como em todos os casos o valor de entrada é legal como saída, é permitido considerar x-0.0uma no-op e x == x-0.0uma tautologia.

Otimizações de mudança de valor

A norma IEEE 754-2008 possui a seguinte citação interessante:

IEEE 754 § 10.4 Significado literal e otimizações de mudança de valor

[...]

As seguintes transformações de alteração de valor, entre outras, preservam o significado literal do código-fonte:

  • A aplicação da propriedade de identidade 0 + x quando x não é zero e não é um NaN de sinalização e o resultado tem o mesmo expoente que x.
  • A aplicação da propriedade de identidade 1 × x quando x não é um NaN de sinalização e o resultado tem o mesmo expoente que x.
  • Alterando a carga ou sinal de um NaN silencioso.
  • [...]

Como todos os NaNs e todos os infinitos compartilham o mesmo expoente, e o resultado arredondado corretamente de x+0.0e x*1.0para finito xtem exatamente a mesma magnitude que x, seu expoente é o mesmo.

sNaNs

NaNs de sinalização são valores de interceptação de ponto flutuante; São valores especiais de NaN cujo uso como um operando de ponto flutuante resulta em uma exceção de operação inválida (SIGFPE). Se um loop que desencadeia uma exceção fosse otimizado, o software não se comportaria mais da mesma maneira.

No entanto, como o usuário2357112 aponta nos comentários , o Padrão C11 deixa explicitamente indefinido o comportamento dos NaNs de sinalização (sNaN ), de modo que o compilador pode assumir que eles não ocorrem e, portanto, as exceções que eles geram também não ocorrem. O padrão C ++ 11 omite a descrição de um comportamento para sinalizar NaNs e, portanto, também o deixa indefinido.

Modos de arredondamento

Nos modos alternativos de arredondamento, as otimizações permitidas podem mudar. Por exemplo, no modo Arredondar para Infinito Negativo , a otimização x+0.0 -> xse torna permitida, masx-0.0 -> x é proibida.

Para impedir que o GCC assuma os modos e comportamentos padrão de arredondamento, o sinalizador experimental -frounding-mathpode ser passado para o GCC.

Conclusão

Clang e GCC , mesmo em -O3, permanecem em conformidade com a IEEE-754. Isso significa que ele deve seguir as regras acima do padrão IEEE-754. nãox+0.0 é nem um pouco idêntico a xtodos, de xacordo com essas regras, mas x*1.0 pode ser escolhido assim : ou seja, quando

  1. Obedeça à recomendação de passar a carga útil inalterada de xquando é um NaN.
  2. Deixe o bit de sinal de um resultado NaN inalterado por * 1.0.
  3. Obedeça à ordem de XOR o bit de sinal durante um quociente / produto, quando nãox for um NaN.

Para ativar a otimização não segura IEEE-754 (x+0.0) -> x, o sinalizador -ffast-mathprecisa ser passado para Clang ou GCC.

Não existirá idonotexist
fonte
2
Advertência: e se for um NaN de sinalização? (Na verdade, eu pensei que poderia ter sido o motivo de alguma forma, mas eu realmente não sei como, então eu perguntei.)
user541686
6
@ Mehrdad: O anexo F, a parte (opcional) do padrão C que especifica a adesão de C à IEEE 754, explicitamente não cobre NaNs de sinalização. (C11 F.2.1., Primeira linha: "Esta especificação não define o comportamento dos NaNs de sinalização".) As implementações que declaram conformidade com o Anexo F permanecem livres para fazer o que desejam com NaNs de sinalização. O padrão C ++ tem seu próprio tratamento do IEEE 754, mas seja o que for (não estou familiarizado), duvido que ele especifique o comportamento do sinal NaN.
User2357112 suporta Monica
2
@Mehrdad: o sNaN invoca um comportamento indefinido de acordo com o padrão (mas provavelmente é bem definido pela plataforma), de modo que o compilador aqui é permitido.
22415 Joshua
1
@ user2357112: A possibilidade de interceptar erros como um efeito colateral para cálculos não utilizados geralmente interfere com muita otimização; se o resultado de um cálculo às vezes é ignorado, um compilador pode adiar o cálculo até que ele saiba se o resultado será usado, mas se o cálculo tiver produzido um sinal importante, isso pode ser ruim.
Supercat
2
Oh, olha, uma pergunta que se aplica legitimamente a C e C ++ que é respondida com precisão para os dois idiomas por uma referência a um único padrão. Isso tornará as pessoas menos propensas a reclamar sobre perguntas marcadas com C e C ++, mesmo quando a pergunta lida com uma linguagem comum? Infelizmente, acho que não.
Kyle Strand
35

x += 0.0não é um NOOP se xé -0.0. O otimizador pode remover todo o loop de qualquer maneira, pois os resultados não são usados. Em geral, é difícil dizer por que um otimizador toma as decisões que toma.

user2357112 suporta Monica
fonte
2
Na verdade, eu postei isso depois de ter acabado de ler o porquê de x += 0.0não ser um não-op, mas pensei que provavelmente não fosse esse o motivo, porque todo o loop deveria ser otimizado de qualquer maneira. Posso comprá-lo, é só não tão inteiramente convincente como eu estava esperando ...
user541686
Dada a propensão para as linguagens orientadas a objetos produzirem efeitos colaterais, eu imaginaria que seria difícil ter certeza de que o otimizador não está mudando o comportamento real.
Robert Harvey
Pode ser o motivo, já que com long longa otimização está em vigor (o fez com o gcc, que se comporta da mesma forma pelo menos duas vezes )
e2-e4
2
@ ringø: long longé um tipo integral, não um tipo IEEE754.
MSalters
1
Que tal x -= 0, é o mesmo?
Viktor Mellgren