【问题标题】:Semantics of volatilevolatile 的语义
【发布时间】:2017-08-18 11:32:29
【问题描述】:

为了这个问题,让我们只看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;
}

但我也保留了前面的示例,因为一些答案和 cmets 已经引用了该示例。

【问题讨论】:

  • 此外,在这种情况下,+ 的操作数的评估顺序与实现无关。程序员现在可以做任何假设吗?
  • Volatile 很像高级程序集。如果您真的想“优化”,请以您自己的名义将其存储在寄存器中。
  • @TatsuyukiIshi 我同意!但是程序员不会看到可观察到的定义行为的任​​何变化。
  • 如果你有一个标有“爆炸物”的盒子,邮递员可以把它当作非爆炸物,因为它不会经常爆炸吗?他当然可以!111!!!
  • Why is volatile needed in C? 的可能重复项很明显,声明为volatile 的变量在使用时将始终执行读取命令。即使使用序列!时期。故事结束。

标签: c compiler-construction language-lawyer volatile


【解决方案1】:

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

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

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

好吧,除了行为是未定义的,因为读取之间没有序列点,从 volatile 读取是一个副作用。一个正确的函数是

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

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

【讨论】:

  • 因此我添加了 para 另外我假设读取本身没有副作用的系统(例如计数器的增量,或者每次读取时值都在变化)。或者这样的系统是否存在?感谢您提供这样一个系统的示例。
  • 现在,这些东西如何符合标准?这些是否都适合某些或其他“实现定义的行为”?这让我得出结论,volatile 应该只在为特定目标编程时使用。它对可移植程序中的程序几乎没有改变。
  • 我同意我应该使用两个测序读取,以免与评估顺序混淆。
  • 该变量可以以可移植的方式在线程或进程之间共享。
  • 我不相信foo() 有问题,因为+ 是对称的,但是如果使用这些值的操作不是对称的,则很容易需要显式的 read'n'save 符号.
【解决方案2】:

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

您由此得出错误的结论。 值可能不会改变。但你不知道。而编译器不知道。

如果编译器不知道为什么要允许编译器假设任何更改? 因此很明显:不!编译器不得在此处组合任何读取访问权限。

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

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

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

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

volatile关键字是防止编译器作弊的手段。

【讨论】:

  • 我的论点是,如果两次读取都可以看到该值,那么如果让他们看到相同的值,程序员就不会抱怨。类比是,如果不能保证所有兔子都是白色的,我可以(强制)将兔子变成黑色。由于标准并没有说所有的兔子都应该是白色的,所以应该没问题。
  • 关于下半场我同意。我在问题中也提到了这一点。我的附带问题是读取具有直接副作用的系统示例有哪些?谢谢你的例子。
  • @AjayBrahmakshatriya。不,你的假设仍然是错误的。如果我不能确保所有兔子都是白色的,我可能仍然可以确保周围只有一只黑兔子。编译器(或您)无权让两只兔子都变成黑色。你做出了无效的假设。可能程序员不知道具体会读取哪些结果,编译器肯定没有任何线索,做一些花哨的猜测也不是编译器的事。
【解决方案3】:
int foo(volatile int * x) { return *x + *x; }

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

6.7.3p7:

7 具有 volatile 限定类型的对象可以通过以下方式修改 实现未知或有其他未知的副作用。 因此,任何引用此类对象的表达式都应为 严格按照抽象机的规则进行评估,如 在5.1.2.3 中描述。此外,在每个序列点的值 最后存储在对象应与规定的一致 抽象机器,除非被提到的未知因素修改 previous.134) 什么构成对具有 volatile 限定类型是实现定义的。


当然,您可以允许它通过引入一个临时的方法只读取一次:

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

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

【讨论】:

  • 这没有任何意义。只是不要将参数声明为 volatile。我明白你这样做是有原因的。编译器也会抱怨它。
  • @PeterJ 确实有道理。如果他希望 foo 只进行一次读取,无论出于何种原因,这就是方法。当然,如果这是一个单独编译的函数,他可以简单地删除 volatile 限定符,但如果没有上下文,foo 是内联的潜在候选者,在这种情况下,如果没有 volatile,则根本无法读取。
  • @PeterJ 没错,这就是为什么如果您希望 foo 始终对 x 执行 1 次读取,volatile 限定符不是可选的。
  • 很遗憾我无法理解你的意思,因为你写的那句话没有任何意义
  • 如果你不需要 volatile 不要使用它,如果你需要不要滥用它。简单
猜你喜欢
  • 1970-01-01
  • 2017-03-15
  • 1970-01-01
  • 2010-09-30
  • 2013-12-19
  • 1970-01-01
  • 2015-03-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多