预计内存带宽利用率是多少?为什么多线程/多处理会让情况变得更糟?

问题描述 投票:0回答:1

我们有 C 代码,可以循环大型 (10M - 1000M) 双精度数组(32 位对齐)并聚合它们。代码(如下)看起来很简单,但速度是我们能得到的最快的。超过 20 种其他方法,从简单到非常复杂 - 使用 SIMD、多线程和/或多处理 - 最终都会导致执行速度变慢。任何智取编译器的尝试都失败了。我们的收获:相信编译器!不要混淆编译器!我们正在使用最新的 clang 以及所有相关的编译器标志 on-fire、-O3 等。

double sum_double(const double *restrict values, const size_t n) {
    double r = 0;
    #pragma omp simd reduction(+:r)
    for (size_t i = 0; i < n; i++)
        r += values[i];
    return r;
}

事情是这样的:在具有 100GB/s 带宽的基础 M2 Mac 上,我们可以达到最大。 65GB/s ≙ ±8B 64 位聚合/秒。作为参考,Numpy 达到了完全相同的吞吐量。

问题:

  • 为什么我们不能超越这个 65GB/s 的阈值?

    还剩下 35GB/s。 这只是相当于我们处理的时间吗?

  • 为什么多线程/处理也值得?

    我们预计多重处理至少相当于 单线程方法。但每次性能都会显着下降
    添加进程或线程?

对此有什么经验或看法吗?还是我们做错了什么?谢谢

c multithreading multitasking memory-bandwidth
1个回答
0
投票

在大多数平台上,只要操作正确向量化展开,此计算主要是内存限制

Here

#pragma omp simd reduction(+:r)
使用
-fopenmp
-fopenmp-simd
正确矢量化循环(问题中未提及,但对于任何 OpenMP 代码来说相当明显)。这是生成的 ARM 汇编循环:

.LBB0_5:
        ldp     q2, q3, [x9, #-16]
        subs    x10, x10, #4
        add     x9, x9, #32
        fadd    v1.2d, v1.2d, v2.2d
        fadd    v0.2d, v0.2d, v3.2d
        b.ne    .LBB0_5

问题是它肯定不是最佳的,因为fadd

指令的
延迟通常为 2-4 个周期(据我所知,Apple M1/M2 CPU 上为 3 个周期),而有 4 个 SIMD 单元可以执行每个周期。因此,如果代码尚未受内存限制,则它可能会受延迟限制。更糟糕的是:SIMD 单元可能会缺乏等待指令的时间!编译器不进一步优化循环的原因之一是 IEEE-754 标准禁止这样做。解决此问题的一种方法是使用 #pragma omp simd reduction(+:r) simdlen(8)
 告诉 OpenMP 使用更广泛的 SIMD 操作。这是生成的代码:

.LBB0_5: ldp q17, q16, [x9, #-64] subs x10, x10, #16 ldp q19, q18, [x9, #-32] ldp q20, q21, [x9] fadd v2.2d, v16.2d, v2.2d fadd v0.2d, v17.2d, v0.2d ldp q17, q16, [x9, #32] fadd v1.2d, v19.2d, v1.2d fadd v3.2d, v18.2d, v3.2d fadd v6.2d, v21.2d, v6.2d fadd v4.2d, v20.2d, v4.2d add x9, x9, #128 fadd v5.2d, v17.2d, v5.2d fadd v7.2d, v16.2d, v7.2d b.ne .LBB0_5
现在,CPU 可以并行执行更多指令,因为它们是

独立的:4 条指令/周期,而且它们还可以更好地流水线化。对于内存限制的代码来说,这应该还远远不够。

请注意,

-march=native

也可能有帮助。

最重要的是,一段代码可能不会使内存饱和,因为对硬件而言,使 DRAM 饱和并不那么容易。这需要大型缓冲区和大量晶体管,以减轻多个 LPDDR5 DRAM 的巨大(且可变)延迟。因此,某些 CPU 架构即使使用完美优化的代码,也无法仅用一个核心来使 DRAM 内存饱和。我的 Intel Skylake CPU 就是这种情况,其中 CPU 的 LFB(行填充缓冲区)单元是阻止这种情况的瓶颈。

对于此类代码来说这不是问题,因为它可以并行化,并且所有内核通常都足以使 DRAM 内存饱和。事实上,通常只需很少的就足够了。

为什么多线程/处理也值得?

如果线程被调度在高效的核心上,它们的速度可能会明显变慢。您应该检查此处是否正确使用了性能核心。这是 Apple M1/M2 甚至 Intel Alderlake(不太严重)等

big-little CPU 上的常见问题。 AFAIK,M1/M2“Pro”版本默认情况下不会发生这种情况。

© www.soinside.com 2019 - 2024. All rights reserved.