Инкремент изменчивой переменной в C

Рассмотрим следующие три выражения:

++x;
x += 1;
x = x + 1;

Насколько я знаю, они идентичны по семантике, игнорируя перегрузку операторов в C++. Однако сегодня я прочитал утверждение, что они разные, особенно когда x объявлен volatile,

Чтобы проверить это утверждение, я написал следующее и скомпилировал его для PowerPC, AMD64, ARMv6 и 68k:

#include <stdint.h>

static volatile uint64_t x = 0;

void a(void)
{
    ++x;
}

void b(void)
{
    x += 1;
}

void c(void)
{
    x = x + 1;
}

На всех четырех этих платформах три функции выдают одинаковый вывод ассемблера, будь то -O1 или -O3. На AMD64 это было всего две инструкции:

incq    _x(%rip)
retq

Следовательно, есть ли правда за этим утверждением? Если так, то в чем разница, и как я могу это разоблачить?

NB: я прекрасно знаю, что volatile не гарантирует атомарность. Это не то, о чем я здесь спрашиваю - если только атомарность не отличается между этими тремя.

2 ответа

Решение

Из проекта стандартного раздела C++ 5.3.2 [expr.pre.incr] говорит:

Если x не относится к типу bool, выражение ++x эквивалентно x+=1

а также 5.17 [expr.ass] говорит:

Поведение выражения вида E1 op = E2 эквивалентно E1 = E1 op E2, за исключением того, что E1 оценивается только один раз.

Так ++x а также x += 1 эквивалентны.

Теперь один случай, когда x += 1 отличается от x = x + 1 в том, что E1 оценивается только один раз. В данном конкретном случае это не имеет значения, но мы можем придумать случай, когда это имеет значение:

#include <stdint.h>

volatile uint64_t x = 0;
volatile uint64_t y[2] = {0} ;

void c(void)
{
   y[x] = y[x] + 1;
}

в этом случае x будет оцениваться дважды, в отличие от этого случая:

void b(void)
{
   y[x] += 1;
}

и сеанс Godbolt показывает для b():

b():                                  # @b()
movq    x(%rip), %rax
incq    y(,%rax,8)
retq

и для c():

c():                                  # @c()
movq    x(%rip), %rax
movq    y(,%rax,8), %rax
incq    %rax
movq    x(%rip), %rcx
movq    %rax, y(,%rcx,8)
retq

Насколько я могу судить, это относится и к С11. Из раздела С11 6.5.3.1 Префиксные операторы увеличения и уменьшения:

Выражение ++E эквивалентно (E+=1).

и из раздела 6.5.16.2 Составное назначение:

Составное присваивание в форме E1 op = E2 эквивалентно выражению простого присваивания E1 = E1 op (E2), за исключением того, что значение E1 вычисляется только один раз

В абстрактной семантике все эти три выражения делают одно и то же. Они имеют доступ x чтобы получить его значение, рассчитайте новое значение, а затем сохраните обновленное значение обратно в x, Есть доступ и магазин. (Выражения также дают значение, которое отбрасывается).

Хотя x = x + 1 упоминает x дважды, левая сторона x не оценивается То есть не полностью: его значение не вычисляется. Он оценивается только в той степени, в которой определяется место, куда пойдет назначенное значение.

Так что здесь возможно двойная оценка местоположения: левая сторона определяет местоположение x и правая сторона тоже. Но определение местоположения не предполагает доступа к самому местоположению.

Для некоторых видов выражений определение местоположения предполагает доступ к значениям. Например:

a[i] = a[i] + 1;

Это сильно отличается от

i = i + 1

так как i здесь только вторичная переменная, значение которой должно быть известно, чтобы определить место хранения a[i] (а также i сам по себе даже не увеличивается). Если i является volatileто два абстрактных доступа к нему в a[i] = a[i] + 1 должен соответствовать двум фактическим доступам.

Другие вопросы по тегам