您的代码(基于 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()
so 执行整个计算,看看使用 C++ 类对速度有什么影响。