每个核心都有自己独立的SIMD执行单元。在一个内核中使用 SIMD 指令不会消耗其他内核中的执行资源。即使在同一物理芯片上的单独内核也是独立的,因此它们可以单独进入睡眠状态以节省电量,以及保持它们隔离的各种其他设计原因。
我知道的一个例外是:AMD Bulldozer 有两个弱整数核心,共享一个 SIMD/FPU 并共享一些缓存。他们称之为“集群”,它基本上是超线程 (SMT) 的替代方案。看大卫·坎特 (David Kanter) 在 RealworldTech 上的推土机文章 https://www.realworldtech.com/bulldozer/.
SIMD 和多核是正交的:您可以拥有多核而无需 SIMD(可能某些 ARM 芯片没有 FPU/NEON),也可以拥有 SIMD 而无需多核。
后者的许多例子,包括最著名的早期 x86 芯片,如 Pentium-MMX 到 Pentium III / Pentium 4,它们具有 MMX / SSE1 / SSE2,但都是单核 CPU。
程序中至少存在三种不同类型的并行性:
-
指令级并行性 https://en.wikipedia.org/wiki/Instruction-level_parallelism:可以在同一个执行线程中重叠不同指令完成的一些工作,从而保留逐条运行每条指令的错觉。通过构建流水线 CPU 核心或超标量(每个时钟多条指令),甚至乱序执行来利用它。 (看我对此问题的回答 https://softwareengineering.stackexchange.com/questions/349972/how-does-a-single-thread-run-on-multiple-cores/350024#350024了解详情。)
创建软件时:通过尽可能避免长依赖链来向硬件公开这种并行性。 (例如替换sum += a[i++]
with sum1+=a[i]; sum2+=a[i+1]; i+=2;
:使用多个累加器展开)。或者使用数组而不是链表,因为要加载的下一个地址的计算成本很低,而不是成为内存中数据的一部分,您必须等待缓存未命中。但大多数 ILP 已经存在于“正常”代码中,无需执行任何特殊操作,并且您可以构建更大/更高级的硬件来找到更多它,并增加每个时钟的平均指令数。
-
数据并行性 https://en.wikipedia.org/wiki/Data_parallelism: 你需要做的same图像的每个像素或音频文件中的每个样本。 (例如混合 2 个图像,或混合两个音频流)。通过在每个 CPU 核心中构建并行执行单元来利用这一点因此,一条指令可以并行执行 16 个单字节加法,从而提高吞吐量,而无需增加每个时钟通过 CPU 内核所需的指令量。这是SIMD:单指令,多数据。
音频/视频是最著名的应用,其中加速是massive因为您可以将大量字节或 16 位元素放入单个固定宽度向量寄存器中。
通过使用智能编译器自动向量化循环来利用 SIMD,或手动。 SIMD 转数sum += a[i];
into sum[0..3] += a[i+0..3]
(对于每个向量 4 个元素,例如int
or float
与 32 位向量)。
-
线程/任务级并行性 https://en.wikipedia.org/wiki/Task_parallelism:利用多核CPU,通过手动编写多线程代码暴露给硬件,或者使用OpenMP或其他自动并行化工具对循环进行多线程处理,或者使用启动多个线程进行大矩阵乘法的库函数或者其他的东西。
或者更简单地通过一次运行多个单独的程序。例如编译用make -j8
使 8 个编译进程同时运行。还可以通过在多台计算机集群甚至分布式计算上运行工作负载来利用粗粒度任务级并行性。
但是,多核 CPU 使得利用细粒度线程级并行性成为可能/高效,其中任务需要共享大量数据(如大型数组),或者通过共享内存进行低延迟通信。 (例如,使用锁来保护共享数据的不同部分,或无锁编程。)
这三种并行性是正交的。
总结一个非常大的数组float
在现代 CPU 上:
您将为每个 CPU 核心启动一个线程,并让每个核心循环访问共享内存中的数组块。 (线程级并行性)。比方说,这可以使您的速度提高 4 倍。 (即使由于内存瓶颈,这可能是不现实的,但您可以想象一些其他不需要读取这么多内存的计算密集型任务,在 28 核 Xeon 或具有其中两个芯片的双插槽服务器上运行。 .)
每个线程的代码将使用 SIMD 在每个内核上分别执行每条指令 4 或 8 次加法。 (SIMD)。这将为您带来 4 或 8 倍的加速。 (或 AVX512 为 16)
您可以使用 8 个向量累加器展开来隐藏浮点加法的延迟。 (ILP)。天湖的vaddps
指令的延迟为 4 个周期,吞吐量为 0.5 个周期(即每个时钟 2 个周期)。因此,8 个累加器勉强足以隐藏该延迟并同时保持 8 个 FP 添加指令运行。
相对于单线程标量的总吞吐量增益sum += a[i++]
是所有这些加速因素的乘积: 4 * 8 * 8
= 256x 非并行化、非向量化、单累加器 ILP 瓶颈幼稚实现的吞吐量,就像您从gcc -O2
一个简单的循环。clang -O3 -march=native -ffast-math
会给出 SIMD 和一些 ILP(因为 clang 知道如何在展开时使用多个累加器,通常使用 4 个累加器,与 gcc 不同。)
您需要 OpenMP 或其他自动并行化来利用多个内核。
有关的:为什么mulss在Haswell上只需要3个周期,与Agner的指令表不同? https://stackoverflow.com/questions/45113527/why-does-mulss-take-only-3-cycles-on-haswell-different-from-agners-instruction更深入地了解 ILP 和 SIMD 的多个累加器以及 FMA 循环。