您的两个代码,一个基于 OpenMP sections 和一个基于 boost::thread,很可能是虚假共享的受害者。错误共享的发生是因为时间加载和存储在整个高速缓存行上操作,而不是直接在它们的操作数上。例如下面的语句:
sum = sum + value;
不仅会导致sum 的值从内存中读取、更新然后写回,而且还会导致整个内存的一小部分(称为高速缓存行)被读取然后写回.现代 x86 CPU 上的高速缓存行通常为 64 字节,这意味着不仅sum 的值将从内存中加载/存储到内存中,而且还有 56 字节。缓存行也总是从 64 的倍数的地址开始。这对您的代码有什么影响?
在您的 OpenMP 部分代码中:
double sum1;
double sum2;
...
// one section operates on sum1
...
// one section operates on sum2
...
sum1 和 sum2 位于父函数 omp_sections 的堆栈上(附注 - omp_ 前缀是为 OpenMP 运行时库中的函数保留的;不要用它来命名您自己的职能!)。作为双精度数,sum1 和 sum2 在 8 字节边界上对齐,总共占用 16 个字节。它们都落入同一高速缓存行的概率是 7/8 或 87.5%。当第一个线程想要更新sum1 时会发生以下情况:
- 它读取保存
sum1的缓存行
- 更新
sum1的值
- 它通知所有其他内核缓存行的内容已更改,因此它们必须在其缓存中使其无效
最后一部分非常关键——它是所谓的缓存一致性的一部分。由于sum1 和sum2 可能落在同一缓存行中,因此执行秒线程的核心必须使其缓存无效并从较低的内存层次结构级别重新加载它(例如,从共享的最后一级缓存或从主存储器)。当第二个线程修改sum2 的值时,情况完全相同。
一种可能的解决方案是使用 reduction 子句,就像使用 OpenMP 工作共享指令 for 一样:
double sum;
#pragma omp parallel sections reduction(+:sum) num_threads(2)
{
...
}
另一种可能的解决方案是在两个值之间插入一些填充,以使它们分开多个缓存行:
double sum1;
char pad[64];
double sum2;
我不知道 C++ 标准是否保证局部变量如何放置在堆栈上,即可能无法保证编译器不会“优化”变量的放置并且不会重新排序他们喜欢sum1、sum2、pad。如果是这样,它们可以被放置在一个结构中。
问题与您的线程案例基本相同。类数据成员取:
double *a; // 4 bytes on x86, 8 bytes on x64
int niter; // 4 bytes
int start; // 4 bytes
int end; // 4 bytes
// 4 bytes padding on x64 because doubles must be aligned
double sum; // 8 bytes
类数据成员在 x86 上占用 24 个字节,在 x64 上占用 32 个字节(x86 在 64 位模式下)。这意味着两个类实例可以放在同一个缓存行中,或者可能共享一个。同样,您可以在 sum 之后添加一个至少 32 字节大小的填充数据成员:
class Calc
{
private:
double *a;
int niter;
int start;
int end;
double sum;
char pad[32];
...
};
请注意,private 变量,包括由 reduction 子句创建的隐式私有副本,可能驻留在各个线程的堆栈上,因此相隔不止一个缓存行,因此不会发生错误共享,并且代码并行运行更快。
编辑:我忘了提到大多数编译器会在优化阶段删除未使用的变量。在 OpenMP 部分的情况下,填充大部分都被优化了。这可以通过应用对齐属性来解决(警告:可能特定于 GCC):
double sum1 __attribute__((aligned(64))) = 0;
double sum2 __attribute__((aligned(64))) = 0;
虽然这消除了错误共享,但它仍然阻止大多数编译器使用寄存器优化,因为sum1 和sum2 是共享变量。因此它仍然会比使用归约的版本慢。在我的测试系统上,如果串行执行时间为 20 秒,则在缓存行边界上对齐两个变量可将执行时间从 56 秒减少到 30 秒。这只是表明,有时 OpenMP 结构会破坏一些编译器优化,并且并行代码的运行速度可能比串行代码慢得多,因此必须小心。
您可以将两个变量都设为lastprivate,这将允许编译器对它们执行寄存器优化:
#pragma omp parallel sections num_threads(2) lastprivate(sum1,sum2)
通过这种修改,部分代码的运行速度与使用工作共享指令的代码一样快。另一种可能的解决方案是累积到局部变量并在循环完成后分配给sum1 和sum2:
#pragma omp section
{
double s = 0;
for (int i = 0; i < niter / 2; i++)
{
for (int j = 0; j < niter; j++)
{
for (int k = 0; k < niter; k++)
{
double x = sin(a[i]) * cos(a[j]) * sin(a[k]);
s += x;
}
}
}
sum1 = s;
}
// Same for the other section
这个基本上等同于threadprivate(sum1)。
很遗憾,我没有安装boost,所以我无法测试您的线程代码。尝试使用Calc::run() 执行整个计算,以了解使用 C++ 类对速度的影响。