(A + B + C) ≠ (A + C + B) и переупорядочение компилятора


добавление двух 32-разрядных целых чисел может привести к переполнению целого числа:

uint64_t u64_z = u32_x + u32_y;

этого переполнения можно избежать, если одно из 32-разрядных целых чисел сначала приведено или добавлено к 64-разрядному целому числу.

uint64_t u64_z = u32_x + u64_a + u32_y;

однако, если компилятор решит изменить порядок добавления:

uint64_t u64_z = u32_x + u32_y + u64_a;

переполнение целого числа все еще может произойти.

разрешено ли компиляторам делать такое переупорядочение или мы можем доверять им, чтобы заметить несоответствие результата и сохранить выражение порядок как есть?

6 109

6 ответов:

если оптимизатор делает такое переупорядочение, он все еще привязан к спецификации C, поэтому такое переупорядочение станет:

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

обоснование:

начнем с

uint64_t u64_z = u32_x + u64_a + u32_y;

дополнительно выполняется слева направо.

целочисленные правила продвижения утверждают, что в первом сложении в исходном выражении u32_x быть произведен в uint64_t. Во втором дополнении,u32_y также будет повышен до uint64_t.

Итак, по порядку чтобы соответствовать спецификации C, любой оптимизатор должен продвигать u32_x и u32_y до 64 разрядных значений без знака. Это эквивалентно добавлению приведения. (Фактическая оптимизация не выполняется на уровне C, но я использую нотацию C, потому что это нотация, которую мы понимаем.)

компилятору разрешено только переупорядочивать под как будто правило. То есть, если переупорядочение всегда будет давать тот же результат, что и указанное упорядочение, то оно допускается. В противном случае (как в вашем примере), нет.

например, учитывая следующее выражение

i32big1 - i32big2 + i32small

который был тщательно построен, чтобы вычесть два значения, которые, как известно, большие, но похожие, и только затем добавить другое небольшое значение (таким образом, избегая любое переполнение), компилятор может изменить порядок в:

(i32small - i32big2) + i32big1

и полагаться на то, что целевая платформа использует арифметику с двумя дополнениями с обтеканием для предотвращения проблем. (Такое переупорядочение может быть разумным, если компилятор нажат для регистров и имеет i32small в реестре уже).

существует правило "как будто" в C, C++ и Objective-C: компилятор может делать все, что ему нравится, пока никакая соответствующая программа не может сказать разницу.

в этих языках a + b + c определяется как (a + b) + c. Если вы можете сказать разницу между этим и, например, a + (b + c), то компилятор не может изменить порядок. Если вы не можете сказать разницу, то компилятор может изменить порядок, но это нормально, потому что вы не можете сказать разница.

в вашем примере, с b = 64 бит, A и c 32 бит, компилятор будет иметь возможность оценить (b + a) + c или даже (b + c) + a, потому что вы не могли бы сказать разницу, но не (a + C) + b, потому что вы можете сказать разницу.

другими словами, компилятору не разрешается делать ничего, что заставляет ваш код вести себя иначе, чем он должен. Не требуется создавать код, который вы думаете, что он будет производить, или что вы думаете, что он должен производить, но код будет дать вам именно те результаты, которые он должен.

слово стандарты:

[ Примечание: операторы могут быть перегруппированы в соответствии с обычным математические правила только там, где операторы действительно ассоциативны или коммутативный.7 например, в следующем фрагменте int a, b;

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

оператор выражения ведет себя точно так же, как

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

из-за ассоциативности и приоритета этих операторов. Таким образом, результат суммы (a + 32760) равен затем добавляется к b, и этот результат затем добавляют к 5, что приводит к значению. На машине в котором переполнения создают исключение и в котором диапазон значения, представимые с помощью int [-32768,+32767], реализация не удается переписать это выражение как

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

так как если бы значения для a и b были соответственно -32754 и -15, сумма a + b приведет к исключению, в то время как исходное выражение не было бы; и выражение не может быть переписывается либо как

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

или

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

так как значения для a и b могли быть соответственно 4 и -8 или -17 и 12. Однако на машине, в которой переполнения не производят исключение и в котором результаты переполнений реверзибельны, выше выражение оператор может быть переписан с помощью реализации в любой из вышеперечисленных способов, потому что тот же результат произойдет. - конец Примечание ]

разрешено ли компиляторам делать такое переупорядочение или мы можем доверять им, чтобы заметить несогласованность результата и сохранить порядок выражения как есть?

компилятор может изменить порядок, только если он дает тот же результат - здесь, как вы заметили, это не так.


можно написать шаблон функции, если вы хотите один, который способствует все аргументы std::common_type перед добавлением-это было бы безопасно, и не полагаться ни на порядок аргументов, ни на руководство кастинг, но он довольно неуклюжий.

это зависит от разрядности unsigned/int.

ниже 2 не то же самое (когда unsigned <= 32 бита). u32_x + u32_y становится 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.

они одинаковы (когда unsigned >= 34 бита). Целое число акций, причиненный u32_x + u32_y добавление происходит в 64-разрядной математике. Порядок не имеет значения.

это UB (когда unsigned == 33 bits). Целое число акций вызвало дополнение к происходят на подпись 33-битную архитектуру, и подписал переполнения УБ.

разрешены ли компиляторы сделать такое переупорядочение ...?

(32-битная математика): переупорядочить да, но те же результаты должны произойти, так что не это переупорядочение ОП предлагает. Ниже то же самое

// 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);

... можем ли мы доверять им, чтобы заметить несогласованность результата и сохранить порядок выражения как есть?

доверие да, но цель кодирования OP не кристально ясна. Должен u32_x + u32_y несут свой вклад? Если OP хочет этот вклад, код должен будь

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);

а не

uint64_t u64_z = u32_x + u32_y + u64_a;