一个C编译器是否允许合并对volatile变量的连续赋值?

63

硬件供应商报告了一种理论上的(非确定性,难以测试,从未在实践中出现)硬件问题,即对某些内存范围进行双字写入可能会破坏任何未来的总线传输。

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

编译器不允许重新排序易失性分配,但不清楚(对我来说)合并是否算作重新排序。我的直觉是它是,但我之前曾被语言专家纠正过!

示例:

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;

(我将单独向我的编译器供应商咨询此事,但我想知道标准的规范/社区解释是什么。)


https://dev59.com/PFQK5IYBdhLWcg3wPNpG - Lundin
7
你检查过编译器生成的汇编代码,看它是否执行了这个操作了吗? - Eric Postpischil
7
如果将内存映射为“可缓存”或“可写合并”,那么可能是MMU将两个单字写入组合成一个双字写入。 - Ian Abbott
@EricPostpischil 正在处理中。正在制作脚本以过滤可能的出现情况。项目构建系统有点抗拒 :-( - Andreas
@EricPostpischil 已确认。编译器不会合并这样的写入操作。虽然我没有考虑 -lto... 项目工具链和构建系统对于在链接后获取汇编代码的支持不太友好,因此不会进行进一步测试。我将相信社区共识和供应商支持渠道,相信这种情况不会发生。 - Andreas
显示剩余2条评论
5个回答

50
不,编译器绝对不允许将这两个写操作优化为单个双字写操作。由于有关优化和副作用的部分写得非常模糊,因此很难引用标准。相关部分可在C17 5.1.2.3找到: “国际标准中的语义描述描述了一个抽象机器的行为,在该机器中,优化问题是无关紧要的。” “访问易失性对象、修改对象、修改文件或调用执行这些操作的函数都是副作用,也就是更改执行环境状态。” “在抽象机器中,所有表达式都按语义规定进行评估。如果实际实现可以推断出某个表达式的值未被使用且未产生所需的任何副作用(包括通过调用函数或访问易失性对象可能引起的任何副作用),则它不需要评估该表达式的一部分。” “对易失性对象的访问严格按照抽象机器的规则进行评估。”访问结构体的一部分本身就是副作用,可能会产生编译器无法确定的后果。例如,假设你的结构体是硬件寄存器映射,这些寄存器需要按照特定顺序写入。例如,某些微控制器文档可能会这样描述:“reg0启用硬件外设,在配置reg1中的详细信息之前必须将其写入”。任何将volatile对象的写合并为单个写的编译器都是不符合规范且有缺陷的。

3
哦,我没有考虑到结构体的访问。在这种情况下,指针就不应该是volatile的,只有成员变量才需要是volatile的(然后我们就会进入嵌套的volatile地狱)。唉,C语言真难啊。很高兴看到你能够超越这个问题。实际的代码并没有这个方面的问题,但它太复杂了,不适合作为一个好的例子。 - Andreas
12
如果结构体的访问是volatile的,即使成员没有声明为volatile,成员访问也将是volatile的。同样适用于"const"。 - Ian Abbott
这是一个非常普遍的误解,但却是错误的。标准将程序文本映射到抽象机器的可观察操作序列上,它并不涉及这些操作如何在现实中反映出来。此外,它明确指出了什么构成了易失性访问,哪些被视为易失性的外部可观测行为是由具体实现定义的。标准对目标代码没有任何规定。 - philipxy

31

编译器不允许将两个这样的赋值合并为一个内存写入操作。必须有两个来自核心的独立写入。@Lundin的回答提供了C标准的相关参考。

然而,请注意缓存(如果存在)可能会欺骗您。关键字volatile并不意味着“未缓存”的内存。因此,除了使用volatile之外,还需要确保地址0xFF000000被映射为未缓存的地址。如果该地址被映射为缓存地址,则缓存硬件可能会将这两个赋值合并为单个内存写入。换句话说,对于缓存内存,两个核心内存写操作最终可能会成为系统内存接口上的单个写操作。


6
“volatile” 绝对意味着非缓存内存。对于预取读取“volatile”限定变量的系统是不合规的。必须根据变量周围的序列点执行“volatile”访问。随着CPU的发展,硬件和/或编译器供应商曾试图将这种内存屏障行为的负担转嫁给应用程序员。但C从未允许“volatile”访问的推测或乱序执行。如果有人发布无法执行符合C标准的硬件,则这不是应用程序员的错。 - Lundin
6
@Lundin,我希望您能提供一些支持该主张的参考资料,因为我不同意。此外,这个小例子https://ideone.com/U8Sq9n表明编译器对volatile变量的映射与普通变量没有任何区别。 - Support Ukraine
17
@Lundin说:“C从来没有允许对易失性访问进行投机执行或乱序执行”,这与“不可缓存”是不同的。你似乎在谈论不将加载/存储器提升出汇编循环,但这与写回高速缓存内存区域的硬件预取完全不同。你可以将其看作C保证了加载/存储器“到高速缓存一致性域”的是可见副作用,而不是DRAM的真实内容。软件无法观察DRAM(除非可能通过同一物理地址的另一个映射或在具有非一致共享内存的假想系统上观察)。 - Peter Cordes
13
如果您希望MMIO访问正常工作,即使您手写汇编,也需要确保包括MMIO地址的地址范围被映射为不可缓存;对于全局的volatile int foo;,要求C编译器这样做是不切实际和不现实的。 - Peter Cordes
7
你可以将自动变量标记为 volatile。这是否意味着编译器必须发出代码来关闭该部分堆栈的缓存?我以前从未见过这种情况,似乎很荒谬。(标记为 volatile 的自动变量非常有用,例如,如果你通过调试器逐步执行程序并想要更改它)。 - fuz
显示剩余24条评论

15

volatile的行为似乎取决于具体的实现,这在一定程度上是由于一个令人好奇的句子所致:“什么构成了对具有volatile限定类型的对象的访问是由实现定义的。”。

在ISO C 99的第5.1.2.3节中也有以下内容:

3 在抽象机器中,所有表达式都按照语义规定进行求值。如果实际实现可以推断出表达式的某个部分的值未被使用且不会产生任何必需的副作用(包括任何通过调用函数或访问volatile对象引起的副作用),则不需要对其进行求值。

因此,尽管已经规定必须根据抽象语义(即不优化)来处理volatile对象,但令人好奇的是,抽象语义本身允许消除死代码和数据流,这些都是优化的示例!

恐怕要想知道volatile会做什么,您需要查看编译器的文档。


让我深入研究供应商文档。在描述实现定义行为的部分中找到了这个:“*什么构成对具有挥发性限定类型(6.7.3)的对象的访问。” - 对具有挥发性类型的对象的任何引用都会导致访问。访问挥发性对象的顺序由源代码中表达的顺序定义。对非挥发性对象的引用按任意顺序安排,受依赖关系的限制。通过向编译器传递标志,可以使任何挥发性访问成为内存屏障! - Andreas
@Andreas 关于原始问题:如果您有一个依赖于对该结构的写入不被合并的程序,那么您很可能不在可移植的ISO C领域内。另一方面,在严格符合规范的程序中,有几个必需使用volatile的情况:在异步信号处理程序中使用volatile sig_atomic_t,以及在函数的本地变量上使用volatile,这些变量在使用setjmp保存上下文并使用longjmp恢复之间进行修改。就标准C而言,我们可以将volatile视为存在于这些情况下。 - Kaz

9
C标准对于易失变量的操作与实际机器操作之间的任何关系是不可知的。虽然大多数实现会指定这样的结构:*(char volatile*)0x1234 = 0x56;将生成一个值为0x56的字节存储到硬件地址0x1234,但一个实现可以在其自由裁量范围内分配例如8192字节的数组空间,并指定*(char volatile*)0x1234 = 0x56;将立即将0x56存储到该数组的0x1234元素中,而不会对硬件地址0x1234进行任何操作。另外,一种实现可能包括一些进程,周期性地将数组0x1234处的任何内容存储到硬件地址0x56中。
符合标准的唯一要求是,在单个线程内对易失变量的所有操作从“抽象机”的角度来看都被认为是绝对有序的。从标准的角度来看,实现可以以任何方式将这些访问转换为真正的机器操作。

2
此外,什么构成了volatile访问是由实现定义的。 - philipxy
1
@philipxy: 的确如此。商业编译器通常会将易失性写操作视为强制编译器有效刷新所有“寄存器缓存”对象,使得为特定平台上任何此类编译器编写的后台 I/O 之类的代码能够与使用类似语义的任何其他供应商编译器一起工作。然而,Clang 和 gcc 拒绝支持这样的语义,因为它们认为这样的代码是“有问题的”。 - supercat

6

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


6
只有当实现选择将其规定为这样时,实际硬件内存操作的序列才是“可观察的”。没有什么可以禁止实现包括自己的虚拟机,其中易失性存储立即更新虚拟机状态,但这些更新需要一段时间才能转换为对真实机器硬件的操作。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接