第1章 编程模型
All problems in computer science can be solved by another level of indirection, except for the problem of too many layers of indirection.
计算机科学中几乎所有的难题都可以通过增加中间层解决,但计算机科学的难题在于已经有了太多中间层。—— David Wheeler[1]
软件定义芯片与 ASIC的*大区别在于,软件定义芯片需要像通用处理器那样执行用户编写的软件。 ASIC仅针对特定应用,它只需提供专用的 API,无须考虑程序员如何对其进行编程的问题。而软件定义芯片的功能,*终是靠程序员来实现的。一套硬件能吸引大量用户投入精力开发软件的一个必要条件是硬件上的软件是向前兼容的:即使新一代的硬件设计发生了翻天覆地的变化,之前用户编写的软件依然可以在新的芯片上正确运行。软件与硬件间进行对话的“语言”即编程模型。
广义的编程模型是指从应用到芯片之间的所有抽象层次。通用处理器芯片在漫长的发展过程中,逐渐形成了由编程语言、编译器中间表示、指令集架构等抽象层次构成的复杂的层次化中间层(indirection)模型。在这些模型中,上层中间层依次掩藏下层中间层的复杂性。例如,为了掩藏指令集架构层指令计数器可以任意跳转(如 x86指令集中的 jump类指令)所带来的复杂流程控制,编程语言层提供了多种流程控制语句,如 C语言中的语句。这样程序员在开发应用时,只需要面向特定中间层开发应用,而无须考虑底层实现的复杂性。
然而,作为一种与通用处理器和 ASIC在芯片架构和编程模型设计上都不同的新兴计算架构,软件定义芯片的编程模型面临着“先有鸡还是先有蛋”的困境:没有软件定义芯片的编程模型,软件定义芯片的芯片设计如“无源之水”,缺少指引芯片设计方向的软件;没有软件定义芯片的芯片设计,软件定义芯片的编程模型设计如“无本之木”,缺少检验编程模型有效性的硬件。
为了破解这个僵局,本章将回溯现代通用处理器体系结构和编程模型协同演化的历程。1.1节详细分析僵局的成因和影响。1.2节考察现代编程模型的中间层结构,然后从中归纳出三种编程模型的设计路线。1.3节考察芯片设计和编程模型应当如何应对半导体器件工艺发展不平衡带来的“三堵高墙”,即“内存墙”、“功耗墙”和“I/O墙”。越来越复杂的硬件催生了五花八门的编程模型。1.4节将从编程模型的演化历程总结“编程模型三元悖论”:新的编程模型无法同时获得高通用性、高开发效率和高执行效率,*多只能同时实现两个目标,而放弃另一个目标。结合计算系统抽象层次对硬件复杂性的处理方法,可以经验性地说明三元悖论的合理性。*后,1.5节基于“三元悖论”,针对软件定义芯片的编程模型困境提出三个可能的研究方向。
1.1 软件定义芯片的编程模型困境
在过去60年里,人类创造了一个指数增长的奇观:芯片性能持续指数增长,芯片之上的应用愈发复杂多样。编程模型作为芯片与应用之间的契约,借由契约的前后一致性,确保了过去的应用可以方便地移植到未来的芯片上。但摩尔定律的终结,如釜底抽薪,破坏了计算产业的奇观。对于软件定义芯片,必须将芯片设计从旧的契约中解放出来,重新思考芯片、编程模型和应用的关系。
没有软件定义芯片编程模型,软件定义芯片的体系结构设计如“无源之水”,缺少指引硬件设计方向的软件。体系结构研究中,对硬件范式转换*直接的应对方法是发明一种新的(领域定制)编程模型。尽管新编程模型在短期内很有吸引力,但这通常意味着程序员必须重新编写代码,并且会给软件开发团队带来严重的理解、交流障碍,令学习曲线变得陡峭。而在硬件架构快速迭代的阶段,直接花费大量人力物力,针对不断演化的体系结构设计开发自动化的编译器也不现实。这导致在设计新硬件范式时,目标应用难以对硬件设计中的决策进行快速响应,因而通常面临无软件可用的困境。
没有软件定义芯片体系结构,软件定义芯片的编程模型设计如“无本之木”,缺少给编程模型开发提供着力点的硬件。编程模型的作用是掩藏复杂的硬件机制。在摩尔定律对增强通用处理器性能还有效的时代,编程模型的设计远比今天简单。虽然处理器的硬件机制可能在代际之间发生巨变,但是新一代处理器的指令集架构(instruction set architecture,ISA)只需要增加少数几条或几类指令。因此,上一代的编程模型、编译器和编程语言只需要做少许改动便可应用在新一代处理器上。然而,随着摩尔定律在增强处理器性能方向上的失效,定制化成为新一代硬件*重要的性能来源。这些定制化硬件很难用一套统一的或者类似的 ISA进行抽象。所以,不同的新硬件都需要不同的编程模型。在新兴硬件范式还没有定型之时,编程模型难以明确到底要掩藏哪些硬件机制。
如果无法解决这个“先有鸡或者先有蛋”的悖论,软件定义芯片的发展将会面临两种结局,即要么由软件无法适应而导致硬件发展停滞,要么软件无法利用硬件进行创新。为了打破这种僵局,需要从根本上重新思考如何设计、编程和使用软件定义芯片。
我们相信,通过回溯现代通用处理器体系结构和编程模型协同演化的历程,对历史经验进行反思和概念探讨,可以克服常识的零散和碎片化,进而更为连贯一致地理解软件定义芯片编程模型的设计方法。
1.2 三条路线
正如本章引言所述,中间层是计算行业增长和生产力进步的主要驱动力。如今大多数计算机专业从业者可能都不知道现代微处理器的工作原理和半导体制程的工艺流程。但是通过维护这些层层相扣的中间层,计算机专业从业者可以在更高的抽象层次,如使用 Python,高效率地进行编码工作。由此才使得今天的应用得以百花齐放。
图1-1展示了当代计算行业中,自顶(应用)向下(芯片)的典型中间层。按照传统的软硬件划分方法,自 ISA以上是软件, ISA以下是硬件。越靠上的中间层抽象层次越高,程序的开发效率越高;越靠下的中间层复杂度越高,程序的执行效率越高。引入新的中间层的目的,就是要掩藏其下方中间层的复杂性,从而提高开发效率。
图1-1 计算机科学中自顶(应用)向下(芯片)的典型中间层示意
如果说整个计算行业琳琅满目的应用像是鳞次栉比的高楼大厦,那么每一个中间层就是一层楼,编程模型就是黏合这些琼台玉宇的水泥。狭义的编程模型是指从应用层到微架构层中,层与层之间的契约。具体而言,编程模型规定了上层的哪些行为合法,以及每个行为在下一层的执行机制。微架构层到物理层中同样存在类似的契约,如寄存器传输层到器件层之间使用网表文件作为契约。由于应用开发者不会与这些契约打交道,因此它们不属于本章讨论的编程模型范畴。
但是,正如本章引言的后半句所言,过多的中间层是一个难以解决的问题。这里的一个关键问题在于,每一个中间层的引入都会对应用在芯片上的性能造成损失。中间层越多,性能损失越大。因此,抽象层次极高的编程语言,如 Python、JavaScript等,主要的设计目标都是提高开发效率和扩大应用范围。为了达到这两个目标,高抽象层次语言具有许多共同的特征,例如,通常被单线程地解释,以及具有基于引用计数等简单算法的垃圾回收机制等。因为这些特征,高抽象层次语言的执行效率极为低下。2020年《科学》杂志刊登了一篇计算机体系结构的论文《顶部还有足够的空间》[2],其中的一个例子表明,使用 Python编写的矩阵乘法程序的运行时间是同等水平开发者使用高度优化的 C语言编写的程序运行时间的100~60000倍,如表1-1所示。不仅如此,高抽象层次语言执行时也需要更大的内存。例如, Python中的整数占用的是24个字节,而不是 C语言的4个字节(因为每个对象都携带类型信息、引用计数等),而列表或字典等数据结构的内存开销则是 C++开销的4倍以上。当然,这些高抽象层次语言的设计目标并不是高效地利用硬件。但当芯片的性能不再随着摩尔定律的前进而增长时,高抽象层次语言和高性能语言之间的执行效率差距就成为尚未充分发掘的金矿。
表1-1 不同程序执行4096×4096矩阵乘法运算的加速对比[2]
注:每个版本代表了一种对 Python源码的连续改进。运行时间是该版本的运行时间。 GFLOPS是该版本每秒执行64位浮点操作的次数(单位为十亿)。绝对加速是相对 Python的速度,而在展示中有附加精度位数的相对加速则是相比前一版本的加速。峰值占比是相比于计算机的835 GFLOPS的占比。
根据开发过程中开发者主要使用哪个中间层,将计算产业的从业者大致分为四种类型(图1-1):硬件开发者,负责设计电路和制造芯片,主要在电路的层次设计 ALU、高速缓存等模块;架构设计师,负责设计微架构 ISA,利用硬件开发者设计的模块搭建计算系统,并将计算系统的功能以 ISA或者 API的形式提供给上层开发者;编译设计师,根据应用需求和架构特性,负责设计编程语言和编译工具链,从而可以将应用开发者编写的应用自动地转化为目标架构可以执行的机器码;应用开发者,使用编程语言开发应用。参照前面的定义,编程模型可以看成应用开发者与硬件开发者进行对话的语言,该语言由架构设计师和编译设计师设计。
考虑到负责掩藏复杂硬件机制从业者类型的不同,可以简要地归纳出三种编程模型的设计路线。首先,有些硬件机制只需要交给架构设计师考虑,一般不需要编译器的干预。例如,当今流行的领域定制加速单元,通常都是由架构设计师提供一组简单的 API或者专用指令,供上层的编译器和应用开发者直接调用。其次,有些硬件机制可以交由编译设计师处理,而不需要让应用开发者了解。例如, CPU中成百上千个寄存器,都可以由编译器自动完成分配。*后,很多硬件机制的性能潜力,必须由应用开发者根据应用的需求编写程序才能被充分开发。例如,多线程处理器的并发执行机制需要应用开发者使用并行编程语言编写程序才能被充分利用。
三种设计路线给编程模型带来截然不同的特征。编程模型的发展历程就是这三条路线相互角力达到平衡的过程。下面将按照时间顺序回溯典型硬件机制的设计动机和编程方法。
1.3 三大障碍
Gene Amdahl因“Amdahl定律”[3]而举世闻名。这个定律指出,并行计算性能随着线程数的增加,边际收益递减。但 Amdahl于1967年同时提出了第二条原则[3],称为“ Amdahl经验法则”或“ Amdahl的另一条定律”:硬件架构设计需要平衡算力、内存带宽和 I/O带宽,理想的处理器计算性能、内存带宽与 I/O带宽的比例为1∶1∶1,即每秒百万条指令数(million instructions per second,MIPS)的处理器计算性能需要1MB的内存和1 Mbit/s的 I/O带宽。
图1-2 1980~2020年 CPU计算性能、内存带宽、磁盘带宽和网络带宽随时间的变化(当硬件性能错位的张力无法在之前的体系结构-编程模型的设计中得以解决时,计算系统遇到了“内存墙”、“功耗墙”和“ I/O墙”[4])(见彩图)
“Amdahl经验法则”在提出时曾被作为金科玉律,但时至今日已鲜为人知。其原因在于,自1985年,由于集成电路工艺的发展,计算系统的内存带宽与 I/O带宽的比例,无法与计算性能维持在1∶1∶1的理想比例。如图1-2所示,在不同的时间段, CPU计算性能、内存带宽、磁盘带宽和网络带宽的增速各有不同。如同地壳运动中两个板块间的错位会形成悬崖峭壁,计算系统中不同模块的性能错位也会形成一堵堵“高墙”。集成电路诞生60年后的今天,产业界和学术界公认三堵“高墙”分别为:1995年后内存性能和 CPU性能错位形成的“内存墙”(memory wall)、2005年后 CPU性能和芯片功耗错位形成的“功耗墙”(power wall)和2015年后 CPU性能和 I/O带宽错
展开