我想在我的应用程序中使用多核计算。我开始开发sample application with openMP (C++)。
当我启动它时,我发现我的多核计算没有比串行计算快(即使在某些情况下,多核计算也比串行计算慢):
./openmp_test
序列号。和:1.77544e + 08时间:21.84
减少2个线程。和:1.77544e + 08时间:21.65
两个部分。和:1.77544e + 08时间:60.65
我的下一个想法是创建boost::thread application来测试CPU内核上的两个线程。结果:
./boost_thread_test
序列号。和:1.42146e + 09时间:179.64
两个增强线程。和:1.42146e + 09时间:493.34
我使用内部装有Core i3 CPU的openSuSe(x64)笔记本电脑。
为什么我的多线程性能这么差?
最佳答案
您的两个代码(一个基于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运行时库中的函数保留;请勿使用它来命名自己的函数!)。作为 double 型,sum1
和sum2
在8字节边界上对齐,总共需要16个字节。它们都落在同一缓存行中的概率为7/8或87.5%。当第一个线程想要更新sum1
时,将发生以下情况:sum1
的缓存行sum1
的值最后一部分非常关键-这是所谓的缓存一致性的一部分。由于
sum1
和sum2
可能落在同一缓存行中,因此执行秒线程的内核必须使它的缓存无效并从较低的内存层次结构级别(例如,从共享的最后一级缓存或从主内存)重新加载它。 。当第二个线程修改sum2
的值时,情况完全相同。一种可能的解决方案是像使用OpenMP工作共享指令
reduction
一样使用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字节(在64位模式下为x86)。这意味着两个类实例可以放入同一高速缓存行中,或者可能共享一个。同样,您可以在
sum
后添加至少32个字节大小的填充数据成员:class Calc
{
private:
double *a;
int niter;
int start;
int end;
double sum;
char pad[32];
...
};
请注意,
private
变量(包括由reduction
子句创建的隐式私有(private)副本)可能驻留在各个线程的堆栈上,因此相隔不止一个缓存行,因此不会发生错误共享,并且代码并行运行速度更快。编辑:我忘了提到大多数编译器在优化阶段会删除未使用的变量。在使用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++类对速度有何影响。