内存屏障的开销

10

我目前正在编写C++代码,并在我的代码中使用了很多内存屏障/栅栏。我知道,MB会告诉编译器和硬件不要重排它周围的写入/读取操作。但是我不知道这个操作对处理器在运行时有多复杂。

我的问题是:这种屏障的运行时开销是多少?我在谷歌上没有找到任何有用的答案......这种开销是否可以忽略不计?还是频繁使用MB会导致严重的性能问题?

最好的问候。

2个回答

3
与算术和“普通”指令相比,我理解这些指令的代价非常高,但没有数据支持这个说法。我喜欢jalf对指令效果的描述,并想补充一点。
通常有几种不同类型的障碍,因此了解它们之间的区别可能是有帮助的。例如,在互斥实现中,在清除锁定字(ppc上的lwsync或ia64上的st4.rel)之前需要像jalf提到的那样的障碍。所有读取和写入必须完成,只能执行在管道中后面没有内存访问且不依赖正在进行的内存操作的指令。
另一种类型的障碍是在获取锁定时使用的障碍(例如,在ia64上的isync或instr.acq)。这会对未来的指令产生影响,因此,如果已经预取了非相关负载,它必须被丢弃。
没有采用获得障碍(借用ia64术语),如果在检查标志位之前somethingElse先进入寄存器,则您的程序可能会产生意外结果。
第三种类型的障碍通常很少使用,需要强制执行存储加载顺序。用于此类顺序强制指令的示例包括ppc上的sync(重量级同步)、ia64上的MF、sparc上的membar #storeload(即使对于TSO也是必需的)。
使用类似ia64的伪代码进行说明,假设有以下内容,而没有中间的mf:
st4.rel ld4.acq
在其中没有保证加载会在存储之后进行。您知道在st4.rel之前的负载和存储已经完成,但那个负载或其他未来的负载(如果非相关)可能会潜入,因为没有任何防止这种情况发生的东西。
由于互斥实现很可能只在其实现中使用获取和释放障碍,因此我希望观察到的效果是,在释放锁定后的内存访问有时可能会在“仍处于临界区域”状态下发生。

十年前的回答非常有趣!我认为我今天在使用ConcurrentQueue和AutoResetEvent时发现了这种效果。在EnQueue并调用event.Set之后,很少(每12-24小时)会出现另一端从WaitOne()返回,只发现TryDequeue()返回false的情况。我发现如果多次输出queue.Count属性,我可以看到它神奇地从0变为1。 - Brain2000
@Brain2000 这不应该发生。使用 Set 的释放语义,该线程上发生的所有事情都应该在 Set 之前“提交”。同样,使用 Wait 的获取语义,Wait 后面的所有读取都应该“检查”新鲜数据。你的代码或者你正在使用的库中可能存在错误。 - relatively_random
@relatively_random 这是我自己的代码。我改用了BlockingCollection后,所有问题都消失了。我相信ConcurrentQueue或AutoResetEvent存在一种罕见的竞态条件。我的代码是这里的其中一个答案:https://dev59.com/JnM_5IYBdhLWcg3wNwQv - Brain2000

1

试着思考指令的作用。它并不会让CPU在逻辑上执行任何复杂的操作,但它会强制等待直到所有读写操作都已提交到主内存中。因此,成本实际上取决于访问主内存的成本(以及未完成的读写操作数量)。

访问主内存通常非常昂贵(10-200个时钟周期),但从某种意义上说,即使没有屏障,这项工作也必须完成,只是可以通过同时执行其他指令来隐藏它,这样你就不会感受到成本如此之高。

它还限制了CPU(和编译器)重新安排指令的能力,因此可能会有间接成本,因为附近的指令无法交错执行,否则可能会产生更有效的执行计划。


6
这个回答不正确。句子“所有读写已提交到主内存”是错误的。主内存不会受到内存栅栏的影响。至少,在具有高速缓存一致性的情况下,为什么要完全访问主内存?而且,访问主内存的周期时间远远超出了预期。 - Timoteo
@Timoteo 主存受内存栅栏影响,但读写的影响取决于所使用的栅栏类型。 - grayxu

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