程序中至少有三种不同的并行方式:
指令级并行性: 可以重叠在同一线程内执行的不同指令的部分工作,保持运行每个指令一个接一个的幻觉。通过构建流水线CPU核心、超标量(multiple instructions per clock),甚至乱序执行来利用它。 (有关详细信息,请参见我在关于此问题的回答)。
创建软件时:尽可能避免长依赖链,将此并行性暴露给硬件。(例如,使用多个累加器展开sum += a [i ++]
,而不是sum1+=a[i]; sum2+=a[i+1]; i+=2;
:)或者使用数组而不是链接列表,因为要加载的下一个地址可以便宜地计算,而不是成为您在缓存未命中时必须等待的内存数据的一部分。但是大多数情况下,ILP已经存在于“正常”代码中,无需进行任何特殊处理,您可以构建更大/更高级的硬件来找到更多的ILP,并增加每个时钟的平均指令数。
数据并行性: 您需要对图像的每个像素或音频文件中的每个样本执行相同的操作。(例如,混合2个图像或混合两个音频流)。通过在每个CPU核心中构建并行执行单元来利用此功能,因此单个指令可以并行执行16个单字节加法,从而使您的吞吐量增加而不需要增加每个时钟要通过CPU核心的指令数量。这就是SIMD:单指令多数据。
音频/视频是最知名的应用程序,其中速度提升是巨大的,因为您可以将许多字节或16位元素放入单个固定宽度的向量寄存器中。
使用智能编译器自动矢量化循环,或手动利用SIMD。 SIMD将sum += a[i];
转换为sum[0..3] += a[i+0..3]
(对于每个向量4个元素,例如具有32位向量的int
或float
)。
线程/任务级并行性: 利用多核CPU,通过手动编写多线程代码或使用OpenMP或其他自动并行化工具将循环多线程化,或使用启动多个线程的库函数进行大型矩阵乘法等操作来暴露给硬件。
或者更简单地同时运行多个单独的程序。例如,使用make -j8
编译,以保持8个编译进程同时运行。粗粒度的任务级并行性也可以通过在多台计算机上运行工作负载来利用,甚至是分布式计算。
但是,多核CPU使得可能/有效地利用细粒度的线程级并行性,其中任务需要共享大量数据(例如大型数组),或通过共享内存进行低延迟通信。(例如,使用锁来保护共享数据的不同部分,或无锁编程)
float
数组:vaddps
指令的延迟为4个周期,吞吐量为0.5个周期(即每个时钟2个)。因此,8个累加器刚好足以隐藏该延迟并保持8个FP加法指令同时进行。sum += a[i++]
的总吞吐量增益是所有加速因子的乘积:4 * 8 * 8
= 非并行化、非向量化、单累加器ILP瓶颈天真实现的吞吐量的256倍,例如你从简单循环中得到的gcc -O2
。 clang -O3 -march=native -ffast-math
将提供SIMD和一些ILP(因为clang知道如何在展开时使用多个累加器,通常使用4个,而不像gcc。)是的,它确实有用。但仅从营销角度来看。如果没有SIMD指令,将很难销售uP或uC。