第二个比第一个好。简单回答:第二个最小化false sharing
现代 CPU 不会将字节一个一个地加载到缓存中。它分批读取一次,称为缓存行。当两个线程试图修改同一缓存行上的不同变量时,一个必须在一个修改后重新加载缓存。
什么时候会发生这种情况?
基本上,内存中附近的元素将位于同一缓存行中。因此,数组中的相邻元素将位于同一缓存行中,因为数组只是一块内存。并且 foo1 和 foo2 也可能在同一个缓存行中,因为它们在同一个类中定义得很近。
class Foo {
private int foo1;
private int foo2;
}
虚假分享有多严重?
我参考Gallery of Processor Cache Effects中的示例6
private static int[] s_counter = new int[1024];
private void UpdateCounter(int position)
{
for (int j = 0; j < 100000000; j++)
{
s_counter[position] = s_counter[position] + 3;
}
}
在我的四核机器上,如果我从四个不同的线程调用带有参数 0、1、2、3 的 UpdateCounter,则需要 4.3 秒才能完成所有线程。
另一方面,如果我使用参数 16,32,48,64 调用 UpdateCounter,则操作将在 0.28 秒内完成!
如何检测虚假分享?
Linux Perf 可用于检测缓存未命中,从而帮助您分析此类问题。
参考CPU Cache Effects and Linux Perf的分析,使用perf从上面几乎相同的代码示例中找出L1缓存未命中:
Performance counter stats for './cache_line_test 0 1 2 3':
10,055,747 L1-dcache-load-misses # 1.54% of all L1-dcache hits [51.24%]
Performance counter stats for './cache_line_test 16 32 48 64':
36,992 L1-dcache-load-misses # 0.01% of all L1-dcache hits [50.51%]
这里显示,在没有虚假共享的情况下,总 L1 缓存命中将从 10,055,747 下降到 36,992。而且性能开销不在这里,是在加载L2、L3缓存、假共享后加载内存这一系列。
行业有什么好的做法吗?
LMAX Disruptor 是一个高性能的线程间消息传递库,它是Apache Storm 中工作线程内通信的默认消息传递系统
底层数据结构是一个简单的环形缓冲区。但为了让它更快,它使用了很多技巧来减少虚假分享。
例如,它定义了超类RingBufferPad在RingBuffer中的元素之间创建pad:
abstract class RingBufferPad
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
另外,当它为缓冲区分配内存时,它会在前后创建 pad,这样它就不会受到相邻内存空间中的数据的影响:
this.entries = new Object[sequencer.getBufferSize() + 2 * BUFFER_PAD];
source
您可能想了解有关所有魔术技巧的更多信息。看看作者的一个帖子:Dissecting the Disruptor: Why it's so fast