与 GPU 相比,为什么 CPU 中的 SIMD 编程水平如此之低?

计算科学 C++ 矢量化
2021-12-08 00:25:36

就我在 C++ 方面的经验来说——利用现代 CPU 的 SIMD 功能来处理更复杂的算法真的很棘手。如果我从高级 OO 的角度看到机会,那么我将不得不将整个事情分解为非常非 OO 类型的代码,单独照看每个寄存器,即使它们彼此之间没有交互,而不仅仅是部分这得益于矢量化,但通常必须将整个数据流以及数据格式重新设计为更基本且更难管理的形式。

自动矢量化工具 (qvec/openmp) 也没有任何好处,因为它们只能处理最基本的数据类型和循环,根本不支持 OO 概念,而且 imo 只能成功地掩盖代码并限制您通过手动方式的选择.

同时,在为 GPGPU 编码时,您基本上可以以完美的 OO 方式(或多/少,取决于语言)进行编码,同时专注于一段数据通过管道的过程,该管道会被复制到所有其他数据数据,精彩!

我知道 gpu“核心”与 cpu 上的 SIMD 单元不同,但从我收集的信息来看,它们比实际的单个核心更接近这一点,因为它们必须以一种锁步的方式移动。

所以基本上,这是为什么呢?

2个回答

在我尝试回答您的问题之前,让我评论一下您的问题陈述中的“低级别”一词。在我看来,我不想说一种编程模型(SIMD 和 SIMT 之一)处于低水平。举个例子,在我的本科文凭项目中,我遇到了许多关于 GPU 编程的低级问题,这是我在 CPU 编程中没有遇到的。这些问题大多涉及GPU的内存架构,例如内存合并访问,银行冲突等。您可以搜索这些主题以获得GPU编程的低级感。

现在回到你的问题,“低级”我假设你的意思是你必须重构你的代码,甚至重新设计你的算法以适应 SIMD 模型,而在 GPU(例如使用 CUDA)上它可能不需要太多的工作,比如那。在某些情况下,您的 CPU 源代码甚至可以使用 CUDA 编译器编译而不会出现任何错误。但是要在 SIMD 中对串行程序进行矢量化,您必须翻译所有分支代码(if,while 块)。

(我不太清楚你在问题陈述中所说的“OOP”是什么意思。如果我做出了错误的假设,请告诉我。)

原因是 GPU 编程 (CUDA) 使用的模型与 CPU 编程中使用的模型不同。GPU 编程的模型称为 SIMT。T 代表线程。

在 SIMD 模型中,所有算术运算都必须以同步方式执行。所以不允许任何分支执行。而在 CUDA 中,就 CUDA 语法而言,分支是允许的,更重要的是,它受到硬件架构和 CUDA 运行时的支持。当在 CUDA/SIMT 中发生分支执行时,线程管理器将协调执行:执行路径相同的线程将被执行,不同的线程将排队等待稍后执行。在此过程中,可能会引入其他独立线程以保持高吞吐量。

如您所见,SIMT 模型可帮助您处理分支执行。

最后,我想让您知道,您可以通过引入另一个语法层在“高级”中对 SIMD 进行编程。你可以试试https://ispc.github.io/

我认为您正在将使用基于 GPGPU 技术(如 C++ AMP)构建的高级库与在可能的最低汇编语言或内在函数级别上编程 SIMD 进行比较。这不是一个公平的比较。

由于您特别提到了 C++ AMP,所以让我用一个基本示例来证明您的前提不正确。介绍性 AMP 示例展示了如何使用 AMP 并行化以下简单向量加法

#include <iostream>

void StandardMethod() {

    int aCPP[] = {1, 2, 3, 4, 5};
    int bCPP[] = {6, 7, 8, 9, 10};
    int sumCPP[5];

    for (int idx = 0; idx < 5; idx++)
    {
        sumCPP[idx] = aCPP[idx] + bCPP[idx];
    }

    for (int idx = 0; idx < 5; idx++)
    {
        std::cout << sumCPP[idx] << "\n";
    }
}

为此,您必须使用 AMP 库函数并以特定方式编写循环。如果我们想在 CPU 上使用 SIMD 指令并行化这个循环怎么办?我们只需要使用相当现代的编译器和合适的标志来编译它!

在 Linux 上用 gcc 4.8.3 编译上述代码并反汇编,我得到

$ gcc -O3 -c vectest.cpp && objdump -M intel -d vectest.o 

vectest.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z14StandardMethodv>:
   0:   55                      push   rbp
   1:   53                      push   rbx
   2:   48 83 ec 68             sub    rsp,0x68
   6:   c7 04 24 01 00 00 00    mov    DWORD PTR [rsp],0x1
   d:   c7 44 24 04 02 00 00    mov    DWORD PTR [rsp+0x4],0x2
  14:   00 
  15:   48 8d 5c 24 40          lea    rbx,[rsp+0x40]
  1a:   c7 44 24 08 03 00 00    mov    DWORD PTR [rsp+0x8],0x3
  21:   00 
  22:   c7 44 24 0c 04 00 00    mov    DWORD PTR [rsp+0xc],0x4
  29:   00 
  2a:   48 8d 6c 24 54          lea    rbp,[rsp+0x54]
  2f:   66 0f 6f 04 24          movdqa xmm0,XMMWORD PTR [rsp]
  34:   c7 44 24 20 06 00 00    mov    DWORD PTR [rsp+0x20],0x6
  3b:   00 
  3c:   c7 44 24 24 07 00 00    mov    DWORD PTR [rsp+0x24],0x7
  43:   00 
  44:   c7 44 24 28 08 00 00    mov    DWORD PTR [rsp+0x28],0x8
  4b:   00 
  4c:   c7 44 24 2c 09 00 00    mov    DWORD PTR [rsp+0x2c],0x9
  53:   00 
  54:   66 0f fe 44 24 20       paddd  xmm0,XMMWORD PTR [rsp+0x20]
  5a:   66 0f 7f 44 24 40       movdqa XMMWORD PTR [rsp+0x40],xmm0
  60:   c7 44 24 10 05 00 00    mov    DWORD PTR [rsp+0x10],0x5
  67:   00 
  68:   c7 44 24 30 0a 00 00    mov    DWORD PTR [rsp+0x30],0xa
  6f:   00 
  70:   c7 44 24 50 0f 00 00    mov    DWORD PTR [rsp+0x50],0xf
  77:   00 
  78:   8b 33                   mov    esi,DWORD PTR [rbx]
  7a:   bf 00 00 00 00          mov    edi,0x0
  7f:   48 83 c3 04             add    rbx,0x4
  83:   e8 00 00 00 00          call   88 <_Z14StandardMethodv+0x88>
  88:   ba 01 00 00 00          mov    edx,0x1
  8d:   be 00 00 00 00          mov    esi,0x0
  92:   48 89 c7                mov    rdi,rax
  95:   e8 00 00 00 00          call   9a <_Z14StandardMethodv+0x9a>
  9a:   48 39 eb                cmp    rbx,rbp
  9d:   75 d9                   jne    78 <_Z14StandardMethodv+0x78>
  9f:   48 83 c4 68             add    rsp,0x68
  a3:   5b                      pop    rbx
  a4:   5d                      pop    rbp
  a5:   c3                      ret    

Disassembly of section .text.startup:

0000000000000000 <_GLOBAL__sub_I__Z14StandardMethodv>:
   0:   48 83 ec 08             sub    rsp,0x8
   4:   bf 00 00 00 00          mov    edi,0x0
   9:   e8 00 00 00 00          call   e <_GLOBAL__sub_I__Z14StandardMethodv+0xe>
   e:   ba 00 00 00 00          mov    edx,0x0
  13:   be 00 00 00 00          mov    esi,0x0
  18:   bf 00 00 00 00          mov    edi,0x0
  1d:   48 83 c4 08             add    rsp,0x8
  21:   e9 00 00 00 00          jmp    26 <_GLOBAL__sub_I__Z14StandardMethodv+0x26>

如您所见,编译器已自动使用 MMX 指令来优化您的循环,而无需添加任何 CPU 或库特定的注释。所以我想说你声称在高级代码中使用 SIMD 比使用 GPGPU 技术更困难的说法是不正确的——恰恰相反。