使用OpenMP块来破坏缓存

时间:2013-08-20 18:06:50

标签: c parallel-processing load openmp load-balancing

我一直在努力提高我的OpenMP解决方案的性能,这通常需要处理数组上的嵌套循环。虽然我已经设法从串行实现的59秒(在老化的双核Intel T6600上)将其降低到37,但我担心缓存同步会引起很多CPU注意(当CPU应该解决我的问题时! )。我一直在努力设置探查器,所以我没有证实这个说法,但我的问题无论如何。根据负载平衡的this lecture

  

CPU正在忙于争夺程序中唯一使用的缓存行,而不是工作。您可以使用一种非常奇怪的技术来解决这个问题:将CPU数据在内存中移动多于一个缓存行。例如,这里我们将每个线程访问的整数移动20个单位。

然后继续提供相关的源代码(应该在四核上运行,因此%4

#pragma omp parallel for schedule(static,1)
    for (unsigned int i=0;i<n;i++) {
    arr[(i%4)*20]++;
}

那就是说,我对“大块”有什么直觉,但上面的实现似乎完全忽略了它,让我相信我的直觉是错误的。

我的问题是:设置一个相当大的块值是否会将数据向下移动到缓存行? IE浏览器。上面的代码不会等同于

#pragma omp parallel for schedule(static, 20)
    for (unsigned int i=0;i<n;i++) {
    arr[i]++;
}

1 个答案:

答案 0 :(得分:6)

您提供的两个代码片段都不相同,因为第一个代码片段将重复大于4的n的相同元素。处理此类数组的正确方法是确保{{1} }是缓存行大小的倍数。现代x86 CPU的缓存行大小为64字节。如果sizeof(arr[0]) * n / #cores是整数或单精度浮点数组,则arr和单个缓存行包含16个元素。对于大小加倍的数据类型,单个缓存行包含8个元素。最好的循环计划块大小在前一种情况下是16的倍数,在后一种情况下是8的倍数。

在处理静态调度循环时,会尝试最大化块大小,以减少每个线程运行的循环次数。例如,如果有4个线程,sizeof(arr[0]) == 4为64,并且块大小设置为8,则将使用以下计划:

n

这远非最优,因为每个线程必须运行循环两次。一个更好的解决方案是将块大小设置为16(这是8的倍数):

thread    iterations (from-to)
------    --------------------
  0          0- 7, 32-39
  1          8-15, 40-47
  2         16-23, 48-53
  3         24-31, 54-63

请注意,静态调度循环的默认块大小为thread iterations (from-to) ------ -------------------- 0 0-15 1 16-31 2 32-47 3 48-63

有时必须处理并行数据,这些数据无法在非重叠的缓存行中传播。例如,#iterations / #threads可能只是一个包含4个元素的数组,这些元素都适合单个缓存行。在这种情况下,应该在数组元素之间插入填充,以确保不同线程处理的数据位于不同的缓存行中。例如:

arr[]

int arr[4]; #pragma omp parallel for for (int i = 0; i < 4; i++) arr[i]++; 会产生以下内存布局:

int arr[4]

如果核心0更新|<-------- a single cache line ---------->| | arr[0] | arr[1] | arr[2] | arr[3] | ... | 而核心1更新arr[0],那么缓存行将在两个核心之间不断反弹 - 错误共享和糟糕的性能。因此,必须在arr[1]arr[0]大小arr[1]字节或CLS - sizeof(arr[0])数组元素之间插入填充,其中CLS/sizeof(arr[0]) - 1是高速缓存行的大小(以字节为单位) 。使用CLSCLS == 64,这会产生15个填充元素。结果布局为:

sizeof(arr[0]) == 4

应该修改剪切的代码:

|<----- one cache line ------>|<--- another cache line ---->|<-- yet another ...
| arr[0] | 15 unused elements | arr[1] | 15 unused elements | arr[2] | ...

以某种方式简化代码的另一个选择是将每个数据元素包装在一个结构中并将填充放在结构中:

// cache line size in number of int elements
#define CLS    (64/sizeof(int))

int arr[4*CLS];

#pragma omp parallel for
for (int i = 0; i < 4; i++)
   arr[i*CLS]++;

无论您使用哪种方法,请记住此类代码变得不可移植,因为各种架构具有不同的缓存行大小。