【发布时间】:2013-08-14 15:56:45
【问题描述】:
我编写了以下简短的 C++ 程序来重现 Herb Sutter 描述的虚假共享效果:
比如说,我们想要执行总量为 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 > # 个核心。另一方面,如果 PADDING 设置为低于 16 的任何值,则应该会遇到严重的争用,因为现在至少有两个线程可能在同一个缓存行上运行,但是受到内置硬件互斥锁的保护。我们希望我们的加速不仅在这种情况下是亚线性的,而且甚至总是
现在,我的第一次尝试几乎满足了这些期望,但避免错误共享所需的最小 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保存在寄存器 = 中,从而隐藏错误共享惩罚。 -
为了学习,我打开了编译器优化,所以
ptr每次都要解引用。 -
@Mysticial:我认为他正在运行优化的构建,但事实并非如此。此外,我对 [required padding] 大约为 8 而不是 16... 这应该与缓存行内的对齐无关的语句感到有些惊讶。如果cache line是64bytes,padding是8,那么在同一个cache line中会有多个int,不管它们是否与cache line对齐。此外,对于 64 字节缓存行和 4 字节整数,您希望填充为 15,而不是 16,对吗?每个缓存行是 1 个值和 15 个占位符...
-
@DavidRodríguez-dribeas 没错。对齐应该无关紧要。但在这种情况下,它可能足以欺骗编译器做一些不那么愚蠢的事情。与往常一样 - 当优化关闭时,一切都会发生。
标签: c++ multithreading caching parallel-processing