(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.