【问题标题】:OpenMP embarrassingly parallel for loop, no speedupOpenMP 令人尴尬的并行 for 循环,没有加速
【发布时间】:2014-09-02 13:33:34
【问题描述】:

我有一个看似非常简单的并行for 循环,它只是将零写入整数数组。但事实证明,线程越多,循环越慢。我认为这是由于一些缓存抖动造成的,所以我使用了时间表、块大小、__restrict__,将并行嵌套在并行块内,然后刷新。然后我注意到读取数组进行归约也比较慢。

这显然应该非常简单,并且应该几乎线性加速。我在这里错过了什么?

完整代码:

#include <omp.h>
#include <vector>
#include <iostream>
#include <ctime>

void tic(), toc();

int main(int argc, const char *argv[])
{
    const int COUNT = 100;
    const size_t sz = 250000 * 200;
    std::vector<int> vec(sz, 1);

    std::cout << "max threads: " << omp_get_max_threads()<< std::endl;

    std::cout << "serial reduction" << std::endl;
    tic();
    for(int c = 0; c < COUNT; ++ c) {
        double sum = 0;
        for(size_t i = 0; i < sz; ++ i)
            sum += vec[i];
    }
    toc();

    int *const ptr = vec.data();
    const int sz_i = int(sz); // some OpenMP implementations only allow parallel for with int

    std::cout << "parallel reduction" << std::endl;
    tic();
    for(int c = 0; c < COUNT; ++ c) {
        double sum = 0;
        #pragma omp parallel for default(none) reduction(+:sum)
        for(int i = 0; i < sz_i; ++ i)
            sum += ptr[i];
    }
    toc();

    std::cout << "serial memset" << std::endl;
    tic();
    for(int c = 0; c < COUNT; ++ c) {
        for(size_t i = 0; i < sz; ++ i)
            vec[i] = 0;
    }
    toc();

    std::cout << "parallel memset" << std::endl;
    tic();
    for(int c = 0; c < COUNT; ++ c) {
        #pragma omp parallel for default(none)
        for(int i = 0; i < sz_i; ++ i)
            ptr[i] = 0;
    }
    toc();

    return 0;
}

static clock_t ClockCounter;

void tic()
{
    ClockCounter = std::clock();
}

void toc()
{
    ClockCounter = std::clock() - ClockCounter;
    std::cout << "\telapsed clock ticks: " << ClockCounter << std::endl;
}

运行这个会产生:

g++ omp_test.cpp -o omp_test --ansi -pedantic -fopenmp -O1
./omp_test
max threads: 12
serial reduction
  elapsed clock ticks: 1790000
parallel reduction
  elapsed clock ticks: 19690000
serial memset
  elapsed clock ticks: 3860000
parallel memset
  elapsed clock ticks: 20800000

如果我使用-O2 运行,g++ 能够优化串行减少并且我得到零时间,因此-O1。此外,加上omp_set_num_threads(1); 会使时间更加相似,尽管仍然存在一些差异:

g++ omp_test.cpp -o omp_test --ansi -pedantic -fopenmp -O1
./omp_test
max threads: 1
serial reduction
  elapsed clock ticks: 1770000
parallel reduction
  elapsed clock ticks: 7370000
serial memset
  elapsed clock ticks: 2290000
parallel memset
  elapsed clock ticks: 3550000

这应该很明显,我觉得我没有看到非常基本的东西。我的 CPU 是 Intel(R) Xeon(R) CPU E5-2640 0 @ 2.50GHz,具有超线程,但在同事的具有 4 核且没有超线程的 i5 上观察到相同的行为。我们都在运行 Linux。

编辑

似乎一个错误是在时间方面,运行:

static double ClockCounter;

void tic()
{
    ClockCounter = omp_get_wtime();//std::clock();
}

void toc()
{
    ClockCounter = omp_get_wtime()/*std::clock()*/ - ClockCounter;
    std::cout << "\telapsed clock ticks: " << ClockCounter << std::endl;
}

产生更“合理”的时间:

g++ omp_test.cpp -o omp_test --ansi -pedantic -fopenmp -O1
./omp_test
max threads: 12
serial reduction
  elapsed clock ticks: 1.80974
parallel reduction
  elapsed clock ticks: 2.07367
serial memset
  elapsed clock ticks: 2.37713
parallel memset
  elapsed clock ticks: 2.23609

但仍然没有加速,只是不再慢了。

EDIT2

正如 user8046 所建议的那样,该代码受大量内存限制。正如 Z boson 所建议的那样,串行代码很容易优化掉,并且不确定这里测量的是什么。所以我做了一个小改动,将总和放在循环之外,这样它就不会在c 的每次迭代中归零。我还用sum+=F(vec[i]) 替换了归约操作,用vec[i] = F(i) 替换了memset 操作。运行方式:

g++ omp_test.cpp -o omp_test --ansi -pedantic -fopenmp -O1 -D"F(x)=sqrt(double(x))"
./omp_test
max threads: 12
serial reduction
  elapsed clock ticks: 23.9106
parallel reduction
  elapsed clock ticks: 3.35519
serial memset
  elapsed clock ticks: 43.7344
parallel memset
  elapsed clock ticks: 6.50351

计算平方根会增加线程的工作量,最终会有一些合理的加速(大约是7x,这是有道理的,因为超线程内核共享内存通道)。

【问题讨论】:

  • 减少如何令人尴尬地并行? sum 变量有一个(必要的)锁。
  • @MadScienceDreams 你说得对,当我写问题标题时,我只是在尝试(令人尴尬的并行)写入标题所指的数组。还原实验后来发生在我身上。但是,double 的减少仍然可以通过使用私有累加器(处理数千个元素)的每个线程 for 循环来实现,然后对每个线程的部分和进行树状或串行减少(总共有 12 个) )。这几乎是令人尴尬的并行(虽然不确定编译器是否会以这种方式实现它 - 我知道我可以)。
  • @HighPerformanceMark 是的,很好。我只使用了clock(),而不是一些更精确的函数来使它更简单......失败。
  • 我在 g++4.8.2-19ubuntu1 和 Xeon E5540 上得到了不同的结果。 “并行缩减”是“串行缩减”的 2 倍
  • @theswine,它稍微快一点。 max threads: 16 serial reduction elapsed clock ticks: 3.81508 parallel reduction elapsed clock ticks: 1.91143 serial memset elapsed clock ticks: 4.37205 parallel memset elapsed clock ticks: 2.95767

标签: c loops parallel-processing openmp


【解决方案1】:

您发现了计时错误。仍然没有加速,因为您的两个测试用例都受大量内存限制。在典型的消费类硬件上,您的所有内核共享一个内存总线,因此使用更多线程不会给您更多带宽,并且由于这是瓶颈,因此加速。如果您减小问题大小以使其适合缓存,或者确定是否增加每个数据的计算次数,这可能会改变,例如,如果您正在计算 exp(vec[i]) 或 1/vec[一世]。对于 memset:你可以用一个线程使内存饱和,在那里你永远不会看到加速。 (仅当您可以访问具有更多线程的第二个内存总线时,如某些多插槽架构)。 关于减少的一个评论,这很可能不是用锁来实现的,这将是非常低效的,但使用对数加速并没有那么糟糕的加法树。

【讨论】:

  • 一个有趣的想法...所以我写了sum += sin(exp(sqrt(double(vec[i])))) 而不是sum += sin(exp(sqrt(double(vec[i])))) 和类似的vec[i] = 0 而不是sin(exp(sqrt(double(i)))),类型转换确保“正常”版本使用数学函数,而不是整数函数。这会将时间更改如下:serial reduction: 17.9 sec, parallel reduction: 99.4 sec, serial memset: 161.4 sec, parallel memset: 38.6 sec。所以我想你是对的,至少就 memset 而言。将不得不考虑为什么减少突然变得如此缓慢。有什么想法吗?
  • @theswine,我应该提到我的 E5540 是 NUMA,这就是为什么我可能看到了加速。
  • 相信你必须好好照顾优化。仍然很容易发现 sum 的值仅取决于最后一次循环运行。如果我只在开始时重置 sum 变量,我会得到更合理的结果:最大线程数:8 个串行减少经过的时钟滴答:12.4259 并行减少经过的时钟滴答:1.48168 串行 memset 经过的时钟滴答:1.89196 并行 memset 经过的时钟滴答:1.56518 那是在沙桥上。如果将总和写入标准输出,则无法优化并使用 -O3
  • 你的声明“对于 memset:你可以用一个线程使内存饱和,你永远不会看到加速。”即使在单套接字系统上也是错误的。单个线程不会使带宽饱和。这是一个常见的误解。
【解决方案2】:

除了您在 Linux 中使用时钟功能的错误之外,您的其余问题都可以通过阅读这些问题/答案来回答。

in-an-openmp-parallel-code-would-there-be-any-benefit-for-memset-to-be-run-in-p/11579987

measuring-memory-bandwidth-from-the-dot-product-of-two-arrays

memset-in-parallel-with-threads-bound-to-each-physical-core

因此,您应该会看到使用 memset 的多线程带来的显着好处,并且即使在单个套接字系统上也可以减少。为此,我编写了自己的工具来测量带宽。您可以从我的 i5-4250U (Haswell) 中找到一些结果,其中 2 个内核(GCC 4.8、Linux 3.13、EGLIBC 2.19)运行超过 1 GB。 vsum 是您的减免。请注意,即使在这两个核心系统上也有显着改进。

一个线程

C standard library
                       GB    time(s)       GB/s     GFLOPS   efficiency
memset:              0.50       0.80       6.68       0.00        inf %
memcpy:              1.00       1.35       7.93       0.00        inf %

Agner Fog's asmlib
                       GB    time(s)       GB/s     GFLOPS   efficiency
memset:              0.50       0.71       7.53       0.00        inf %
memcpy:              1.00       0.93      11.51       0.00        inf %

my_memset   
                     0.50       0.71       7.53       0.00        inf %


FMA3 reduction tests
                       GB    time(s)       GB/s     GFLOPS   efficiency
vsum:                0.50       0.53      10.08       2.52        inf %
vmul:                0.50       0.68       7.93       1.98        inf %
vtriad:              0.50       0.70       7.71       3.85        inf %
dot                  1.00       1.08       9.93       2.48        inf %

两个线程

C standard library
                       GB    time(s)       GB/s     GFLOPS   efficiency
memset:              0.50       0.64       8.33       0.00        inf %
memcpy:              1.00       1.10       9.76       0.00        inf %

Agner Fog's asmlib
                       GB    time(s)       GB/s     GFLOPS   efficiency
memset:              0.50       0.36      14.98       0.00        inf %
memcpy:              1.00       0.66      16.30       0.00        inf %

my_memset
                     0.50       0.36      15.03       0.00        inf %


FMA3 tests
standard sum tests with OpenMP: 2 threads
                       GB    time(s)       GB/s     GFLOPS   efficiency
vsum:                0.50       0.41      13.03       3.26        inf %
vmul:                0.50       0.39      13.67       3.42        inf %
vtriad:              0.50       0.44      12.20       6.10        inf %
dot                  1.00       0.97      11.11       2.78        inf %

这是我的自定义 memset 函数(我还有其他几个类似的测试)。

void my_memset(int *s, int c, size_t n) {
    int i;
    __m128i v = _mm_set1_epi32(c);
    #pragma omp parallel for
    for(i=0; i<n/4; i++) {
        _mm_stream_si128((__m128i*)&s[4*i], v);
    }
}

编辑:

您应该使用-O3-ffast-math 进行编译。在外循环之外定义总和,然后将其打印出来,这样 GCC 就不会对其进行优化。 GCC 不会自动矢量化减少,因​​为浮点算术不是关联的,并且矢量化循环可能会破坏 IEEE 浮点规则。使用 -ffast-math 允许浮点算术是关联的,这允许 GCC 向量化减少。应该指出的是,已经在 OpenMP 中进行了缩减假设浮点运算是关联的,因此它已经违反了 IEEE 浮点规则。

double sum = 0;
tic();
for(int c = 0; c < COUNT; ++ c) { 
    #pragma omp parallel for reduction(+:sum)
    for(int i = 0; i < sz_i; ++ i)
        sum += ptr[i];
}
toc();
printf("sum %f\n", sum);

编辑:

我测试了您的代码并进行了一些修改。使用多线程进行缩减和 memset 可以加快速度

max threads: 4
serial reduction
dtime 1.86, sum 705032704
parallel reduction
dtime 1.39 s, sum 705032704
serial memset
dtime 2.95 s
parallel memset
dtime 2.44 s
serial my_memset
dtime 2.66 s
parallel my_memset
dtime 1.35 s

这是我使用的代码(g++ foo.cpp -fopenmp -O3 -ffast-math)

#include <omp.h>
#include <vector>
#include <iostream>
#include <ctime>
#include <stdio.h>

#include <xmmintrin.h>

void my_memset(int *s, int c, size_t n) {
    int i;
    __m128i v = _mm_set1_epi32(c);
    for(i=0; i<n/4; i++) {
        _mm_stream_si128((__m128i*)&s[4*i], v);
    }
}

void my_memset_omp(int *s, int c, size_t n) {
    int i;
    __m128i v = _mm_set1_epi32(c);
    #pragma omp parallel for
    for(i=0; i<n/4; i++) {
        _mm_stream_si128((__m128i*)&s[4*i], v);
    }
}

int main(int argc, const char *argv[])
{
    const int COUNT = 100;
    const size_t sz = 250000 * 200;
    std::vector<int> vec(sz, 1);

    std::cout << "max threads: " << omp_get_max_threads()<< std::endl;

    std::cout << "serial reduction" << std::endl;
    double dtime;
    int sum;

    dtime = -omp_get_wtime();
    sum = 0;
    for(int c = 0; c < COUNT; ++ c) {
        for(size_t i = 0; i < sz; ++ i)
            sum += vec[i];
    }
    dtime += omp_get_wtime();
    printf("dtime %.2f, sum %d\n", dtime, sum);

    int *const ptr = vec.data();
    const int sz_i = int(sz); // some OpenMP implementations only allow parallel for with int

    std::cout << "parallel reduction" << std::endl;


    dtime = -omp_get_wtime();
    sum = 0;
    for(int c = 0; c < COUNT; ++ c) {
        #pragma omp parallel for default(none) reduction(+:sum)
        for(int i = 0; i < sz_i; ++ i)
            sum += ptr[i];
    }
    dtime += omp_get_wtime();
    printf("dtime %.2f s, sum %d\n", dtime, sum);

    std::cout << "serial memset" << std::endl;

    dtime = -omp_get_wtime();
    for(int c = 0; c < COUNT; ++ c) {
        for(size_t i = 0; i < sz; ++ i)
            vec[i] = 0;
    }   
    dtime += omp_get_wtime();
    printf("dtime %.2f s\n", dtime);

    std::cout << "parallel memset" << std::endl;
    dtime = -omp_get_wtime();
    for(int c = 0; c < COUNT; ++ c) {
        #pragma omp parallel for default(none)
        for(int i = 0; i < sz_i; ++ i)
            ptr[i] = 0;
    }
    dtime += omp_get_wtime();
    printf("dtime %.2f s\n", dtime);

    std::cout << "serial my_memset" << std::endl;

    dtime = -omp_get_wtime();
    for(int c = 0; c < COUNT; ++ c) my_memset(ptr, 0, sz_i);

    dtime += omp_get_wtime();
    printf("dtime %.2f s\n", dtime);

    std::cout << "parallel my_memset" << std::endl;
    dtime = -omp_get_wtime();
    for(int c = 0; c < COUNT; ++ c) my_memset_omp(ptr, 0, sz_i);
    dtime += omp_get_wtime();
    printf("dtime %.2f s\n", dtime);

    return 0;
}

【讨论】:

  • 这并不能真正回答我的问题。正如你所说,我知道它应该更快,但我不知道为什么它不是
  • @theswine,我明白了,你得给我一点时间,我不知道我今天能不能调试你的代码。但是,如果您仔细研究我链接的那些问题/答案,您可能会找到答案。
  • @theswine,我在答案的末尾添加了一些文本,可以解释为什么您没有看到改进。稍后我会尝试对此进行更多研究。
  • @theswine,好的,我运行了您的代码并进行了一些更改。请参阅我的更新答案。使用并行版本我得到了更快的时间。在您的系统上测试并查看。
  • @theswine,至于 SSE,这与使用 SSE 无关。 GLIBC(或 EGLIBC)无论如何都可能使用 SSE。这是关于时间与非时间存储的。 GLIBC 使用_mm_store_ps 使用临时存储,my_memset 使用_mm_stream_ps 使用非临时存储。临时存储必须读取才能写入。非临时存储只写。当您有大量无法放入缓存的数据时,请使用非临时存储。
【解决方案3】:

您正在使用 std::clock 报告使用的 CPU 时间,而不是实时。因此,每个处理器的时间都会相加,并且总是高于单线程时间(由于开销)。

http://en.cppreference.com/w/cpp/chrono/c/clock

【讨论】:

    最近更新 更多