Приращение изменчивой переменной в C
Рассмотрим следующие три выражения:
++x;
x += 1;
x = x + 1;
Насколько мне известно, они идентичны в семантике, игнорируя перегрузку операторов на С++. Однако сегодня я прочитал утверждение, что они разные, особенно когда 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
не гарантирует атомарность. Это не то, о чем я прошу здесь, - если сама атомарность не отличается от трех.
Ответы
Ответ 1
Из проекта стандартного раздела С++ 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
Насколько я могу судить, это относится и к C11. Из раздела C11 6.5.3.1
Операторы приращения и уменьшения префиксов:
Выражение ++ E эквивалентно (E + = 1).
и из раздела 6.5.16.2
Составное назначение:
Сопутствующее присвоение формы E1 op = E2 эквивалентно простому выражение E1 = E1 op (E2), за исключением того, что lvalue E1 является оценивается только один раз
Ответ 2
В абстрактной семантике все три этих выражения делают то же самое. Они получают доступ к 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
должны соответствовать двум действительным доступам.