Подтвердить что ты не робот

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

может произойти переполнение целых чисел.

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

4b9b3361

Ответ 1

Если оптимизатор выполняет такое переупорядочение, он все еще привязан к спецификации 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, потому что это обозначение, которое мы понимаем.)

Ответ 2

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

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

i32big1 - i32big2 + i32small

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

(i32small - i32big2) + i32big1

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

Ответ 3

В 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, потому что вы можете сказать разницу.

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

Ответ 4

Цитата из стандартов:

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

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

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

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

из-за ассоциативности и приоритета этих операторов. Таким образом результат суммы (a + 32760) добавляется к b, и этот результат затем добавляется к 5, что приводит к значению, присвоенному a. На машине в которых переполнение создает исключение и в котором диапазон значения, представляемые 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. Однако на машине, в которой переполнение не производит исключение и в котором результаты переполнения обратимы, выше выражение выражения может быть переписано реализацией в любой из вышеуказанных способов, потому что тот же результат будет иметь место. - конечная нота]

Ответ 5

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

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


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

Ответ 6

Это зависит от ширины бита 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). Целочисленные акции вызвали добавление к подписанной 33-битной математике, а подписанное переполнение - UB.

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

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

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