第二个比第一个更好。答案很简单:第二个尽量减少错误共享
现代 CPU 不会不将字节逐个加载到缓存中。它在称为缓存行的批处理中读取一次。当两个线程尝试修改同一缓存行上的不同变量时,必须在修改缓存后重新加载缓存。
什么时候会发生这种情况?
基本上,内存中的附近元素将位于同一缓存行中。因此,数组中的相邻元素将位于同一缓存行中,因为数组只是一个内存块。foo1 和 foo2 也可能在同一缓存行中,因为它们在同一类中定义得很近。
class Foo {
private int foo1;
private int foo2;
}
虚假分享有多糟糕?
我参考了处理器缓存效果库中的示例 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和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];
源
您可能想了解有关所有魔术技巧的更多信息。看看作者的一篇文章:剖析破坏者:为什么它这么快