(A + B + C) ≠ (A + C + B) e reordenação do compilador


108

Adicionar dois inteiros de 32 bits pode resultar em um estouro de inteiros:

uint64_t u64_z = u32_x + u32_y;

Esse estouro pode ser evitado se um dos inteiros de 32 bits for convertido primeiro ou adicionado a um inteiro de 64 bits.

uint64_t u64_z = u32_x + u64_a + u32_y;

No entanto, se o compilador decidir reordenar a adição:

uint64_t u64_z = u32_x + u32_y + u64_a;

o estouro de inteiro ainda pode acontecer.

Os compiladores podem fazer esse reordenamento ou podemos confiar que eles notarão a inconsistência do resultado e manterão a ordem das expressões como está?


15
Na verdade, você não mostra um estouro de inteiro porque parece que há uint32_tvalores adicionados - que não estouram, eles quebram. Esses não são comportamentos diferentes.
Martin Bonner apoia Monica em

5
Veja a seção 1.9 dos padrões c ++, ela responde diretamente à sua pergunta (há até um exemplo que é quase exatamente igual ao seu).
Holt

3
@Tal: Como outros já declararam: não há estouro de inteiros. Os não assinados são definidos para embrulhar, pois os assinados são um comportamento indefinido, portanto, qualquer implementação servirá, incluindo daemons nasais.
muito honesto para este site

5
@Tal: Bobagem! Como já escrevi: o padrão é muito claro e requer acondicionamento, não saturação (isso seria possível com assinado, pois é UB conforme o padrão.
muito honesto para este site

15
@rustyx: Quer você chame de empacotamento ou estouro, o ponto que ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0resulta em permanece 0, enquanto (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)resulta em 0x100000000, e esses dois valores não são iguais. Portanto, é significativo se o compilador pode ou não aplicar essa transformação. Mas sim, o padrão só usa a palavra "estouro" para inteiros com sinal, não sem sinal.
Steve Jessop

Respostas:


84

Se o otimizador fizer esse reordenamento, ele ainda estará vinculado à especificação C, então esse reordenamento se tornaria:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Justificativa:

Começamos com

uint64_t u64_z = u32_x + u64_a + u32_y;

A adição é realizada da esquerda para a direita.

As regras de promoção de inteiro indicam que na primeira adição na expressão original, u32_xseja promovido para uint64_t. Na segunda adição, u32_ytambém será promovido a uint64_t.

Portanto, para estar em conformidade com a especificação C, qualquer otimizador deve promover u32_xe u32_ypara valores não sinalizados de 64 bits. Isso é equivalente a adicionar um elenco. (A otimização real não é feita no nível C, mas eu uso a notação C porque é uma notação que entendemos.)


Não é associativo à esquerda, então (u32_x + u32_t) + u64_a?
Inútil

12
@Useless: Klas lançou tudo para 64 bits. Agora, a ordem não faz nenhuma diferença. O compilador não precisa seguir a associatividade, apenas precisa produzir o mesmo resultado exato como se o fizesse.
gnasher729

2
Parece sugerir que o código do OP seria avaliado assim, o que não é verdade.
Inútil

@Klas - gostaria de explicar por que esse é o caso e como exatamente você chegou ao seu exemplo de código?
rustyx

1
@rustyx Precisava de uma explicação. Obrigado por me empurrar para adicionar um.
Klas Lindbäck

28

Um compilador só pode reordenar sob a regra como se . Ou seja, se a reordenação sempre der o mesmo resultado da ordenação especificada, ela é permitida. Caso contrário (como no seu exemplo), não.

Por exemplo, dada a seguinte expressão

i32big1 - i32big2 + i32small

que foi cuidadosamente construído para subtrair os dois valores que são conhecidos como grandes, mas semelhantes, e só então adicionar o outro valor pequeno (evitando assim qualquer estouro), o compilador pode escolher reordenar em:

(i32small - i32big2) + i32big1

e confie no fato de que a plataforma de destino está usando aritmética de dois complementos com wrap-round para evitar problemas. (Tal reordenação pode ser sensata se o compilador for pressionado por registradores e por acaso já houver i32smallum registrador).


O exemplo de OP usa tipos não assinados. i32big1 - i32big2 + i32smallimplica tipos assinados. Preocupações adicionais entram em jogo.
chux - Reintegrar Monica em

@chux Com certeza. O que eu estava tentando mostrar é que, embora eu não possa escrever (i32small-i32big2) + i32big1(porque pode causar UB), o compilador pode reorganizá-lo de forma eficaz porque o compilador pode ter certeza de que o comportamento será correto.
Martin Bonner apoia Monica em

3
@chux: Preocupações adicionais como UB não entram em jogo, porque estamos falando sobre um compilador reordenado sob a regra as-if. Um compilador específico pode tirar vantagem de conhecer seu próprio comportamento de estouro.
MSalters

16

Existe a regra "como se" em C, C ++ e Objective-C: o compilador pode fazer o que quiser, desde que nenhum programa em conformidade possa notar a diferença.

Nessas linguagens, a + b + c é definido como sendo o mesmo que (a + b) + c. Se você puder dizer a diferença entre isso e, por exemplo, a + (b + c), então o compilador não poderá alterar a ordem. Se você não consegue perceber a diferença, o compilador está livre para alterar a ordem, mas tudo bem, porque você não consegue perceber a diferença.

No seu exemplo, com b = 64 bits, aec 32 bits, o compilador teria permissão para avaliar (b + a) + c ou mesmo (b + c) + a, porque você não poderia dizer a diferença, mas não (a + c) + b porque você pode notar a diferença.

Em outras palavras, o compilador não tem permissão para fazer nada que faça seu código se comportar de forma diferente do que deveria. Não é necessário para produzir o código que você acha que ele iria produzir, ou que você acha que deveria produzir, mas o código vai dar-lhe exactamente os resultados que deveria.


Mas com uma grande ressalva; o compilador está livre para assumir nenhum comportamento indefinido (neste caso, estouro). Isso é semelhante a como uma verificação de estouro if (a + 1 < a)pode ser otimizada.
csiz

7
@csiz ... em variáveis assinadas . Variáveis ​​sem sinal têm semântica de estouro bem definida (wrap-around).
Gavin S. Yancey

7

Citando os padrões :

[Nota: Os operadores podem ser reagrupados de acordo com as regras matemáticas usuais apenas quando os operadores realmente são associativos ou comutativos.7 Por exemplo, no fragmento seguinte int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

a declaração de expressão se comporta exatamente da mesma forma que

a = (((a + 32760) + b) + 5);

devido à associatividade e precedência desses operadores. Assim, o resultado da soma (a + 32760) é a seguir adicionado a b, e esse resultado é então adicionado a 5, resultando no valor atribuído a a. Em uma máquina na qual os overflows produzem uma exceção e na qual o intervalo de valores representáveis ​​por um int é [-32768, + 32767], a implementação não pode reescrever esta expressão como

a = ((a + b) + 32765);

já que se os valores de aeb fossem, respectivamente, -32754 e -15, a soma a + b produziria uma exceção, enquanto a expressão original não; nem a expressão pode ser reescrita como

a = ((a + 32765) + b);

ou

a = (a + (b + 32765));

uma vez que os valores de aeb podem ter sido, respectivamente, 4 e -8 ou -17 e 12. No entanto, em uma máquina na qual os estouros não produzem uma exceção e na qual os resultados dos estouros são reversíveis, a declaração de expressão acima pode ser reescrito pela implementação em qualquer uma das maneiras acima, porque o mesmo resultado ocorrerá. - nota final]


4

Os compiladores podem fazer esse reordenamento ou podemos confiar que eles notarão a inconsistência do resultado e manterão a ordem das expressões como está?

O compilador pode reordenar apenas se der o mesmo resultado - aqui, como você observou, não.


É possível escrever um modelo de função, se você quiser, que promova todos os argumentos std::common_typeantes de adicionar - isso seria seguro, e não depende da ordem dos argumentos ou da conversão manual, mas é muito desajeitado.


Eu sei que a conversão explícita deve ser usada, mas desejo saber o comportamento do compilador quando essa conversão foi omitida por engano.
Tal de

1
Como eu disse, sem conversão explícita: a adição à esquerda é realizada primeiro, sem promoção integral e, portanto, sujeita a empacotamento. O resultado dessa adição, possivelmente agrupado, é então promovido uint64_tpara adição ao valor mais à direita.
Inútil

Sua explicação sobre a regra como se está totalmente errada. A linguagem C, por exemplo, especifica quais operações devem acontecer em uma máquina abstrata. A regra "como se" permite que ele faça absolutamente o que quiser, desde que ninguém perceba a diferença.
gnasher729

O que significa que o compilador pode fazer o que quiser, desde que o resultado seja o mesmo determinado pelas regras de associatividade à esquerda e conversão aritmética mostradas.
Inútil

1

Depende da largura do bit unsigned/int.

Os 2 abaixo não são os mesmos (quando unsigned <= 32bits). u32_x + u32_ytorna-se 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Eles são os mesmos (quando unsigned >= 34bits). As promoções de inteiros fizeram com que a u32_x + u32_yadição ocorresse na matemática de 64 bits. A ordem é irrelevante.

É UB (quando unsigned == 33bits). As promoções de inteiros fizeram com que a adição ocorresse na matemática de 33 bits assinados e o estouro assinado é UB.

Os compiladores têm permissão para fazer esse reordenamento ...?

(Matemática de 32 bits): Reordenar sim, mas os mesmos resultados devem ocorrer, portanto, não que a reordenação OP propõe. Abaixo estão os mesmos

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... podemos confiar que eles perceberão a inconsistência do resultado e manterão a ordem das expressões como está?

Confie, sim, mas o objetivo de codificação do OP não é muito claro. O u32_x + u32_ycarry deve contribuir? Se o OP deseja essa contribuição, o código deve ser

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Mas não

uint64_t u64_z = u32_x + u32_y + u64_a;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.