易变的

时间:2017-08-18 11:32:30

标签: c compiler-construction language-lawyer volatile

为了这个问题,让我们看一下volatile变量的读数。我读过的所有讨论中,唯一的结论是对声明为volatile的同一变量的多次读取无法优化为单一效果。

但我相信这有点严格。考虑对变量进行两次读取,它们之间没有任何副作用,或者没有读取它们之间的任何其他volatile变量。

现在我们知道volatile变量中的值可以随时更改(编译器没有提示)。但是程序员无法确保在两次读取之间发生更改。这意味着看到相同值的两个读取都是程序的有效行为。

编译器是否可以强制执行此行为?执行一次读取并使用该值两次。

例如

int foo(volatile int * x) {
    return *x + *x;
}

在这种情况下,编译器可以执行单次读取吗?

我希望我的查询清楚。

此外,我假设一个系统,其中读取本身没有副作用(如计数器的增量,或每次读取时值的变化)。或者这样的系统存在吗?

我查看了从gccclang生成的程序集,即使进行了最大程度的优化,它们也会插入两个读取。我的问题是他们过于保守了吗?

编辑:为了不使我的问题复杂化并避免与实现定义的子表达式评估顺序混淆,我们可以看看这个例子 -

int foo(volatile int * x) {
    int a = *x;
    int b = *y;
    return a + b;
}

但我也保留上一个例子,因为有些答案和评论引用了这一点。

3 个答案:

答案 0 :(得分:4)

从内存位置读取可能会产生副作用。当然,该程序必须使用超过标准的C.阅读只能在依赖于实现定义的行为的程序中产生副作用。

一个常见的例子是从memory-mapped peripheral读取。在许多体系结构中,当数据被读取或写入特定范围的存储器位置时,主处理器与外围设备交换数据。如果内存位置映射到外设,则执行两次读取会向外设发送两个读取请求。外设可以对每次读取执行非幂等操作。

例如,从串行通信外设读取一个字节每次都会传输外设输入队列中的下一个字节。因此,如果使用该串行外设的字节读取寄存器的地址调用foo,则它从外设的读缓冲区中拉出两个连续的字节。不允许编译器将行为更改为只读取一个字节。

好吧,除了行为是未定义的,因为读取之间没有序列点,并且从volatile中读取是副作用。正确的功能是

int foo2(volatile int *x) {
    int x1 = *x;
    int x2 = *x;
    return x1 + x2;
}

我希望大多数编译器为foo生成与foo2相同的代码。

答案 1 :(得分:3)

  

现在我们知道volatile变量中的值可以随时更改(编译器没有提示)。但是程序员无法确保在两次读取之间发生更改。这意味着看到相同值的两个读取都是程序的有效行为。

你从中得出了错误的结论。 值可能不会发生变化。但你不知道。并且编译器不知道。

如果编译器不知道为什么应该允许编译器假设有关更改的任何内容? 因此明确:不!编译器不能在这里组合任何读访问权。

这是一个奇怪的假设。 如果你不能确保所有的兔子都是白色的,那怎么可能是一个有效的假设,都是黑色的?

第一次读取本身也会导致值发生变化。

如果你看一些硬件,单独进行读访问至关重要。 一些定时器或中断控制器在读取时会清除一些位。

UART或以太网控制器也可以通过一个地址提供整个接收缓冲区。你必须从同一地址多次阅读。

volatile关键字是阻止编译器执行操作的方法。

答案 2 :(得分:2)

int foo(volatile int * x) { return *x + *x; }

必须生成两个读取。无法优化易失性访问。他们就像IO。

6.7.3p7

  

7具有volatile限定类型的对象可能会以某种方式进行修改   未知的实施或具有其他未知的副作用。   因此,任何涉及这种对象的表达方式都应如此   严格按照抽象机的规则进行评估,如   在5.1.2.3中描述。此外,在每个序列点的值   最后存储在对象中的应与该规定的一致   抽象机,除了由提到的未知因素修改   previous.134)什么构成对具有的对象的访问   volatile-qualified类型是实现定义的。

当然,您可以允许通过引入临时内容来执行一次阅读:

int foo(volatile int * x) { int x_cp = *x; return x_cp + x_cp; }

(根据generated assembly判断,gcc会在优化级别-O1或更高级别提示。)