在 C 中递增一个 volatile 变量
Incrementing a volatile variable in 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
因此,这种说法有什么道理吗?如果是这样,有什么区别,我该如何公开它?
注意:我完全清楚 volatile
不能保证原子性。这不是我在这里要问的 - 除非原子性本身是三者之间的不同之处。
来自 C++ 标准草案部分 5.3.2
[expr.pre.incr] 说:
If x is not of type bool, the expression ++x is equivalent to x+=1
和5.17
[expr.ass] 说:
The behavior of an expression of the form E1 op = E2 is equivalent to
E1 = E1 op E2 except that E1 is evaluated only once.
所以++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;
}
和 b()
的 godbolt session shows:
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
前缀递增和递减运算符:
The expression ++E is equivalent to (E+=1).
以及来自第 6.5.16.2
部分的复合赋值:
Acompound assignment of the form E1 op= E2 is equivalent to the simple
assignment expression E1 = E1 op (E2), except that the lvalue E1 is
evaluated only once
在抽象语义中,所有这三个表达式都做同样的事情。他们访问 x
来检索它的值,计算新值,然后将更新后的值存储回x
。有一个访问和一个商店。 (表达式也会产生一个值,该值将被丢弃)。
虽然 x = x + 1
提到 x
两次,但左侧 x
未被评估。也就是说,不完全:它的value没有计算出来。它仅在确定分配值的位置的范围内进行评估。
因此这里可能存在对位置的双重评估:左侧确定 x
的位置,右侧也是如此。但是确定位置并不涉及访问位置本身。
对于某些类型的表达式,确定位置确实涉及访问值。例如:
a[i] = a[i] + 1;
这与
完全不同
i = i + 1
因为 i
在这里只是一个辅助变量,必须知道其值才能确定 a[i]
的存储位置(并且 i
本身甚至不会递增) .如果i
是volatile
,那么a[i] = a[i] + 1
中对它的两次抽象访问必然对应两次实际访问
考虑以下三个表达式:
++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
因此,这种说法有什么道理吗?如果是这样,有什么区别,我该如何公开它?
注意:我完全清楚 volatile
不能保证原子性。这不是我在这里要问的 - 除非原子性本身是三者之间的不同之处。
来自 C++ 标准草案部分 5.3.2
[expr.pre.incr] 说:
If x is not of type bool, the expression ++x is equivalent to x+=1
和5.17
[expr.ass] 说:
The behavior of an expression of the form E1 op = E2 is equivalent to E1 = E1 op E2 except that E1 is evaluated only once.
所以++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;
}
和 b()
的 godbolt session shows:
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
前缀递增和递减运算符:
The expression ++E is equivalent to (E+=1).
以及来自第 6.5.16.2
部分的复合赋值:
Acompound assignment of the form E1 op= E2 is equivalent to the simple assignment expression E1 = E1 op (E2), except that the lvalue E1 is evaluated only once
在抽象语义中,所有这三个表达式都做同样的事情。他们访问 x
来检索它的值,计算新值,然后将更新后的值存储回x
。有一个访问和一个商店。 (表达式也会产生一个值,该值将被丢弃)。
虽然 x = x + 1
提到 x
两次,但左侧 x
未被评估。也就是说,不完全:它的value没有计算出来。它仅在确定分配值的位置的范围内进行评估。
因此这里可能存在对位置的双重评估:左侧确定 x
的位置,右侧也是如此。但是确定位置并不涉及访问位置本身。
对于某些类型的表达式,确定位置确实涉及访问值。例如:
a[i] = a[i] + 1;
这与
完全不同i = i + 1
因为 i
在这里只是一个辅助变量,必须知道其值才能确定 a[i]
的存储位置(并且 i
本身甚至不会递增) .如果i
是volatile
,那么a[i] = a[i] + 1
中对它的两次抽象访问必然对应两次实际访问