为啥这种令人尴尬的并行算法的性能没有随着多线程而提高?

Posted

技术标签:

【中文标题】为啥这种令人尴尬的并行算法的性能没有随着多线程而提高?【英文标题】:Why does the performance of this embarrassingly parallel algorithm not improve with multi-threading?为什么这种令人尴尬的并行算法的性能没有随着多线程而提高? 【发布时间】:2016-01-07 01:13:05 【问题描述】:

这是我在这里的第一篇文章,虽然我确实定期访问该网站并在这里找到很多有价值的信息。

我有一个令人尴尬的并行算法,我预计它会通过多线程显示出巨大的性能改进。

这是我对多线程的第一次体验,经过大量阅读和审查。

我正在使用 VS 2012 使用 C++,我的 Windows 7 笔记本电脑有一个具有四个内核和大量内存的 i7 处理器。

基础工作分解为这个伪代码

for (int i = 0; i<iMax; i++)
    for (int j = 0; j<jMax; j++)
        T[j] += E[j][i] * SF;
    

T、E 和 SF 是浮点数。

该实现使用来自here 的(修改后的)线程池。

并从此函数为线程池构建和添加一堆任务

void doWork(float *T, float *E, float SF, int numNodes)

    // Critical for performance that these loops vectorize.....
    for (int nodeCounter = 0; nodeCounter < numNodes; nodeCounter++)
        T[nodeCounter] += E[nodeCounter] * SF;
    
;

使用这个结构,

tp.enqueue(std::bind(&doWork, timeStepDisplacements.T1, T1MODE, T1MPF, numNodes));

在我的测试中,numNodes 为 1,000,000,我为 50 个外部循环中的每一个调用此例程 3 次(使用不同的数组)。我也有另一个循环 (100) 在它的外面,所以我的测试代码生成 15,000 个这些任务,每个任务执行 1,000,000 次乘加。

编辑:将外部循环计数更正为 100,任务数从 7,500 到 15,000

当我使用 8、16 或更多线程设置我的线程池时,性能仅比串行代码略好 - 比如 8.8 秒与 9.3 秒。

所以我的问题是为什么性能提升如此之小?

注意 - 如果使用不同的任务例程(下面的 work_proc),相同的线程池设置会显示出巨大的性能提升。

void work_proc()

    int i = 555;
    std::random_device rd;
    std::mt19937 rng(rd());

    // build a vector of random numbers
    std::vector<int> data;
    data.reserve(100000);
    std::generate_n(std::back_inserter(data), data.capacity(), [&]() return rng(); );
    std::sort(data.begin(), data.end());

我可以毫无问题地发布整个代码 - 但我想我会从这些关键部分开始。

提前感谢您提供的任何见解。

【问题讨论】:

在内部循环中增加j 可能意味着很多缓存未命中。也许尝试重构循环以使缓存更友好。 您的操作系统是在不同的内核还是在同一个内核上运行线程? 每个内核是否有单独的浮点处理器或硬件辅助? Jonathan - 实际实现使用一维数组,旨在确保我获得良好的循环矢量化。 请尽量提供一个完整的例子,我们可以编译运行。我发现在这么少的背景下很难对此进行推理。 【参考方案1】:

你可能忽略了一些重要的部分,但如果你的伪代码是准确的,那么看起来瓶颈是内存访问。

单核可以足够快地添加数字,以充分利用您的 DRAM,因此拆分该工作不会获得太多性能。

编辑:如果您知道您的 DRAM 类型和 I/O 时钟速率,您可以计算您的 DRAM 传输速率。是关于它的速度吗?

例如:15000*1000000 在 9.3 秒内浮动,读取速度为 6.4 GB/s。如果您写入的数量相同,则为 12.8 GB/s,这是您说您在 cmets 中使用的 DDR3-1600 的最大速率...

所以这肯定是你的问题。

请注意,您不应该真的需要写入相同的数量,因此,如果您重新构建算法以使缓存更友好,您可能会使其在您的机器上几乎快两倍。

如果你让每个工人做 4 个 E,比如:

T[nodeCounter] += (E1[nodeCounter] + E2[nodeCounter] + E3[nodeCounter] + E4[nodeCounter])*SF

那么这将显着降低您的 T 带宽,并使您非常接近最大速度。

【讨论】:

Matt - 现在想弄清楚我的硬件规格,CPU 是 2.5GHz 的 i7-4710MQ。我在哪里找到其他信息并不明显 - 但我正在寻找...... 内存为 DDR3L @ 1600 MHz 你能用 [nodecounter&255] 代替 [nodecounter] 进行测试吗?这将消除任何内存带宽问题(虽然它可能会引入争用,所以尝试使用 1 和 2 个线程) Matt - 我会考虑重组以提高缓存友好性 - 尽管不幸的是我还有其他竞争考虑......但我认为我可以重组。问题是我看不到为 E 和 T 获得更好缓存的方法。 看起来你对 E 无能为力,因为它很大,你只需要阅读每个部分一次。但是您可以确保您不会多次覆盖 T[s] ......或者至少确保您在一个被踢出缓存之前完成所有写入。

以上是关于为啥这种令人尴尬的并行算法的性能没有随着多线程而提高?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Python 多处理解决令人尴尬的并行问题

为啥我没有看到通过 Python 中的多处理加速?

python中令人尴尬的并行问题

许多内核上令人尴尬的并行工作扩展性差

JAVA并行框架学习之ForkJoin

JAVA并行框架学习之ForkJoin