【问题标题】:OpenMP slows down program instead of speeding it up: a bug in gcc?OpenMP 减慢程序而不是加速它:gcc 中的错误?
【发布时间】:2011-08-21 17:57:27
【问题描述】:

我将首先提供一些有关我遇到的问题的背景信息,以便您知道我要做什么。我一直在帮助开发某个软件工具,并发现我们可以从使用 OpenMP 并行化该软件中的一些最大循环中受益匪浅。实际上,我们成功地并行化了循环,并且仅使用两个内核,循环的执行速度提高了 30%,这是一个不错的改进。另一方面,我们注意到一个使用递归调用遍历树结构的函数中有一个奇怪的现象。在 OpenMP 开启的情况下,程序在此处实际上变慢了,并且该函数的执行时间增加了一倍多。我们认为树结构可能对并行化不够平衡,并在此函数中注释掉了 OpenMP pragma。不过,这似乎对执行时间没有影响。我们目前使用带有 -fopenmp 标志的 GCC 编译器 4.4.6 来支持 OpenMP。这是当前的问题:

如果我们不在代码中使用任何 omp pragma,一切运行良好。但是如果我们只在程序的 main 函数的开头添加以下内容,则树遍历函数的执行时间会翻倍,从 35 秒增加到 75 秒:

//beginning of main function
...
#pragma omp parallel
{
#pragma omp single
{}
}
//main function continues
...

有没有人知道为什么会发生这种情况?我不明白为什么程序会因为使用 OpenMP 编译指示而大大减慢。如果我们去掉所有的 omp pragma,树遍历函数的执行时间又会回落到 35 秒。我猜这是某种编译器错误,因为我现在没有其他解释。

【问题讨论】:

    标签: performance parallel-processing openmp


    【解决方案1】:

    并非所有可以并行化的东西都应该并行化。如果您使用的是单个线程,则只有一个线程执行它,其余线程必须等到该区域完成。他们可以旋转等待或睡眠。大多数实现都以自旋等待开始,希望单个区域不会花费太长时间,并且等待线程可以比休眠更快地看到完成。自旋等待会占用大量处理器周期。您可以尝试指定等待应该是被动的 - 但这仅在 OpenMP V3.0 中并且只是对实现的提示(因此它可能没有任何效果)。基本上,除非您在并行区域中有大量工作可以弥补单机,否则单机将大大增加并行开销,并且可能会使并行化成本过高。

    【讨论】:

    • 感谢您的回答,但这仍然不能解释执行时间的增加。问题中给出的代码几乎立即完成,但会减慢程序的其余部分,即使空的“pragma omp single”块不应该以任何方式影响它。我开始怀疑使用 OpenMP 是否真的会禁用某些编译器优化,因此在程序中使用 OpenMP anywhere 会减慢程序中代码 everywhere 的执行速度。
    • 根据编译器的不同,某些优化可能在 OpenMP 区域内被禁用。但是,它应该对区域以外的代码没有影响。也就是说,我们已经看到在 OpenMP 区域之后诸如内存分配器之类的东西具有更高的开销并减慢了速度的情况。但是,在您的情况下,如果代码与您显示的一样,那么由于 OpenMP 的设置,您会看到一些额外的开销。它不应该像您所指出的那样增加运行时间。您应该查看生成的代码 (-S),看看它与 OpenMP 有多大不同。
    【解决方案2】:

    首先,OpenMP 在首次尝试时通常会降低性能。如果您不完全理解它,使用 omp parallel 可能会很棘手。如果您能告诉我更多有关程序结构的信息,特别是以下由????注释的问题,我可能会有所帮助。

    //beginning of main function
    ...
    #pragma omp parallel
    {
    
    ???? What goes here, is this a loop? if so, for loop, while loop?
    
    #pragma omp single
       { 
    
         ???? What goes here, how long does it run? 
      }
    }
    
    //main function continues
    ....
    ???? Does performance of this code reduce or somewhere else?
    

    谢谢。

    【讨论】:

    • 代码和写的一样,在并行区域里面除了一个空的单个区域之外什么都没有。此代码几乎立即完成,仅用于测试目的。我目前没有代码,但稍后在 main 中调用另一个函数,我们将其称为 complexFunction。如果我们取出两个 omp pragma 或只取出“单个”pragma,这个 complexFunction 将执行 35 秒。我的猜测是编译器完全去掉了空的并行区域。如果我们把这两个 pragma 写回去,执行时间又会上升到 75 秒。
    【解决方案3】:

    谢谢大家。今天,我们能够通过链接 TCMalloc(ejd 提供的解决方案之一)来解决这个问题。执行时间立即下降,与非线程版本相比,我们能够将执行时间缩短 40% 左右。我们使用了 2 个内核。似乎当在 Unix 上使用带有 GCC 的 OpenMP 时,您还应该选择标准内存管理解决方案的替代品。否则程序可能会变慢。

    【讨论】:

      【解决方案4】:

      我做了更多的测试并做了一个小测试程序来测试问题是否与内存操作有关。我无法在我的小型测试程序中复制空的并行单区域导致程序变慢的问题,但我能够通过并行化一些 malloc 调用来复制变慢。

      在具有 2 个 CPU 内核的 Windows 7 64 位上运行测试程序时,与在没有 OpenMP 的情况下运行程序相比,在 gcc (g++) 编译器中使用 -fopenmp 标志并运行已编译的程序并没有导致明显的减速支持。

      但是,在同一台计算机上的 Kubuntu 11.04 64 位上执行相同操作会将执行提高到非 OpenMP 版本的 4 倍以上。这个问题似乎只出现在 Unix 系统上,而不出现在 Windows 上。

      我的测试程序的源代码如下。我还上传了 win 和 unix 版本的 zipped-source 以及带有和不支持 OpenMP 的 win 和 unix 版本的汇编源代码。这个压缩包可以在这里下载http://www.2shared.com/file/0thqReHk/omp_speed_test_2011_05_11.html

      #include <stdio.h>
      #include <windows.h>
      #include <list>
      #include <sys/time.h>
      //#include <cstdlib>
      
      using namespace std;
      
      int main(int argc, char* argv[])
      {
      //  #pragma omp parallel
      //  #pragma omp single
      //  {}
      
        int start = GetTickCount();
        /*
        struct timeval begin, end;
        int usecs;
        gettimeofday(&begin, NULL);
        */
        list<void *> pointers;
      
        #pragma omp parallel for default(shared)
        for(int i=0; i< 10000; i++)
          //pointers.push_back(calloc(20000, sizeof(void *)));
          pointers.push_back(malloc(20000));
      
        for(list<void *>::iterator i = pointers.begin(); i!= pointers.end(); i++)
          free(*i);
      
        /*
        gettimeofday(&end, NULL);
        if (end.tv_usec < begin.tv_usec) {
          end.tv_usec += 1000000;
          begin.tv_sec += 1;
        }
        usecs = (end.tv_sec - begin.tv_sec) * 1000000;
        usecs += (end.tv_usec - begin.tv_usec);
        */
      
        printf("It took %d milliseconds to finish the memory operations", GetTickCount() - start);
        //printf("It took %d milliseconds to finish the memory operations", usecs/1000);
      
        return 0;
        }
      

      现在仍然没有答案的是,我可以做些什么来避免在 Unix 平台上出现此类问题..

      【讨论】:

      • 这个很简单。如果您使用的是标准 malloc,那么它只是线程安全的,因为它为整个分配例程执行了锁定。这实质上是序列化代码并增加了锁开销。如果您想在并行区域内使用 malloc,那么您应该探索一些其他可用的内存分配包,这些包更适合多线程环境(例如,Hoard、ptmalloc、tcmalloc 等)。请参阅主题:“多线程是否强调内存碎片?” (stackoverflow.com/questions/5875989/…)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-24
      相关资源
      最近更新 更多