multithreading循环的效率

问候贵族社区,

我想要以下循环:

for(i = 0; i < MAX; i++) A[i] = B[i] + C[i]; 

这将在使用线程的共享内存四核计算机上并行运行。 下面的两个替代方案正在考虑由这些线程执行的代码,其中tid是线程的id:0,1,2或3。

(为简单起见,假设MAX是4的倍数)

选项1:

 for(i = tid; i < MAX; i += 4) A[i] = B[i] + C[i]; 

选项2:

 for(i = tid*(MAX/4); i < (tid+1)*(MAX/4); i++) A[i] = B[i] + C[i]; 

我的问题是,是否有一个比另一个更有效,为什么?

第二个比第一个好。 简单回答:第二个最小化错误共享

现代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中的元素之间创建填充:

 abstract class RingBufferPad { protected long p1, p2, p3, p4, p5, p6, p7; } 

此外,当它为缓冲区分配内存时,它会在前面和后面创建填充,这样它就不会受到相邻内存空间中数据的影响:

 this.entries = new Object[sequencer.getBufferSize() + 2 * BUFFER_PAD]; 

资源

您可能想要了解有关所有魔术技巧的更多信息。 看看作者的post之一: 解剖破坏者:为什么它如此之快