“ volatile”对于多核系统的可移植C代码是否有任何保证?

看了bunch of other questions and their answers之后,我的印象是 C语言中的“ volatile”关键字的确切含义尚未达成广泛共识。

即使标准本身似乎还不够清晰,每个人都无法同意what it means

其他问题:

  1. 根据您的硬件和编译器,它似乎提供了不同的保证。
  2. 它会影响编译器优化,但不会影响硬件优化,因此,在执行自己的运行时优化的高级处理器上,甚至不清楚编译器能否阻止您希望阻止的任何优化。 (某些编译器确实会生成指令来阻止某些系统上的某些硬件优化,但这似乎并未以任何方式进行标准化。)

总结问题,似乎(经过大量阅读)“ volatile”保证了类似的内容:该值将不仅从寄存器读取/写入,而且至少写入内核的L1缓存,其顺序与读/写在代码中的顺序相同。但这似乎没有用,因为在同一线程内从寄存器读/写到寄存器已足够,而与L1缓存协调并不能保证关于与其他线程的协调的任何其他信息。我无法想象仅与L1缓存进行同步的重要性。

使用1
volatile唯一被广泛认可的用法似乎是用于旧的或嵌入式系统,其中某些内存位置通过硬件映射到I / O功能,例如内存中的某个位(直接(在硬件中)控制灯光),或者内存中的一点告诉您键盘键是否按下(因为它是通过硬件直接连接到键的)。

在目标包括多核系统的可移植代码中,似乎没有出现“ use 1”。

使用2
与“使用1”没有太大区别的是可以由中断处理程序(可以控制灯光或存储来自按键的信息)在任何时间读取或写入的内存。但是为此已经存在一个问题,即取决于系统,带有might run on的中断处理程序a different core its own memory cache和“ volatile”不能保证所有系统上的缓存一致性。

因此,“使用2”似乎超出了“易失性”所能提供的范围。

使用3
我看到的唯一其他无可争议的用途是防止通过指向编译器未意识到的相同内存的不同变量的不同变量对访问进行错误优化。但这可能只是无可争议的,因为人们没有在谈论它-我只看到其中一个提及。而且我认为C标准已经认识到“不同”的指针(如指向函数的不同args)可能指向同一项目或附近的项目,并且已经指定编译器必须生成即使在这种情况下也可以工作的代码。但是,我无法在最新的标准(500页!)中快速找到该主题。

所以“使用3个”可能根本不存在

我的问题:

“易失性”是否可以保证多核系统的可移植C代码中的任何内容?


编辑-更新

浏览latest standard后,看来答案至少是非常有限是:
1.标准针对特定类型“ volatile sig_atomic_t”反复指定特殊处理。但是该标准还说,在多线程程序中使用信号功能会导致不确定的行为。因此,该用例似乎仅限于单线程程序与其信号处理程序之间的通信。
2.该标准还为setjmp / longjmp指定了“ volatile”的明确含义。 (重要的示例代码在其他questionsanswers中给出。)

因此,更精确的问题变为:
除(1)允许单线程程序从其信号处理程序接收信息,或(2)允许setjmp代码查看变量外,“ volatile”是否对多核系统的便携式C代码有任何保证?在setjmp和longjmp之间修改?

这仍然是一个是/否问题。

如果为“ yes”,那么可以显示一个无错误的可移植代码示例,如果省略了“ volatile”,则该示例会出现错误,这将是很好的。如果为“否”,那么我认为对于多核目标,在这两种非常特殊的情况下,编译器可以随意忽略“易失性”。

xuanyuanwsx 回答:“ volatile”对于多核系统的可移植C代码是否有任何保证?

我不是专家,但是cppreference.com在我看来information on volatile相当不错。这是要点:

  

通过左值表达式进行的每次访问(读和写)   挥发分定型的药物被认为对   优化的目的,并严格按照   抽象机的规则(即所有写入均在   在下一个序列点之前的某个时间)。这意味着在   单线程执行,易失性访问无法优化   或相对于另一个可见的副作用重新排序   通过易失性访问中的一个顺序点。

它也有一些用途:

  

挥发物的用途

     

1)静态易失性对象对内存映射的I / O端口进行建模,而静态   const volatile对象对内存映射的输入端口进行建模,例如   实时时钟

     

2)将sig_atomic_t类型的静态易失对象用于   与信号处理程序进行通信。

     

3)对于包含   setjmp宏的调用是唯一保证的局部变量   在longjmp返回后保留其值。

     

4)此外,volatile变量可用于禁用某些   优化形式,例如禁用无效存储消除或   不断折叠以进行微基准测试。

当然,它提到volatile对于线程同步没有用:

  

请注意,易失性变量不适合通信   线程之间;它们不提供原子性,同步性或   内存排序。读取易失性变量的值,该变量由   另一个没有同步或并发修改的线程   由于数据争用,两个未同步的线程是未定义的行为。

,

首先,从历史上看,关于volatile访问的含义和类似含义的不同理解存在各种障碍。参见这项研究:Volatiles Are Miscompiled,and What to Do about It

除了该研究中提到的各种问题之外,volatile的行为是可移植的,除了它们的一个方面:当它们充当内存屏障时。内存屏障是一种可以防止并发无序执行代码的机制。使用volatile作为内存屏障当然是不可移植的。

尽管我个人认为该语言很清楚,但是C语言是否保证volatile的存储行为显然是有争议的。首先,我们有副作用的正式定义,C17 5.1.2.3:

  

访问volatile对象,修改对象,修改文件或调用执行任何这些操作的函数都是副作用,它们都是状态的变化。执行环境。

该标准定义了术语排序,作为确定评估(执行)顺序的一种方式。定义是正式而繁琐的:

  

先于排序是评估之间的不对称,可传递,成对关系       由单个线程执行,这会导致这些评估中的部分顺序。       给定任意两个求值A和B,如果A在B之前排序,则执行A       应该在执行B之前执行。(相反,如果A在B之前排序,则B是       (在A之后排序。)如果A不在B之前或之后排序,则A和B为       未排序。当对A进行排序时,对评估A和B进行不确定的排序       B之前或之后,但未指定。13)序列点的存在       在表达式A和B的求值之间,意味着每次计算值和       在每次值计算和副作用之前先对与A相关的副作用进行排序       与B相关联。(有关序列点的摘要,请参见附件C。)

上面的TL; DR基本上是,如果我们有一个包含副作用的表达式A,则必须在另一个表达式B之前执行,以防{{1} }在B之后排序。

通过此部分可以优化C代码:

  

在抽象机中,所有表达式均按语义指定的方式求值。实际的   如果实现可以推断出未使用其值并且没有产生所需的副作用(包括由调用函数或访问a引起的任何副作用),则实现无需评估表达式的一部分   易失性对象)。

这意味着程序可以按照标准在其他地方强制执行的顺序(计算顺序等)来计算(执行)表达式。但是,如果可以推断出未使用该值,则无需评估(执行)该值。例如,操作A不需要求值0 * x,只需将表达式替换为x

除非访问变量是一个副作用。意味着在0x的情况下,即使结果始终为0,它也必须求值(执行)volatile。不允许优化。>

此外,该标准还谈到了可观察到的行为:

  

符合标准的实现的最低要求是:

     
      
  • 严格根据抽象机的规则评估对易失对象的访问。
      /-/   这是程序的可观察到的行为
  •   

鉴于以上所有情况,在书面C源代码的语义另有说明的情况下,一致的实现(编译器+底层系统)可能无法按未排序的顺序执行0 * x对象的访问。

这意味着在此示例中

volatile

两个赋值表达式必须进行评估,volatile int x; volatile int y; z = x; z = y; 必须必须在z = x;之前进行评估。将这两个操作外包给两个不同序列内核的多处理器实现不符合要求!

难题是,编译器无法对预取缓存和指令流水线之类的东西做很多事情,尤其是在OS之上运行时。因此,编译器将此问题移交给程序员,告诉他们内存障碍现在是程序员的责任。尽管C标准明确指出该问题需要由编译器解决。

尽管编译器不一定要解决问题,所以z = y;充当存储屏障是不可移植的。这已经成为实施质量的问题。

,
  

总结问题,似乎(大量阅读后)   “ volatile”保证类似:该值将被读/写   不仅来自/指向寄存器,而且至少指向内核的L1缓存   读/写在代码中的显示顺序相同。

不,绝对不是。这使得volatile对于MT安全代码几乎毫无用处。

如果这样做的话,那么volatile对于多线程共享的变量将非常有用,因为排序L1缓存中的事件是您在典型CPU(主板上的多核或多CPU)中所需要做的全部工作能够以典型的预期成本(即,对于大多数原子或不满足互斥锁操作而言不是很大的成本)进行正常C / C ++或Java多线程实现的协作。

但是从理论上或实践上,volatile都不会在缓存中提供任何保证的排序(或“内存可见性”)。

(注:以下内容基于对标准文档的合理解释,标准的意图,历史惯例以及对编译器作者期望的深刻理解。这种方法基于历史,实际实践以及对本书的期望和理解真实世界中的真实人,这比解析不知名的,规范化的文档的单词要强得多,也更可靠。

在实践中, volatile确实保证了ptrace-ability,即在任何优化级别上都可以为运行的程序使用调试信息的功能,并且事实上调试信息对于这些volatile对象是有意义的:

  • 您可以使用ptrace(一种类似ptrace的机制)在涉及易失性对象的操作之后的序列点处设置有意义的断点:您可以真正在这些点处断点(请注意,只有当您愿意设置许多断点,因为任何C / C ++语句都可以编译为许多不同的程序集起点和终点,例如在大规模展开的循环中);
  • 在执行线程停止运行时,您可以读取所有易失性对象的值,因为它们具有其规范的表示形式(遵循各自类型的ABI);非易失性局部变量可以具有非典型表示,例如。移位表示:用于索引数组的变量可能会乘以单个对象的大小,以便于索引编制;或者可以用指向数组元素的指针代替它(只要对变量的所有使用都进行了相似的转换)(可以将dx转换为in的整数);
  • 您还可以修改这些对象(只要内存映射允许,因为具有const限定的具有静态生存期的易失对象可能位于只读映射的内存范围内)。

实际上,可变性保证比严格的ptrace解释要多一点:它还保证可变性自动变量在堆栈上有一个地址,因为它们没有分配给寄存器,而寄存器分配会使ptrace的操作更加精细(编译器可以输出调试信息来解释如何将变量分配给寄存器,但是读取和更改寄存器状态比访问内存地址要复杂得多。

请注意,完整的程序调试能力(即至少在顺序点处考虑所有变量的可变性)是由编译器的“零优化”模式提供的,该模式仍会执行诸如算术简化之类的琐碎优化(通常存在不保证在所有模式下均不进行优化)。但是volatile比非优化要强:x-x可以简化为非易失性整数x,但不能简化为volatile对象。

因此 volatile意味着可以保证按原样进行编译,例如系统调用的编译器从源代码到二进制文件/程序集的转换不是重新解释,更改或优化,编译器。请注意,库调用可能不是系统调用。许多正式的系统功能实际上是库函数,提供了一个薄薄的中介层,通常最后取决于内核。 (特别是getpid不需要进入内核,可以很好地读取包含信息的操作系统提供的内存位置。)

易失性交互是与真实机器外部世界的交互,必须遵循“抽象机器”。它们不是程序部分与其他程序部分的内部交互。编译器只能推理知道的内容,即内部程序部分。

用于易失性访问的代码生成应遵循与该内存位置的最自然的交互:这并不奇怪。这意味着某些易失性访问应该是原子性的:如果在架构上读取或写入long表示形式的自然方式是原子性的,则可以预期是读取或写入volatile long中的元素是原子的,例如,因为编译器不应生成笨拙的低效率代码来逐字节访问易失性对象

您应该能够通过了解体系结构来确定这一点。您无需了解任何有关编译器的信息,因为 volatile意味着编译器应该是透明的

但是volatile只不过是强迫针对特定情况进行最优化的最不期望的程序集发出存储操作:volatile语义意味着一般情况下的语义。

一般情况是编译器在没有任何有关构造的信息时执行的操作:f.ex。通过动态分派在左值上调用虚拟函数是一般情况,在编译时确定表达式指定的对象类型是特殊情况后,直接调用重写器。编译器始终对所有构造都具有一般情况处理,并且遵循ABI。

Volatile对同步线程或提供“内存可见性”并没有什么特殊要求: volatile仅在执行或停止的线程内部(即 CPU核心

  • volatile没有说什么内存操作到达主RAM(您可以使用汇编指令或系统调用来设置特定的内存缓存类型,以获得这些保证);
  • volatile不保证何时将内存操作提交到任何级别的缓存(甚至不包括L1)

仅第二点表示volatile在大多数线程间通信问题中没有用;在没有涉及与CPU外部但仍在内存总线上的硬件组件进行通信的任何编程问题中,第一点基本上都是无关紧要的。

从运行线程的核心的角度来看,volatile的特性提供了有保证的行为,这意味着从该线程的执行顺序的角度来看,传递给该线程的异步信号是运行的,请参见源代码顺序。

除非您计划将信号发送到线程(一种非常有用的方法,用于合并有关当前正在运行的线程的信息,并且没有事先约定的停止点),否则volatile不适合您。

,

ISO C 标准,不是,但实际上,我们运行线程的所有机器都有一致的共享内存,因此 volatile 在实践中的工作方式类似于 _Atomic 和 {{ 1}}。

还有一个问题是 memory_order_relaxed 对编译器究竟意味着什么。该标准允许有回旋余地,但在实际编译器中,这意味着加载或存储必须实际发生在 asm 中。不多也不少。 (不能以这种方式工作的编译器无法正确编译使用手动 volatile 的 C11 之前的多线程代码,因此事实上的标准是编译器普遍有用的要求,并且任何人都想要实际使用它们。ISO C 为实现留下了足够多的选择,即 DeathStation 9000 可能符合 ISO C 标准并且几乎完全无法用于实际程序,并且破坏了大多数实际代码库。)

保证 volatile 访问按源顺序发生的要求通常被解释为按该顺序放置 asm,使运行时重新排序受目标机器内存模型的支配。 volatile 访问不是按顺序排序的。其他任何东西,所以简单的操作仍然可以独立于它们进行优化。


When to use volatile with multi threading? 是问题的 C++ 版本。答:基本上从不,使用 stdatomic。我在那里的回答解释了为什么缓存一致性使 volatile 在实践中有用:我知道没有 C 或 C++ 实现volatile 需要显式刷新任何内容以使存储对其他内核可见。它编译为一个普通的 asm 存储指令,对于足够窄到“自然”原子的变量。

(内存屏障只是让这个核心等待,例如,直到存储从存储缓冲区提交到 L1d 缓存,从而在执行以后的加载/存储之前变得全局可见。因此,他们将这个核心的访问排序为一致的共享内存。)

例如,Linux 内核依赖于此,将 shared_var.store(1,std::memory_order_relaxed) 用于线程间可见性,将 volatile 用于对这些访问进行排序的内存屏障,以及原子 RMW 操作。所有可以跨这些内核运行单个 Linux 实例的多核系统都具有一致的共享内存。

有一些罕见的系统具有不一致的共享内存,例如一些集群。但是您不会在不同的一致性域中运行同一进程的线程。 (或在其上运行操​​作系统的单个实例)。相反,共享内存的映射方式必须与正常的回写缓存不同,否则您必须进行显式刷新。

本文链接:https://www.f2er.com/3166064.html

大家都在问