1

我想在我的应用程序中使用多核计算。我开始使用 openMP (C++) 开发示例应用程序

当我启动它时,我发现我的多核计算并不比串行计算快(在某些情况下,甚至多核计算也比串行计算慢):

./openmp_test

串行。总和:1.77544e+08 时间:21.84

减少,2个线程。总和:1.77544e+08 时间:21.65

两节。总和:1.77544e+08 时间:60.65

我的下一个想法是创建boost::thread 应用程序来测试 CPU 核心上的两个线程。结果:

./boost_thread_test

串行。总和:1.42146e+09 时间:179.64

两个增强线程。总和:1.42146e+09 时间:493.34

我使用内置酷睿 i3 CPU 的 openSuSe (x64) 笔记本电脑。

为什么我的多线程性能如此糟糕?

4

2 回答 2

3

您的代码(基于 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 运行时库中的函数保留的;不要用它来命名您自己的函数!)。双打,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++ 标准是否保证局部变量如何放置在堆栈上,即可能无法保证编译器不会“优化”变量的位置并且不会像这样重新排序它们sum1sum2, 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;

尽管这消除了错误共享,但它仍然阻止大多数编译器使用寄存器优化,因为sum1sum2是共享变量。因此它仍然会比使用归约的版本慢。在我的测试系统上,如果串行执行时间为 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++ 类对速度有什么影响。

于 2012-12-10T12:18:42.193 回答
0

太长了,不能作为评论

sin和的实现有些奇怪cos

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

编辑2:并且还消除了大量冗余sincos调用。在函数single_thread中,编译器将循环不变调用移动到sin循环cos之外,但它没有在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;
             }
         }
于 2012-12-10T10:51:49.027 回答