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


12

看着经过一大堆 其他 问题 他们的 答案,我得到的印象是有什么在C“挥发性”关键字表示正好没有广泛的协议。

即使标准本身似乎也不够清晰,每个人都无法理解其含义

除其他问题外:

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

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

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

看来,“用1”不移植的代码,其目标包括多核系统发生。

USE 2
与“ use 1”没什么不同,它是可由中断处理程序(可以控制灯光或从键存储信息)随时读取或写入的内存。但是为此已经存在一个问题,即取决于系统,中断处理程序可能会在 具有自己的内存缓存的不同内核运行,并且“ volatile”不能保证所有系统上的缓存一致性。

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

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

那么“使用3”也许根本不存在

因此,我的问题是:

在多核系统的可移植C代码中,“ volatile”是否完全可以保证?


编辑-更新

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

因此,更精确的问题变成了:
除了(1)允许单线程程序从其信号处理程序接收信息之外,还是(2)允许setjmp,“ volatile”是否可以保证多核系统的可移植C代码中的任何内容?代码以查看在setjmp和longjmp之间修改的变量?

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

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


3
便携式C中存在信号;信号处理程序更新的全局变量呢?这将需要volatile通知程序它可能异步更改。
Nate Eldredge

2
@NateEldredge Global虽然本身就很不稳定,但还不够好。它也必须是原子的。
尤金(Eugene Sh)。

@EugeneSh .:是的,当然。但是眼前的问题是关于volatile具体的,我认为这是必要的。
Nate Eldredge

与L1缓存进行协调并不能保证与其他线程的协调。 ”“与L1缓存进行协调”不足以与其他线程进行通信吗?
curiousguy19年

1
也许有关,C ++弃用挥发性建议,很多人都在这里提高的担忧,也许它的结果的建议地址将影响到C委员会
MM

Answers:


1

总结一下问题,似乎(经过大量阅读)“ volatile”保证了类似的结果:该值将不但从/向寄存器,而且至少向内核的L1缓存中读/写,其顺序与读/写出现在代码中

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

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

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

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

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

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

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

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

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

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

易失性访问的代码生成应遵循与该内存位置最自然的交互:这应该不足为奇。这意味着某些易失性访问应该是原子的:如果在long体系结构上自然地读写a的表示形式是原子的,则可以预期a的读取或写入volatile long将是原子的,因为编译器不应生成效率低下的愚蠢代码,例如逐字节访问易失性对象

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

但是volatile只不过是强制释放针对特定情况最不优化的预期程序集来执行存储操作:volatile语义表示一般情况下的语义。

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

Volatile对同步线程或提供“内存可见性”并没有什么特别的事情:volatile仅提供从执行或停止的线程内部(即CPU内核内部)看到的抽象级别的保证

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

仅第二点意味着volatile在大多数线程间通信问题中没有用;第一点本质上与任何编程问题无关,这些编程问题不涉及与CPU外部但仍在内存总线上的硬件组件进行通信。

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

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


6

我不是专家,但是cppreference.com上似乎有一些非常不错的信息volatile。这是要点:

通过volatile限定类型的左值表达式进行的每次访问(读和写)都被认为是可观察到的副作用,用于优化目的,并且严格按照抽象机的规则进行评估(也就是说,所有写操作均在以下位置完成)在下一个序列点之前的某个时间)。这意味着在单个执行线程中,相对于由序列点与易失性访问分隔开的另一个可见副作用,易失性访问无法优化或重新排序。

它还提供了一些用途:

挥发物的用途

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

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

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

4)此外,易失性变量可用于禁用某些形式的优化,例如,禁用死存储消除或微基准的恒定折叠。

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

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


2
特别是(2)和(3)与可移植代码有关。
Nate Eldredge

2
@TED尽管有域名,但该链接指向的是有关C而不是C ++的信息
David Brown

@NateEldredge很少longjmp在C ++代码中使用。
curiousguy19年

@DavidBrown C和C ++具有可观察的SE的相同定义,并且基本上具有相同的线程基元。
curiousguy19年

4

首先,从历史上看,人们对volatile获取和类似含义的不同理解存在各种障碍。查看此研究:挥发物编译错误,以及如何处理

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

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

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

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

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

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

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

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

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

除非访问变量是副作用。这意味着如果情况xvolatile,即使结果始终为0 ,它也必须求值(执行)0 * x。不允许进行优化。

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

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

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

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

这意味着在这个例子中

volatile int x;
volatile int y;
z = x;
z = y;

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

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

但是,编译器并不一定要解决问题,因此volatile,作为存储屏障是不可移植的。这已经成为实施质量的问题。


@curiousguy没关系。
隆丁

@curiousguy没关系,只要它是带有或不带有限定符的某种整数类型即可。
隆丁

如果它是一个简单的非易失性整数,为什么z要真正执行冗余写入?(如z = x; z = y;)该值将在下一条语句中删除。
curiousguy19年

@curiousguy因为无论如何都必须以指定的顺序执行对volatile变量的读取
隆丁

z真的分配两次吗?您如何知道“读取已执行”?
curiousguy19年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.