您给出的两个代码片段并不等效,因为第一个代码片段会不断重复n
大于 4 的相同元素。处理此类数组的正确方法是确保它sizeof(arr[0]) * n / #cores
是缓存行大小的倍数。现代 x86 CPU 的缓存线大小为 64 字节。如果arr
是整数或单精度浮点数组,则sizeof(arr[0]) == 4
单个缓存行包含 16 个元素。对于大小为两倍的数据类型,单个高速缓存行可容纳 8 个元素。最佳循环调度块大小在前一种情况下是 16 的倍数,在后一种情况下是 8 的倍数。
在处理静态调度的循环时,我们会尝试最大化块大小,以减少每个线程运行的循环数量。例如,如果有 4 个线程,n
为 64 并且块大小设置为 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
这远非最佳,因为每个线程必须运行循环两次。更好的解决方案是将块大小设置为 16(8 的倍数):
thread iterations (from-to)
------ --------------------
0 0-15
1 16-31
2 32-47
3 48-63
请注意,静态调度循环的默认块大小是#iterations / #threads
.
有时必须并行处理不能在非重叠高速缓存行之间传播的数据。例如arr[]
,可能只是一个包含 4 个元素的数组,它们都适合单个高速缓存行。在这种情况下,应该在数组元素之间插入填充,以确保由不同线程处理的数据位于不同的缓存行中。例如:
int arr[4];
#pragma omp parallel for
for (int i = 0; i < 4; i++)
arr[i]++;
int arr[4]
导致以下内存布局:
|<-------- a single cache line ---------->|
| arr[0] | arr[1] | arr[2] | arr[3] | ... |
如果核心 0 更新arr[0]
和核心 1 更新arr[1]
,那么缓存线将在两个核心之间不断反弹 - 错误共享和性能不佳。arr[0]
因此,必须在arr[1]
大小CLS - sizeof(arr[0])
字节或CLS/sizeof(arr[0]) - 1
数组元素之间插入填充,其中CLS
是高速缓存行的大小(以字节为单位)。和这CLS == 64
组成sizeof(arr[0]) == 4
了 15 个填充元素。生成的布局将是:
|<----- 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]++;
以某种方式简化代码的另一种选择是将每个数据元素包装在一个结构中并将填充放在结构中:
// cache line size in number of bytes
#define CLS (64)
typedef struct _item
{
int data;
int padding[CLS/sizeof(int)-1];
} item;
item arr[4];
#pragma omp parallel for
for (int i = 0; i < 4; i++)
arr[i].data++;
无论您使用哪种方法,请记住此类代码变得不可移植,因为各种体系结构具有不同的缓存行大小。