数组C[]=A[]*B[],用于高性能计算

Array C[]=A[]*B[] in high-performance calculation

本文关键字:用于 高性能 计算 数组      更新时间:2023-10-16

我相信在C++中有这样的代码是很常见的

for(size_t i=0;i<ARRAY_SIZE;++i)
    A[i]=B[i]*C[i];

一个通常提倡的替代方案是:

double* pA=A,pB=B,pC=C;
for(size_t i=0;i<ARRAY_SIZE;++i)
    *pA++=(*pB++)*(*pC++);

我想知道的是,改进该代码的最佳方式,因为IMO需要考虑以下事项:

  1. CPU缓存。CPU如何填充缓存以获得最佳命中率
  2. 我想苏格兰和南方能源公司可以改善这一点吗
  3. 另一件事是,如果代码可以并行化呢?例如,使用OpenMP。在这种情况下,指针技巧可能不可用

如有任何建议,我们将不胜感激!

My g++4.5.2为两个循环生成完全相同的代码(修复了double *pA=A, *pB=B, *pC=C;中的错误,它是

.L3:
    movapd  B(%rax), %xmm0
    mulpd   C(%rax), %xmm0
    movapd  %xmm0, A(%rax)
    addq    $16, %rax
    cmpq    $80000, %rax
    jne .L3

(我的ARRAY_SIZE是10000)

编译器的作者已经知道这些技巧了。不过,OpenMP和其他并发解决方案值得研究。

性能规则是

  1. 尚未

  2. 获取目标

  3. 测量

  4. 了解有多少改进是可能的,并验证花时间来实现它是值得的。

对于现代处理器来说更是如此。关于您的问题:

  1. 简单的索引到指针映射通常由编译器完成,如果他们不这样做,他们可能有充分的理由。

  2. 处理器通常已经针对顺序访问缓存进行了优化:简单的代码生成通常会提供最佳性能。

  3. 苏格兰和南方能源公司或许可以改善这一点。但如果您已经受到带宽限制,则不会这样做。因此,我们回到的测量和确定边界阶段

  4. 并行化:和SSE一样。如果您的带宽有限,那么使用单个处理器的多个核心将没有帮助。根据存储器体系结构的不同,使用不同的处理器可能会有所帮助。

  5. 手动循环展开(在现已删除的答案中建议)通常是个坏主意。编译器知道如何在有价值的时候做到这一点(例如,如果它可以进行软件管道),而对于现代OOO处理器,情况往往并非如此(它增加了指令和跟踪缓存的压力,而OOO执行、对跳跃的猜测和寄存器重命名将自动带来展开和软件管道的大部分好处)。

第一种形式正是编译器将识别和优化的那种结构,几乎可以肯定会自动发出SSE指令。

对于这种琐碎的内部循环,缓存效果是无关紧要的,因为您正在遍历所有内容。如果您有嵌套循环或一系列操作(如g(f(a,B),C)),那么您可能会尝试安排重复访问小块内存,以更方便缓存。

不要用手展开循环。如果这是一个好主意(可能不是在现代CPU上),您的编译器也会这样做。

如果循环很大,并且其中的操作足够复杂,以至于您还没有内存绑定,那么OpenMP可能会有所帮助。

一般来说,以自然而直接的方式编写代码,因为这是优化编译器最容易理解的。

何时开始考虑SSE或OpenMP?如果这两者都是真的:

  • 如果您发现与您的代码相似的代码在您的项目中出现20次或更多:
    for (size_t i = 0; i < ARRAY_SIZE; ++i)
    A[i] = B[i] * C[i];
    或一些类似的操作
  • 如果ARRAY_SIZE通常大于1000万,或者如果profiler告诉您此操作正在成为瓶颈

然后,

  • 首先,将其变成一个函数:
    void array_mul(double* pa, const double* pb, const double* pc, size_t count)
    { for (...) }
  • 其次,如果您能够找到合适的SIMD库,请更改函数以使用它。
    • 良好的便携式SIMD库
    • SIMD C++库

顺便说一句,如果你有很多操作只比这稍微复杂一点,例如A[i] = B[i] * C[i] + D[i],那么支持表达式模板的库也会很有用。

您可以使用一些简单的并行化方法。Cuda将依赖于硬件,但SSE几乎是每个CPU的标准配置。还可以使用多个线程。在多线程中,您仍然可以使用指针技巧,这不是很重要。编译器也可以完成这些简单的优化。如果您使用的是Visual Studio 2010,则可以使用parallel_invoke并行执行函数,而无需处理windows线程。在Linux中,pThread库非常容易使用。

我认为使用valarrays是专门用于此类计算的。我不确定它是否能提高性能。