False sharing 和 128 字节对齐/填充

30

在研究无锁/无等待算法时,我偶然发现了伪共享问题。进一步挖掘后,我找到了Folly的源代码(Facebook的C++库),特别是这个头文件FOLLY_ALIGN_TO_AVOID_FALSE_SHARING宏的定义(目前在第130行)。最让我惊讶的是其值:128(而不是64)...

/// An attribute that will cause a variable or field to be aligned so that
/// it doesn't have false sharing with anything at a smaller memory address.
#define FOLLY_ALIGN_TO_AVOID_FALSE_SHARING __attribute__((__aligned__(128)))

据我所知,现代CPU的缓存块长度为64字节。事实上,到目前为止我找到的所有资源,包括英特尔公司的这篇文章,都谈到了64字节对齐和填充以帮助解决虚假共享问题。
然而,Facebook的开发人员在需要时将类成员对齐和填充到128字节。然后我发现,在FOLLY_ALIGN_TO_AVOID_FALSE_SHARING的定义上面有一个解释的开头:
enum {
    /// Memory locations on the same cache line are subject to false
    /// sharing, which is very bad for performance.  Microbenchmarks
    /// indicate that pairs of cache lines also see interference under
    /// heavy use of atomic operations (observed for atomic increment on
    /// Sandy Bridge).  See FOLLY_ALIGN_TO_AVOID_FALSE_SHARING
    kFalseSharingRange = 128
};

虽然它为我提供了更多细节,但我仍然觉得需要一些见解。我很好奇在原子操作大量使用的情况下,如何同步连续高速缓存行或对它们进行任何RMW操作可能会相互干扰有人能告诉我这怎么可能发生吗?


我曾经很喜欢阅读Herb Sutter在Dr Dobbs上的解释:http://www.drdobbs.com/parallel/eliminate-false-sharing/217500206 - Robinson
1
这里的一个重要概念是“缓存关联性”。 - Ben Voigt
13
空间预取器旨在将一对缓存行保留在L2缓存中。 - Hans Passant
1
@Hans:我会接受那个作为答案(不过我在第2.2.5.4章节找到了)。 - polyvertex
2
你的可能更加更新。你可以自己编写答案。 - Hans Passant
1
@Ben:有用的提示。谢谢! - polyvertex
3个回答

6
如Hans在评论中指出的那样,关于这个问题的一些信息可以在"Intel® 64和IA-32架构优化参考手册"中找到,在第3.7.3节“用于二级缓存的硬件预取”中,介绍了Intel Core微架构中的以下内容:
“Streamer - 从内存加载数据或指令到二级缓存。要使用streamer,请将数据或指令组织成128字节的块,对齐在128字节上。当内存中存在该块中两个缓存行之一的第一个访问时,触发streamer预取该对行。”

1
在像Sandybridge系列这样的现代微体系结构中,实际上是L2空间预取器喜欢完成对齐的缓存行对;Streamer则是单独的。(Intel Core已经过时,比如2007年左右的Core2Duo Conroe和Penryn。) - Peter Cordes
字节对齐和伪共享会导致x86-64性能差异在一些未知的最新x86微架构上进行了基准测试。此外,有关为什么128在现代x86上是有意义的更多细节,请参见理解std::hardware_destructive_interference_size和std::hardware_constructive_interference_size - Peter Cordes

0

看起来,虽然英特尔使用64字节的缓存行,但还有其他各种架构使用128字节的缓存行...例如:

http://goo.gl/8L6cUl

电源系统使用128字节长度的高速缓存行。与英特尔处理器(64字节高速缓存行)相比,这些更大的高速缓存行有...
我在互联网上发现散落的笔记中,其他架构,甚至是旧的架构,也使用同样的方法:

http://goo.gl/iNAZlX

SGI MIPS R10000处理器在Origin计算机中。 该处理器的缓存行大小为128字节。
因此,Facebook程序员可能想要保险起见,不想基于处理器架构拥有大量的#define/#if集合,因为存在某些新的Intel处理器具有128字节的缓存行而没有人记得更正代码的风险。

4
然而,在Sandy Bridge架构上对原子增量进行操作时,一对缓存行也会发生干扰...实际上,他们似乎更新了"FOLLY_ALIGN_TO_AVOID_FALSE_SHARING"的值,因为针对那个特定架构进行了一系列测试。即:根据这些结果,64字节的对齐和填充是不够的。 - polyvertex

-1
无论您是否使用原子操作,缓存都具有“缓存行”,这是缓存操作的最小单位。它的范围从32到128字节不等,取决于处理器型号。虚假共享是指在同一缓存行中的元素在不同线程(在不同处理器上运行)之间“共享”。当发生这种情况时,更新“其值”的一个处理器将强制所有其他处理器“摆脱”该数据的副本。在原子操作的情况下会更糟,因为为执行任何原子操作,执行该操作的处理器将需要确保所有其他处理器在更新值之前已经“摆脱了它们的副本”,以确保没有其他处理器在更新值之前使用“旧”值,而这需要大量的缓存维护消息通过系统传播,并且处理器重新加载它们先前在缓存中拥有的值。

从性能的角度来看,如果您有一个变量只会被一个线程使用,将它们分离到自己的缓存行中(在原始帖子的示例中,假定这是128字节),通过将数据对齐到该值来实现 - 这意味着每个数据块都在一个偶数缓存行边界上开始,并且没有其他处理器将“共享”相同的数据(除非您真正共享线程之间的数据 - 在这种情况下,您必须进行相关的缓存维护以确保正确更新处理器之间的数据)。

[1] 或者在具有多个核心的现代 CPU 中的处理器核心。为简单起见,我使用术语“处理器”或“处理器”来对应于真正的处理器插座或一个插座内的处理器核心。对于这个讨论来说,区别几乎无关紧要。


4
我理解“false-sharing”的概念。我的问题是关于对齐和填充所使用的值(128字节)与强制他们选择此值的架构之间的关系(例如:据我所知,Sandy Bridge具有64字节的缓存行)。 - polyvertex
我认为一些旧的英特尔架构也使用了128字节的缓存行。也许他们不仅拥有在最近几年生产的处理器? - Mats Petersson
4
你的回答非常详细,我感谢你。但请仔细阅读我的问题,它可能有些冗长并且不太清楚:“微基准测试表明,缓存行对也会出现干扰……(在 Sandy Bridge 上观察到)。”这里涉及的架构问题是毫无疑问的。他们更改了对齐方式为 128 字节,因为在 Sandy Bridge 上的结果很差。我想要理解的是为什么和如何通过更大的填充可以在这种特定情况下减少错误共享。对于你来说,@Hans 在评论中提到的内容可能也很有帮助。 - polyvertex
很不幸,我对最新的英特尔处理器并不是非常熟悉。大约12-15年前,在AMD做基准测试时,我非常了解各个型号之间的差异。但现在我不太了解了,因为我家里的所有机器都有AMD处理器(仍然忠于我10年前工作的品牌),而我在工作中仅使用ARM处理器已经8年左右了。[嗯,我的工作机器本身有一个英特尔处理器或其他型号,但我并没有真正关注它的行为方式] - Mats Petersson

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