我编写了下面这个简短的C++程序,以重现Herb Sutter所描述的伪共享效应:
假设我们要执行总数为WORKLOAD的整数操作,并希望它们平均分配到一定数量(PARALLEL)的线程中。为了进行此测试,每个线程将从整数数组中递增其自己专用的变量,因此该过程可以理想地并行化。
每个线程将在单独的缓存行上工作(假设缓存行长度为64字节)。因此,速度提升将呈线性增长,直到PARALLEL > # cores。然而,如果PADDING设置在小于16的任何值,则应该会遇到严重的争用情况,因为至少两个线程现在可能会在同一缓存行上运行,但是这里由内置的硬件互斥锁来保护。我们预计,在这种情况下,我们的加速比不仅会是亚线性的,甚至始终 <1,因为存在隐形的锁定车队。
现在,我的第一个尝试几乎满足了这些期望,但需要避免伪共享的最小PADDING值约为8而不是16。我困惑了大约半个小时,直到我得出了显而易见的结论,即没有保证我的数组在主存储器中准确地对齐到缓存行的开头。实际对齐可能会因包括数组大小在内的许多条件而有所变化。
在这个例子中,我们当然不需要特别对齐数组,因为我们可以将PADDING设置为16,一切都能正常工作。但是有些情况下,某个结构体是否对齐到缓存行可能会产生影响。因此,我添加了一些代码来获取有关我的数组实际对齐方式的信息。
假设我们要执行总数为WORKLOAD的整数操作,并希望它们平均分配到一定数量(PARALLEL)的线程中。为了进行此测试,每个线程将从整数数组中递增其自己专用的变量,因此该过程可以理想地并行化。
void thread_func(int* ptr)
{
for (unsigned i = 0; i < WORKLOAD / PARALLEL; ++i)
{
(*ptr)++;
}
}
int main()
{
int arr[PARALLEL * PADDING];
thread threads[PARALLEL];
for (unsigned i = 0; i < PARALLEL; ++i)
{
threads[i] = thread(thread_func, &(arr[i * PADDING]));
}
for (auto& th : threads)
{
th.join();
}
return 0;
}
我认为这个想法很容易理解。如果你设置
#define PADDING 16
每个线程将在单独的缓存行上工作(假设缓存行长度为64字节)。因此,速度提升将呈线性增长,直到PARALLEL > # cores。然而,如果PADDING设置在小于16的任何值,则应该会遇到严重的争用情况,因为至少两个线程现在可能会在同一缓存行上运行,但是这里由内置的硬件互斥锁来保护。我们预计,在这种情况下,我们的加速比不仅会是亚线性的,甚至始终 <1,因为存在隐形的锁定车队。
现在,我的第一个尝试几乎满足了这些期望,但需要避免伪共享的最小PADDING值约为8而不是16。我困惑了大约半个小时,直到我得出了显而易见的结论,即没有保证我的数组在主存储器中准确地对齐到缓存行的开头。实际对齐可能会因包括数组大小在内的许多条件而有所变化。
在这个例子中,我们当然不需要特别对齐数组,因为我们可以将PADDING设置为16,一切都能正常工作。但是有些情况下,某个结构体是否对齐到缓存行可能会产生影响。因此,我添加了一些代码来获取有关我的数组实际对齐方式的信息。
int main()
{
int arr[PARALLEL * 16];
thread threads[PARALLEL];
int offset = 0;
while (reinterpret_cast<int>(&arr[offset]) % 64) ++offset;
for (unsigned i = 0; i < PARALLEL; ++i)
{
threads[i] = thread(thread_func, &(arr[i * 16 + offset]));
}
for (auto& th : threads)
{
th.join();
}
return 0;
}
虽然这个解决方案在我的情况下很好用,但我不确定它是否是一种普遍的好方法。所以这里有我的问题:
除了我在上面的示例中所做的方式之外,是否有任何常见的方法可以使内存中的对象对齐到缓存行?
(使用g ++ MinGW Win32 x86 v.4.8.1 posix dwarf rev3)
*ptr
保存在寄存器中,从而隐藏虚假共享的惩罚。 - Mysticialptr
进行解引用。 - Rene R.