Linux 上的多核计算性能低下(openMP、boost::thread 等)

Posted

技术标签:

【中文标题】Linux 上的多核计算性能低下(openMP、boost::thread 等)【英文标题】:Low performance of multicore calculations on Linux (openMP, boost::thread, etc) 【发布时间】:2012-12-10 09:42:05 【问题描述】:

我想在我的应用程序中使用多核计算。我开始开发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) 笔记本电脑。

为什么我的多线程性能这么差?

【问题讨论】:

您必须提供有关您应用多线程的问题的更多信息。另外,你的时间是哪个单位的?线程会增加开销(线程创建、上下文切换、错误共享),在执行多线程时必须考虑这些开销,如果计算时间很短,这将克服任何好处。 看来我找到了解决方案。问题出在 boost::timer 中(我用它来查看经过时间)。当我将 boost::timer 更改为本机 openMP 时间函数 omp_get_wtime() 时,我得到的 omp_reduction 函数比串行快两倍(./openmp_test Serial。总和:1.77544e+08 时间:21.8169 减少,2 个线程。总和:1.77544e+ 08 时间:10.8623) 在较小的集合上,niter = 50,100...,使用 boost 线程示例,结果似乎符合预期 - 线程运行大约减少了两倍。目前正在运行完整的 2000 示例,但它似乎只会确认您的结果(调试版本已运行超过 10 分钟) 是的,boost::timer 测量的是 cpu 时间而不是挂钟时间,这在为多线程应用程序计时时不好。 使用 shell 的time(即time ./openmp_test)而不是在你的应用程序中这样做。 【参考方案1】:

您的两个代码,一个基于 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
...

sum1sum2 位于父函数 omp_sections 的堆栈上(附注 - omp_ 前缀是为 OpenMP 运行时库中的函数保留的;不要用它来命名您自己的职能!)。作为双精度数,sum1sum2 在 8 字节边界上对齐,总共占用 16 个字节。它们都落入同一高速缓存行的概率是 7/8 或 87.5%。当第一个线程想要更​​新sum1 时会发生以下情况:

它读取保存sum1的缓存行 更新sum1的值 它通知所有其他内核缓存行的内容已更改,因此它们必须在其缓存中使其无效

最后一部分非常关键——它是所谓的缓存一致性的一部分。由于sum1sum2 可能落在同一缓存行中,因此执行秒线程的核心必须使其缓存无效并从较低的内存层次结构级别重新加载它(例如,从共享的最后一级缓存或从主存储器)。当第二个线程修改sum2 的值时,情况完全相同。

一种可能的解决方案是使用 reduction 子句,就像使用 OpenMP 工作共享指令 for 一样:

double sum;

#pragma omp parallel sections reduction(+:sum) num_threads(2)

   ...

另一种可能的解决方案是在两个值之间插入一些填充,以使它们分开多个缓存行:

double sum1;
char pad[64];
double sum2;

我不知道 C++ 标准是否保证局部变量如何放置在堆栈上,即可能无法保证编译器不会“优化”变量的放置并且不会重新排序他们喜欢sum1sum2pad。如果是这样,它们可以被放置在一个结构中。

问题与您的线程案例基本相同。类数据成员取:

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;

虽然这消除了错误共享,但它仍然阻止大多数编译器使用寄存器优化,因为sum1sum2 是共享变量。因此它仍然会比使用归约的版本慢。在我的测试系统上,如果串行执行时间为 20 秒,则在缓存行边界上对齐两个变量可将执行时间从 56 秒减少到 30 秒。这只是表明,有时 OpenMP 结构会破坏一些编译器优化,并且并行代码的运行速度可能比串行代码慢得多,因此必须小心。

您可以将两个变量都设为lastprivate,这将允许编译器对它们执行寄存器优化:

#pragma omp parallel sections num_threads(2) lastprivate(sum1,sum2)

通过这种修改,部分代码的运行速度与使用工作共享指令的代码一样快。另一种可能的解决方案是累积到局部变量并在循环完成后分配给sum1sum2

#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++ 类对速度的影响。

【讨论】:

非常感谢您的建议。使用缩减子句可以将部分的计算速度加快到与并行 for 情况相同的时间。在 section 的情况下添加 char pad[64] 并为 boost::thread 添加 char pad[32] 不会加速计算(它仍然比串行慢 1.5 倍)。 @Genuine,填充很可能作为未使用的变量被删除。请参阅我的更新答案。【参考方案2】:

评论太长了

sincos 的实现有些奇怪。

编辑:当然它与sincos 无关,但与数组a 的访问模式有关)。

(编辑 2: 并且大量冗余的sincos 调用被消除。在函数single_thread 中,编译器将循环不变调用移到sincos 之外循环,但它不会在Calc::run 方法中移动它们。所以这解释了性能上的差异。是时候提出一个问题,为什么编译器会做不同的事情:) )

比较有和没有以下变化的程序。

虽然单线程版本的执行时间大致相同(约 12 秒),但原始多线程版本的执行时间约为 18 秒(即比单线程版本慢),但修改后的多线程版本执行大约 7 秒 (niter == 1000)。

--- thread-smp-orig.cxx        2012-12-10 12:40:03.547640307 +0200
+++ thread-smp.cxx        2012-12-10 12:37:27.990650712 +0200
@@ -26,11 +26,13 @@ public:
         double x;
         for (int i = start; i < end; i++)
         
+            double sai = sin(a[i]);
             for (int j = 0; j < niter; j++)
             
+                double caj = cos(a[j]);
                 for (int k = 0; k < niter; k++)
                 
-                    x = sin(a[i]) * cos(a[j]) * sin(a[k]);
+                    x = sai * caj * sin(a[k]);
                     sum += x;
                 
             
@@ -48,11 +50,13 @@ double single_thread(double a[], const i
     double x;
     for (int i = 0; i < niter; i++)
     
+        double sai = sin(a[i]);
         for (int j = 0; j < niter; j++)
         
+            double caj = cos(a[j]);
             for (int k = 0; k < niter; k++)
             
-                x = sin(a[i]) * cos(a[j]) * sin(a[k]);
+                x = sai * caj * sin(a[k]);
                 sum += x;
             
         

【讨论】:

非常感谢您的建议。在 boost::thread 的情况下,您的补丁确实可以加速计算(两个线程比我笔记本电脑上的串行函数快两倍)。 另外,我刚刚发现在两个 omp 部分的情况下,同样的补丁加速计算。 @正版,如果你看一下原程序的反汇编,你可以看到在Calc::run方法中,所有对sin/cos的调用都在最内层循环中。但在函数single_thread 中,编译器自己将它们移出循环,就好像应用了补丁一样。

以上是关于Linux 上的多核计算性能低下(openMP、boost::thread 等)的主要内容,如果未能解决你的问题,请参考以下文章

C++ openmp并行程序在多核linux上如何最大化使用cpu

[openMP] OpenMP在visual studio和mac上的配置

OpenMp实现并行化

多核 CPU 上的 Redis 性能

使用 openMP 进行多核处理与多线程

使用 Visual Studio 2013 降低 OpenMP 的性能