是否可以重新订购对挥发物的访问?

时间:2013-02-09 06:37:16

标签: c++ c++11 volatile

考虑以下对volatile内存的写入顺序,我从David Chisnall's article at InformIT,“了解C11和C ++ 11 Atomics”中获取:

volatile int a = 1;
volatile int b = 2;
             a = 3;

我对C ++ 98的理解是,根据C ++ 98 1.9,这些操作无法重新排序:

  

贴合   实现需要模拟(仅)抽象机器的可观察行为   解释如下   ...   抽象机器的可观察行为是它对易失性数据的读写顺序   调用库I / O函数

Chisnall说,对订单保存的约束仅适用于单个变量,写出符合要求的实现可以生成执行此操作的代码:

a = 1;
a = 3;
b = 2;

或者这个:

b = 2;
a = 1;
a = 3;

C ++ 11重复了C ++ 98的措辞

  

贴合   如下所述,实现需要模拟(仅)抽象机器的可观察行为   下方。

但是关于volatile s(1.9 / 8):

  

严格按照抽象机的规则评估对volatile对象的访问。

1.9 / 12表示访问volatile glvalue(包括上面的变量abc)是副作用,并且1.9 / 14表示一个完整表达式(例如,一个语句)中的副作用必须先于同一个线程中后一个完整表达式的副作用。这使我得出结论,Chisnall显示的两个重新排序是无效的,因为它们不符合抽象机器所规定的排序。

我是否忽视了某些事情,或者Chisnall是否错了?

(请注意,这不是一个线程问题。问题是是否允许编译器在单个线程中重新排序对不同volatile变量的访问。)

6 个答案:

答案 0 :(得分:5)

IMO Chisnalls的解释(由你提出)显然是错误的。更简单的情况是C ++ 98。 sequence of reads and writes to volatile data需要保留,并且适用于任何易失数据的有序读写序列,而不是单个变量。

如果你考虑volatile的最初动机:内存映射I / O,这就很明显了。在mmio中,通常在不同的存储器位置有几个相关的寄存器,I / O设备的协议需要对其寄存器集进行特定的读写操作 - 寄存器之间的顺序很重要。

C ++ 11的措辞避免讨论绝对sequence of reads and writes,因为在多线程环境中,跨线程没有一个明确定义的此类事件序列 - 这不是问题,如果这些访问转到独立的内存位置。但我相信,对于任何具有良好定义顺序的易失性数据访问序列,规则保持与C ++ 98相同 - 无论在该序列中访问多少个不同位置,都必须保留顺序。

这是一个完全独立的问题,需要实现什么。如何(甚至 if )可以从程序外部观察到易失性数据访问,以及程序的访问顺序如何映射到外部可观察事件是未指定的。实施应该可以给你一个合理的解释和合理的保证,但合理的取决于上下文。

C ++ 11标准为非同步的易失性访问之间的数据竞争留下了空间,因此没有任何东西需要通过完整的内存栅栏或类似的结构来围绕它们。如果存在真正用作外部接口的存储器部分 - 用于存储器映射的I / O或DMA - 则实现可能是合理的,可以保证对这些部件的易失性访问如何暴露给消耗设备。 / p>

可以从标准中推断出一种保证(参见[into.execution]):类型volatile std::sigatomic_t的值必须具有与写入顺序相符的值,即使在信号处理程序中 - 至少在单线程程序。

答案 1 :(得分:2)

你是对的,他错了。对于不同的volatile变量的访问不能由编译器重新排序,只要它们出现在单独的完整表达式中,即由C ++ 98称为序列点分隔,或者在C ++ 11术语中,一个访问在另一个之前排序。

Chisnall似乎试图解释为什么volatile对于编写线程安全代码毫无用处,通过显示依赖于volatile的简单互斥实现,这将被编译器重新排序打破。他是正确的volatile对线程安全没用,但不是因为他给出的原因。这不是因为编译器可能会重新排序对volatile个对象的访问,而是因为CPU可能会重新排序它们。原子操作和内存障碍阻止编译器 CPU根据线程安全需要重新排序屏障。

请参阅Sutter内容丰富的volatile vs volatile文章中表1右下方的单元格。

答案 2 :(得分:0)

看起来可能会发生。

此页面上有一个讨论:

http://gcc.gnu.org/ml/gcc/2003-11/msg01419.html

答案 3 :(得分:0)

这取决于你的编译器。例如,从Visual Studio 2005开始的MSVC ++保证* volatiles不会被重新排序(实际上,微软所做的是放弃并假设程序员将永远滥用volatile - MSVC ++现在围绕{{1}的某些用法添加了内存屏障}})。其他版本和其他编译器可能没有这样的保证。

长话短说:不要赌它。正确设计代码,不要滥用volatile。根据需要使用内存屏障或完整的互斥锁。 C ++ 11的volatile类型将有所帮助。

答案 4 :(得分:0)

目前,我将假设你的a=3只是复制和粘贴的错误,你真的认为它们是c=3

这里真正的问题是评估之间的区别之一,以及其他处理器如何看到事物。标准描述了评估顺序。从这个角度来看,您完全正确 - 根据该订单分配abc,必须按顺序评估作业。

可能对应于这些值对其他处理器可见的顺序。在典型(当前)CPU上,该评估仅将值写入缓存。硬件可以从那里重新排序,因此(例如)写入主存储器的顺序完全不同。同样,如果另一个处理器尝试使用这些值,它可能会看到它们以不同的顺序发生变化。

是的,这是完全允许的 - CPU仍然按照标准规定的顺序评估分配,因此满足要求。标准根本不对评估后发生的事情提出任何要求,这就是这里发生的事情。

我应该补充一点:虽然在某些硬件上已经足够了。例如,x86使用缓存侦听,因此如果另一个处理器尝试读取由一个处理器更新的值(但仍然只在缓存中),那么具有当前值的处理器将保留另一个处理器的读取处理器直到可以写出当前值,这样其他处理器才能看到当前值。

但并非所有硬件都是如此。虽然保持严格的模型可以保持简单,但是在确保一致性的额外硬件方面以及在拥有大量处理器时的简单速度方面,它也相当昂贵。

编辑:如果我们暂时忽略线程,那么问题就会变得更简单一些 - 但并不多。根据C ++ 11,§1.9/ 12:

  

当对库I / O函数的调用返回或对volatile对象的访问进行评估时,即使调用所隐含的某些外部操作(例如I / O本身)或者易失性访问可能还没有完成。

因此,对易失性对象的访问必须按顺序启动,但不一定按顺序完成。不幸的是,它通常是外部可见的完成。因此,我们几乎回到了通常的as-if规则:编译器可以根据需要重新排列事物,只要它不会产生外部可见的变化。

答案 5 :(得分:-2)

C ++ 98并未说明指令无法重新排序。

  

抽象机器的可观察行为是它对易失性数据的读写顺序以及对库I / O函数的调用

这表示它是读取和写入的实际序列,而不是生成它们的指令。任何说明指令必须以程序顺序反映读写的论据同样可以说,对RAM本身的读写必须按程序顺序进行,显然这是对要求的荒谬解释。

简单地说,这并不意味着什么。没有“一个正确的地方”来观察读写的顺序(RAM总线?CPU总线?在L1和L2缓存之间?从另一个线程?从另一个核心?),所以这个要求基本上没有意义。 / p>

在对线程的任何引用之前,C ++的版本显然没有指定从另一个线程看到的volatile变量的行为。和C ++ 11(明智地,IMO)didn't change this但是通过明确定义的线程间语义引入了合理的原子操作。

对于内存映射硬件,这始终是特定于平台的。 C ++标准甚至没有假装解决如何正确完成的问题。例如,平台可能只有一部分内存操作在该上下文中是合法的,比如绕过可以重新排序的写发布缓冲区,并且C ++标准当然不会强制编译器发出正确的指令那个特定的硬件设备 - 怎么可能呢?

更新:我看到一些downvotes因为人们不喜欢这个道理。不幸的是,这是事实。

如果C ++标准禁止编译器重新排序对不同易失性的访问,理论上这种访问的顺序是程序可观察行为的一部分,那么它还要求编译器发出禁止CPU这样做的代码。该标准没有区分编译器的作用以及编译器生成的代码使CPU执行的操作。

由于没有人认为标准要求编译器发出指令以防止CPU重新排序对volatile变量的访问,而现代编译器不这样做,所以没有人应该相信C ++标准禁止编译器重新排序对不同volatile的访问。