c++ 我的代码,与OpenMP并行,所以慢,因为并发内存访问?

yfwxisqw  于 12个月前  发布在  其他
关注(0)|答案(2)|浏览(188)

我有一些代码,其中一个(通常是大的)数组的内容被添加到另一个相同大小的数组中:

for (long i = 0; i < len; i++)
     data1[i] += data2[i];

看起来这应该很容易使用OpenMP并行化:

#pragma omp parallel for
for (long i = 0; i < len; i++)
     data1[i] += data2[i];

然而,即使我有8个核心+8个超线程,加速也是最小的(可能是10%)。我试着微调了一下:

#pragma omp parallel for schedule(dynamic, 1000)

但没有效果。我看到所有内核和超线程都在并发工作,但只有10%或15%。
其他功能,例如在一个数组中找到最小值(这样你只需要读,而不是写),使用并行化可以轻松地运行5倍的速度。
这让我想到了一个假设,也许并行化没有多大帮助,因为写内存很慢,不能由不同的内核并发完成。这有道理吗我有很老的硬件:HP Z600工作站(2010),配有两个Xeon E5540 CPU,每个CPU为4个内核+ 4个超线程,并配有8 MB高速缓存。内存为1066 MHz的DDR3。
我的问题:

  • 你认为这就是并行化做不了多少事情的原因吗?
  • 我能做些什么来从并行化中获益更多?
ulydmbyx

ulydmbyx1#

你的假设基本上是正确的,你的计算通常是“内存限制”的,也就是说,与从/到内存传输的数据量相比,计算太少了,瓶颈是CPU和内存之间的传输速率。因此,使用更多的核心并没有帮助。
在较新的CPU(特别是为服务器设计的CPU)上,您可能会观察到更高的速度提升,因为近年来内存带宽的增长速度超过了单核性能。

编辑:注意,在明显的内存限制计算的情况下,最好的性能并不总是在最大可能的线程数下获得,因为它们都在竞争访问内存。使用2个或4个线程可能比使用8个线程快(有点)。
编辑2:为了得到更完整的答案,我收集了在评论(@Homer512)和@JerryCoffin的回答中提到的其他可能的影响:

  • NUMA影响,因为您有2个CPU(当一个CPU尝试访问连接到另一个CPU的内存时,内存访问速度较慢)
  • 线程创建开销(如果数组不够大,则可见)
  • (尚未提及)“冷启动”效应(当CPU空闲时,主板可能会降低频率,并且可能需要一点时间才能回到基本频率,因此第一次计算较慢)

所有上述效果部分取决于您编写测试代码的方式。这就是为什么展示整个代码而不仅仅是一小部分代码是很重要的。还有:

  • 涡轮频率效应:当使用单个内核时,CPU大部分时间以Turbo频率运行,而当使用所有内核时,它更可能以基频运行。在您的CPU上,Turbo频率将为单核执行提供给予约10%的性能奖励。
ecfdbz9o

ecfdbz9o2#

根据您编写代码的方式,创建线程以运行代码所花费的时间很容易出错。例如,考虑将片段扩展为完整程序的代码,并在各种大小的向量上运行代码:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <algorithm>
#include <chrono>
#include <numeric>
#include <locale>
#include <omp.h>
#include <iomanip>

template <class Container>
void init(Container &data1, Container &data2) {
    srand(1);
    std::generate(data1.begin(), data1.end(), rand);
    std::generate(data2.begin(), data2.end(), rand);
}

int main() { 
    std::cout.imbue(std::locale(""));
    using namespace std::chrono;

    for (unsigned size = 250'000; size < 50'000'000; size *= 2) {
        std::vector<uint64_t> data1(size);
        std::vector<uint64_t> data2(size);

        init(data1, data2);        

        auto start = high_resolution_clock::now();
        for (int i=0; i<size; i++)
            data1[i] += data2[i];

        auto stop = high_resolution_clock::now();

        auto sum = std::accumulate(data1.begin(), data1.end(), 0ULL);
        init(data1, data2);

        auto start2 = high_resolution_clock::now();
        #pragma omp parallel for
        for (int i=0; i<size; i++)
            data1[i] += data2[i];
        auto stop2 = high_resolution_clock::now();

        auto sum2 = std::accumulate(data1.begin(), data1.end(), 0ULL);

        std::cout << "size: " << size << "\n";
        std::cout << "\tsum 1: " << sum << "\n";
        std::cout << "\tsum 2: " << sum2 << "\n";

        auto dur1 = duration_cast<microseconds>(stop - start).count();
        auto dur2 = duration_cast<microseconds>(stop2 - start2).count();

        std::cout << "\t  no OMP: " << std::setw(10) << dur1 << "us\n";
        std::cout << "\twith OMP: " << std::setw(10) << dur2 << "us\n";
        std::cout << "\tSpeedup: " << (double)dur1 / dur2 << "\n"; 
    }
}

当我运行它时,我得到这样的结果:

size: 250,000
    sum 1: 537,095,867,682,884
    sum 2: 537,095,867,682,884
      no OMP:        275us
    with OMP:      1,261us
    Speedup: 0.218081
size: 500,000
    sum 1: 1,073,756,018,481,283
    sum 2: 1,073,756,018,481,283
      no OMP:        479us
    with OMP:        172us
    Speedup: 2.78488
size: 1,000,000
    sum 1: 2,147,344,996,944,184
    sum 2: 2,147,344,996,944,184
      no OMP:        655us
    with OMP:        224us
    Speedup: 2.92411
size: 2,000,000
    sum 1: 4,294,715,742,631,183
    sum 2: 4,294,715,742,631,183
      no OMP:      2,230us
    with OMP:        496us
    Speedup: 4.49597
size: 4,000,000
    sum 1: 8,590,750,767,966,991
    sum 2: 8,590,750,767,966,991
      no OMP:      6,303us
    with OMP:      2,135us
    Speedup: 2.95222
size: 8,000,000
    sum 1: 17,180,424,628,343,770
    sum 2: 17,180,424,628,343,770
      no OMP:     12,336us
    with OMP:      4,061us
    Speedup: 3.03768
size: 16,000,000
    sum 1: 34,358,679,947,677,733
    sum 2: 34,358,679,947,677,733
      no OMP:     23,917us
    with OMP:      7,805us
    Speedup: 3.06432
size: 32,000,000
    sum 1: 68,722,612,973,387,093
    sum 2: 68,722,612,973,387,093
      no OMP:     47,524us
    with OMP:     15,447us
    Speedup: 3.07658

几乎不管我设置的起始大小如何,在第一次运行时,OpenMP代码都非常慢,但随后的运行速度要快得多。
虽然很难100%确定,但我猜这是(至少在很大程度上)创建线程池以并行运行代码的时候了。完成后,使用OpenMP的后续运行将显示更接近您希望的结果:OpenMP版本通常比没有的版本快3倍左右。
免责声明:当然,我使用的CPU与你的不同(E5-2680 V4),这意味着它的内存也不同(DDR4与因此,在您的机器上,即使使用相同的代码,您的结果也可能不会遵循相同的模式。我当然不会期望它具有完全相同的比率,但我猜如果有足够大的阵列,您将从OpenMP获得超过10%的改进。根据Peter Cordes的评论,它可能会被证明是相同水平的改进(很少有人像Peter一样了解x86微架构的细节--而这少数人中的大多数为英特尔/AMD工作,不能和我们这些凡人谈论它)。

相关问题