在查看了 bunch of other questions and their answers 之后,我得到了一致的印象,即 C 中普遍存在的关键字是“没有 6” 0104 中的关键字“0x9”。
甚至标准本身似乎也不够清晰,让每个人都同意 what it means 。
除其他问题外:
总结这个问题,似乎(在阅读了很多之后)“ volatile ”保证如下:该值不仅会从/到寄存器读取/写入,而且至少会读取/写入内核的 L1 缓存,顺序与读/写出现在代码中。但这似乎没用,因为在同一线程内从/向寄存器读取/写入已经足够了,而与 L1 缓存的协调并不能保证与其他线程协调的任何进一步内容。我无法想象什么时候只与 L1 缓存同步会很重要。
使用 1
唯一广泛同意使用 volatile 似乎是用于旧的或嵌入式系统,其中某些内存位置硬件映射到 I/O 功能,例如内存中的位控制(直接,在硬件中)灯,或内存中的一点告诉您键盘键是否按下(因为它由硬件直接连接到键)。
似乎 "use 1"不会出现在目标包括多核系统的可移植代码中。
使用 2
与“使用 1”没有太大区别的是可以由中断处理程序随时读取或写入的内存(可能控制灯或存储来自键的信息)。但是已经为此我们遇到了问题,根据系统,中断处理程序 might run on a different core 和 its own memory cache 和“ volatile ”不能保证所有系统上的缓存一致性。
所以 "use 2"似乎超出了 "volatile"所能提供的范围。
使用 3
我看到的唯一其他无可争议的用途是通过指向同一内存的不同变量来防止访问错误优化,编译器没有意识到是同一内存。但这可能只是无可争议的,因为人们并没有谈论它——我只看到一个提到它。而且我认为 C 标准已经认识到“不同”的指针(如指向函数的不同参数)可能指向相同的项目或附近的项目,并且已经指定编译器必须生成即使在这种情况下也能工作的代码。但是,我无法在最新的(500 页!)标准中快速找到这个主题。
所以 "use 3"也许根本不存在 ?
因此我的问题是:
在用于多核系统的可移植 C 代码中,“ volatile ”是否能保证任何东西?
编辑——更新
浏览 latest standard 后,看起来答案至少是非常有限的:
1、标准反复规定了对特定类型“volatile sig_atomic_t”的特殊处理。然而,该标准还指出,在多线程程序中使用信号函数会导致未定义的行为。所以这个用例似乎仅限于单线程程序与其信号处理程序之间的通信。
2. 标准还明确规定了与setjmp/longjmp 相关的“volatile”的含义。 (重要的示例代码在其他 questions 和 answers 中给出。)
所以更精确的问题变成了:
除了 (1) 允许单线程程序从其信号处理程序接收信息,或 (2) 允许 setjmp 代码查看在setjmp 和 longjmp?
这仍然是一个是/否的问题。
如果"is",如果您能展示一个无错误的可移植代码示例,如果省略“ volatile ”,该代码会变得有问题,那就太好了。如果“否”,那么对于多核目标,我想编译器可以自由地忽略这两种非常具体的情况之外的“ volatile ”。
最佳答案
To summarize the problem, it appears (after reading a lot) that "volatile" guarantees something like: The value will be read/written not just from/to a register, but at least to the core's L1 cache, in the same order that the reads/writes appear in the code.
不, 它绝对不是 。这使得 volatile 对于 MT 安全代码几乎毫无用处。
如果是这样,那么 volatile 对于由多个线程共享的变量将非常有用,因为在能够协作的典型 CPU(即主板上的多核或多 CPU)中对 L1 缓存中的事件进行排序是您需要做的全部以某种方式使 C/C++ 或 Java 多线程的正常实现成为可能,并且具有典型的预期成本(即,对于大多数原子或非满足互斥操作的成本并不高)。
但是无论是理论上还是实践中, volatile 都不会在缓存中提供任何有保证的排序(或“内存可见性”)。
(注:以下内容基于对标准文件的合理解释、标准的意图、历史实践,以及对编译器作者期望的深刻理解。这种方法基于历史、实践以及对真实人的期望和理解真实世界,这比解析一个文档中的文字更强大、更可靠,该文档不是众所周知的一流规范编写并且已经多次修订。)
在实践中, volatile 确实保证了 ptrace-ability ,即在任何优化级别 中为正在运行的程序使用调试信息的能力,以及调试信息对这些 volatile 对象有意义的事实:
ptrace
(一种类似 ptrace 的机制)在涉及 volatile 对象的操作之后在序列点设置有意义的断点:您真的可以在这些点上真正中断(请注意,这仅在您愿意设置许多断点时才有效)因为任何 C/C++ 语句都可以编译为许多不同的程序集起点和终点,就像在大规模展开的循环中一样); 在实践中,volatile 保证比严格的 ptrace 解释多一点:它还保证 volatile 自动变量在堆栈上有一个地址,因为它们没有分配给寄存器,寄存器分配会使 ptrace 操作更加微妙(编译器可以输出调试信息来解释变量如何分配给寄存器,但读取和更改寄存器状态比访问内存地址稍微复杂一些)。
请注意,完整的程序调试能力,即考虑到所有变量至少在序列点都是易变的,是由编译器的“零优化”模式提供的,这种模式仍然执行像算术简化这样的琐碎优化(通常不能保证没有优化所有模式)。但是 volatile 比非优化更强:
x-x
可以简化为非 volatile 整数 x
但不是 volatile 对象。所以 volatile 意味着保证按 编译,就像系统调用的编译器从源代码到二进制/汇编的翻译一样,编译器不会以任何方式重新解释、更改或优化。请注意,库调用可能是也可能不是系统调用。许多官方系统函数实际上是库函数,它们提供了一个薄层的插入,并且通常在最后服从内核。 (特别是
getpid
不需要进入内核,并且可以很好地读取操作系统提供的包含信息的内存位置。)易变交互是与真实机器 外部世界的交互,必须遵循“抽象机器”。它们不是程序部分与其他程序部分的内部交互。编译器只能推理它所知道的,即内部程序部分。
volatile 访问的代码生成应该遵循与该内存位置最自然的交互:它应该不足为奇。这意味着一些 volatile 访问应该是原子的:如果在架构上读取或写入
long
表示的自然方式是原子的,那么预期 volatile long
的读取或写入将是原子的,因为编译器应该例如,不要生成愚蠢的低效代码来逐字节访问 volatile 对象。您应该能够通过了解架构来确定这一点。您不必了解有关编译器的任何信息,因为 volatile 意味着编译器应该是透明的 。
但是 volatile 只是强制为特定情况下最不优化的预期程序集发出执行内存操作: volatile 语义意味着一般情况语义。
一般情况是编译器在没有关于构造的任何信息时所做的事情:f.ex。通过动态分派(dispatch)在左值上调用虚函数是一种一般情况,在编译时确定表达式指定的对象的类型后直接调用覆盖程序是一种特殊情况。编译器始终对所有构造进行一般情况处理,并且它遵循 ABI。
Volatile 在同步线程或提供“内存可见性”方面没有什么特别之处: volatile 仅提供抽象级别的保证 从正在执行或停止的线程内部看到,即 CPU 内核内部 :
只有第二点意味着 volatile 在大多数线程间通信问题中没有用;第一点与任何不涉及与 CPU 之外的硬件组件通信但仍在内存总线上的编程问题本质上无关。
从运行线程的内核的角度来看,volatile 提供有保证的行为的属性意味着传递给该线程的异步信号,从该线程的执行顺序的角度来看运行,请参阅源代码顺序中的操作.
除非您计划向您的线程发送信号(一种非常有用的方法来整合有关当前正在运行的线程的信息,而没有事先商定的停止点),否则 volatile 不适合您。
关于c - "volatile"是否能保证多核系统的可移植 C 代码中的任何内容?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58695320/