是否允许 C 编译器合并对 volatile 变量的顺序赋值?

Is a C compiler allowed to coalesce sequential assignments to volatile variables?

我遇到了硬件供应商报告的理论上的(不确定的、难以测试的、实践中从未发生过的)硬件问题,其中双字写入某些内存范围可能会破坏任何未来的总线传输。

虽然我没有在 C 代码中明确写入任何双字,但我担心允许编译器(在当前或未来的实现中)将多个相邻字赋值合并为一个双字赋值。

不允许编译器对 volatiles 的赋值重新排序,但(对我而言)不清楚合并是否算作重新排序。我的直觉说是,但我之前被语言律师纠正过!

示例:

typedef struct
{
   volatile unsigned reg0;
   volatile unsigned reg1;
} Module;

volatile Module* module = (volatile Module*)0xFF000000u;

// two word stores, or one double-word store?
module->reg0 = 1;
module->reg1 = 2;

(我会单独询问我的编译器供应商,但我很好奇标准的 canonical/community 解释是什么。)

不行,编译器绝对不允许将那两次写入优化为单个双字写入。很难引用标准,因为关于优化和副作用的部分写得太模糊了。相关部分见C17 5.1.2.3:

The semantic descriptions in this International Standard describe the behavior of an abstract machine in which issues of optimization are irrelevant.

Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.

In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).

Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.

当您访问结构的一部分时,这本身就是一种副作用,可能会产生编译器无法确定的后果。例如,假设您的结构是一个硬件寄存器映射,并且这些寄存器需要按特定顺序写入。例如,一些微控制器文档可能是这样的:“reg0 启用硬件外围设备,必须先写入,然后才能在 reg1 中配置详细信息”。

volatile 对象写入合并为一个的编译器将是不合格的并且完全损坏。

改变它会改变程序的可观察行为。所以不允许编译器这样做。

编译器不允许将两个这样的赋值写入单个内存。内核必须有两个独立的写入。 @Lundin的回答给出了C标准的相关参考。

但是,请注意缓存(如果存在)可能会欺骗您。关键字 volatile 并不意味着“未缓存”内存。所以除了使用 volatile 之外,你还需要确保地址 0xFF000000 被映射为未缓存。如果地址被映射为高速缓存,高速缓存 HW 可能会将两次分配变成一次内存写入。换句话说 - 对于缓存内存,两个核心内存写入操作可能最终会成为系统内存接口上的单个写入操作。

C 标准不知道易失性对象上的操作与实际机器上的操作之间的任何关系。虽然大多数实现会指定像 *(char volatile*)0x1234 = 0x56; 这样的构造会生成值为 0x56 的字节存储到硬件地址 0x1234,但实现可以在闲暇时为例如分配 space。一个 8192 字节的数组并指定 *(char volatile*)0x1234 = 0x56; 将立即将 0x56 存储到该数组的元素 0x1234,而无需对硬件地址 0x1234 执行任何操作。或者,一个实现可能包括一些进程,该进程定期将该数组的 0x1234 中的任何内容存储到硬件地址 0x56。

一致性所需要的只是在单个线程中对易失性对象的所有操作,从抽象机的角度来看,被视为绝对有序的。从标准的角度来看,实现可以以他们认为合适的任何方式将此类访问转换为真实的机器操作。

volatile 的行为似乎取决于实现,部分原因是一句奇怪的句子说:“什么构成对具有 volatile 限定类型的对象的访问是实现定义的”。

在 ISO C 99 的第 5.1.2.3 节中,还有:

3 In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).

因此,尽管要求 volatile 对象必须按照抽象语义进行处理(即未优化),但奇怪的是,抽象语义本身 允许消除死代码和数据流,这是优化的例子!

恐怕要知道 volatile 会做什么和不会做什么,您必须查看编译器的文档。