跳转到主要内容

4.8 谬误与陷阱

从程序员的角度看,DLP 是仅次于 ILP 的最容易利用的并行性;从体系结构设计者的角度看,它也可能是最简单的一类。不过,其中仍有许多谬误与陷阱。

谬误:GPU 作为协处理器,处于不利地位

主存与 GPU 存储器相分离固然有缺点,但与 CPU 保持一定距离也有优势。

例如,PTX 的存在,部分原因就在于 GPU 具有 I/O 设备的性质。编译器与硬件之间的这一间接层,让 GPU 体系结构设计者拥有远大于系统处理器设计者的灵活性。我们往往很难事先知道,一项体系结构创新能否获得编译器和程序库的良好支持,以及它对应用是否重要。有时,一种新机制可能只在一两代产品中有用,随后便随着 IT 世界的变化而不再重要。PTX 允许 GPU 设计者探索性地尝试创新,并在效果不佳或重要性下降时,在后续几代中将其移除,从而鼓励了实验。对于系统处理器,纳入新特性所需的论据自然要充分得多,因而能进行的实验也少得多:发布二进制机器码,通常就意味着这一体系结构今后的所有代际都必须支持这些新特性。

PTX 的价值已有实例证明:不同代的体系结构曾彻底改变硬件指令集,从类似 x86 的面向存储器的形式,改为类似 RISC-V 的面向寄存器的形式,并把地址宽度加倍至 64 位,却没有破坏 NVIDIA 的软件栈。

陷阱:只关注向量体系结构的峰值性能,忽略启动开销

早期存储器—存储器型向量处理器,例如 TI ASC 和 CDC STAR-100,启动时间很长。对于某些向量问题,向量长度必须超过 100,向量代码才会比标量代码快!由 STAR-100 派生的 CYBER 205,执行 DAXPY 的启动开销为 158 个时钟周期,使向量与标量性能持平所需的向量长度明显增加。如果 Cray-1 与 CYBER 205 的时钟频率相同,那么在向量长度超过 64 以前,Cray-1 都更快。实际上,Cray-1 的时钟频率也更高,尽管 205 推出的时间更晚,因此两者性能交叉时的向量长度超过了 100。

陷阱:提升向量性能,却没有相应提升标量性能

这种不平衡是许多早期向量处理器的问题,也是 Cray 计算机的设计者 Seymour Cray 改写既有规则的领域。许多早期向量处理器的标量单元相当慢,启动开销也很大。即使在今天,向量性能较低、但标量性能较好的处理器,也可能胜过峰值向量性能更高的处理器。良好的标量性能能够压低各种开销,例如 strip mining,并减轻 Amdahl 定律的影响。

一个很好的例子是,比较一台高速标量处理器和一台标量性能较低的向量处理器。Livermore Fortran 内核包含 24 个科学计算内核,各自的可向量化程度不同。图 4.37 给出了两个不同处理器在这套基准上的性能。尽管向量处理器的峰值性能更高,但以调和平均值衡量时,较低的标量性能使它慢于高速标量处理器。

今天,这种危险的另一种表现,是只提高向量性能——例如增加通道数——却不提高标量性能。这种短视也会把计算机引向不平衡的设计。

下一个谬误与此密切相关。

处理器所有循环中的最低速率(MFLOPS)所有循环中的最高速率(MFLOPS)全部 24 个循环的调和平均值(MFLOPS)
MIPS M/120-50.803.891.85
Stardent-15000.4110.081.72
图 4.37 两个不同处理器运行 Livermore Fortran 内核的性能测量。MIPS M/120-5 与 Stardent-1500(原名 Ardent Titan-1)都使用 16.7 MHz 的 MIPS R2000 芯片作为主 CPU。Stardent-1500 使用向量单元执行标量浮点运算,其标量性能按最低速率衡量,约为使用 MIPS R2010 浮点芯片的 MIPS M/120-5 的一半。对于高度可向量化的循环,向量处理器的最高速率超过后者的 2.5 倍。不过,以全部 24 个循环的调和平均值衡量总性能时,Stardent-1500 较低的标量性能抵消了较高向量性能的优势。

谬误:不提供足够的存储器带宽,也能获得良好的向量性能

从 DAXPY 循环与 Roofline 模型中可以看到,存储器带宽对所有 SIMD 体系结构都十分重要。DAXPY 每次浮点运算需要 1.5 次存储器访问,这也是许多科学计算程序的典型比例。即使浮点运算完全不花时间,Cray-1 也无法提高所用向量指令序列的性能,因为它受存储器限制。当编译器通过分块改变计算,使数据能够保留在向量寄存器中时,Cray-1 的 Linpack 性能出现了飞跃。这种方法降低了每 FLOP 对应的存储器访问次数,使性能提高到接近原来的两倍!对于原先需要更高带宽的循环,此时 Cray-1 的存储器带宽便足够了。

谬误:GPU 的存储器性能不足时,只要增加线程就行

GPU 使用大量 CUDA 线程来隐藏访问主存的延迟。如果各 CUDA 线程的存储器访问十分分散,或彼此缺少关联,存储器系统响应每个独立请求的速度就会越来越慢。最终,即使线程很多,也无法掩盖这些延迟。因此,要让“增加 CUDA 线程”的策略奏效,不仅需要大量 CUDA 线程,这些线程的存储器访问还必须具有良好的局部性。

表格阅读