计算机体系结构中,将处理器和加速器归类为“通用”的趋势日益增长。在今年国际计算机体系结构研讨会(ISCA 2014)上发表的论文中,45篇论文中有9篇明确提到了通用处理器;另有一篇提到了通用FPGA(现场可编程门阵列),还有一篇提到了通用MIMD(多指令多数据)超级计算机,这已经将定义的范围扩大到了极限。本文提出论点,认为根本不存在真正的通用处理器,并且对这种设备的信念是有害的。
许多在ISCA 2014上发表的论文,即使没有明确提到通用处理器或内核,也提到了通用程序,通常是在GPGPU(通用图形处理器)的背景下,这是一个本身就存在矛盾的术语。
现代GPU具有I/O功能,可以运行任意大小的程序(或者,如果不能,可以存储临时结果并启动新的程序阶段),支持广泛的算术运算,具有复杂的流程控制等等。在GPU上实现康威生命游戏是学生们相当常见的练习,因此很明显,底层的基质是图灵完备的。
以下是通用处理器的一种定义:如果它可以运行任何算法,那么它就是通用的。这不是一个特别有趣的定义,因为它忽略了性能方面,而性能一直是大多数处理器发展的驱动目标。
因此,仅仅处理器是图灵完备的,还不足以将其归类为通用;它必须能够高效地运行所有程序。加速器(包括GPU)的存在表明,迄今为止构建通用处理器的所有尝试都失败了。如果它们成功了,那么它们在运行委托给加速器的算法时将是高效的,并且加速器市场将不复存在。
考虑到这一点,让我们探讨一下人们在提到通用处理器时真正指的是什么:这些设备优化的特定工作负载类别以及这些优化是什么。
通用处理器的一个常见要求是它可以运行通用操作系统——这意味着操作系统要么是Unix,要么(像MS Windows)具有与Unix类似的一组底层抽象。例如,大多数现代处理器缺乏清晰表达Multics中内存模型的能力,Multics具有细粒度的共享和对内存映射I/O设备的透明虚拟化访问。
运行操作系统的能力是公认定义的基础。如果你从被认为是通用的处理器中移除运行操作系统的能力,那么结果通常被称为微控制器。在多任务、保护模式操作系统成为核心要求之前,一些现在被认为是微控制器的设备曾被认为是通用CPU。
运行操作系统的重要要求(再次强调,它必须是高效的,而不仅仅是运行模拟器)与Gerald Popek和Robert Goldberg为虚拟化列举的要求相似。6 这并不奇怪,因为操作系统进程实际上是一个虚拟机,只不过具有非常高级的虚拟设备集(文件系统而不是磁盘,套接字而不是以太网控制器)。
为了满足这些要求,处理器需要区分特权指令和非特权指令,以及在非特权模式下执行特权指令时陷入特权模式的能力。此外,它需要实现受保护的内存,以便将非特权程序彼此隔离。这比Popek和Goldberg关于内存访问必须虚拟化的要求要弱一些,因为操作系统不一定需要给每个进程它拥有隔离线性地址空间的错觉。
传统上,所有与I/O相关的指令都是特权的。内存映射I/O区域相对于I/O端口的独立命名空间的一个优点是,它可以将直接访问硬件的权限委托给非特权程序。这在网络接口和GPU中越来越常见,但这要求硬件提供一些虚拟化支持(例如,隔离的命令队列,这些队列不能请求直接内存访问,以访问或来自另一个程序控制的内存)。
在MIPS中,特权指令被建模为由CP0(控制协处理器)实现。这可以追溯到微型计算机之前的模型,在微型计算机之前的模型中,CPU由少量芯片组成,而MMU(内存管理单元)通常放置在不同的芯片上(甚至是一组芯片上)。
在现代多核系统中,相当常见的做法是将一些内核几乎纯粹分配给用户空间应用程序,所有中断都从这些内核路由走,以便它们可以一直运行,直到另一个内核上的调度器确定它们应该停止。因此,尚不清楚多核系统中的每个处理器是否都需要能够运行所有内核代码。Cell微处理器3 中探索了这种模型,其中每个SPE(协同处理元件)都能够运行一个小的存根,当它需要操作系统功能时,该存根会向PowerPC内核发送消息。这种设计在超级计算机中相对常见,在超级计算机中,每个节点都包含一个运行操作系统和处理I/O的慢速商品处理器,以及一个针对HPC(高性能计算)任务优化的更快处理器,应用程序对该处理器具有独占控制权。
Barrelfish研究操作系统1 提出,随着从多核架构向众核架构的过渡,时间分复用对于虚拟化来说不如空间分复用有意义。与其一个内核运行多个应用程序,不如一个应用程序在其运行期间独占使用一个或多个内核。如果这种转变发生,它将大大改变要求。
RISC I处理器5 中使用的启发式方法,是通过检查Unix代码库上Portable C Compiler生成的代码发现的,即通用代码大约每七条指令包含一条分支指令。这一观察结果表明,为什么分支预测器是大多数现代超标量流水线CPU中如此重要的特性。例如,奔腾4一次可以有大约140条指令处于飞行状态,因此(如果这一观察结果是正确的)需要正确预测未来20个分支才能保持流水线满负荷运行。如果分支预测器的命中率为90%,那么它正确预测未来20个分支的概率仅为12%。
幸运的是,对于奔腾4来说,并非所有分支都具有相同的难度,CPU相关代码中最常见的分支是循环末尾的向后分支。奔腾4包含一个静态启发式方法,即该分支在首次使用时始终被采用,并且复杂的循环预测器将确定哪个迭代是最后一个迭代。
这一观察结果在很大程度上仍然适用于C代码。现在在很大程度上也适用于其他语言,但有一些注意事项。特别是,虽然它适用于C的朴素编译,但绝对不适用于Smalltalk或JavaScript等动态语言的朴素编译。
考虑简单表达式a+b。在C中,你静态地知道a和b的类型。该操作要么是整数加法,要么是浮点加法。现在考虑C++中相同的代码行。如果a是原始类型,那么这是一个简单的算术运算(尽管可能有一些复杂的代码,包括函数调用,以强制b转换为相同的类型)。如果a是一个类,它对加法运算符具有虚拟重载,那么它是一个通过vtable(虚函数表)查找的间接分支。编译器将在vtable中的固定偏移量处插入函数指针的加载,然后是跳转到加载地址的间接跳转。
在JavaScript中,这更加复杂。JavaScript中加法的确切语义非常复杂;让我们通过将所有非数字加法视为等效来简化。在数值情况下,JavaScript中的所有值都是双精度浮点值。然而,大多数处理器在执行整数运算时比浮点运算快得多,因此,理想情况下,所有适合53位整数(64位IEEE兼容双精度的尾数大小)的算术运算都应作为整数运算执行。不幸的是,要做到这一点,你必须首先检查操作数是整数值还是浮点值(这增加了另一个分支,并且通常比从执行整数运算获得的节省成本更高)。你必须先检查它是否是其他内容(对象),然后才能最终执行算术运算。
这意味着,在朴素编译的JavaScript中,一个简单的加法可能涉及两个分支。优化这一点一直是过去40年大量研究的重点(大多数技术适用于Smalltalk和类似语言,早于JavaScript)。这些技术涉及基于运行时获得类型信息的动态重编译代码;这确保了涉及最常见类型的情况不包含分支。
值得注意的是,SPARC架构具有标签整数类型,以及专门为动态语言设计的加法和减法指令。7 SPARC标签整数为30位,32位字中的最低两位保留用于类型信息。如果任何一个操作数具有非零标签,或者如果结果溢出,则标签操作会设置条件代码(或陷阱)。
这些指令并没有被广泛使用,原因有几个。它们使用的标签值定义与大多数32位实现相反(为指针使用0标签更简单,因为它允许指针值在不修改的情况下使用)。对于JavaScript,也常使用NaN(非数字)表示来区分对象指针和数值。这是可能的,因为大多数64位架构都有一个“内存空洞”——虚拟地址空间中间的一个范围,无法映射——这与某些NaN值的指针解释方便地对齐。这允许生成的代码假设值是浮点数,并为NaN值分支或陷阱。
尽管JavaScript作为一种通用编程语言非常流行,但大多数“通用”处理器仍然需要编译器执行复杂的卷积来生成适度高效的代码。重要的是,所有从JavaScript生成良好代码的技术都需要一个执行动态重编译的虚拟机。如果在一个处理器上运行通用代码最有效的方法是为不同的(虚构的)架构实现一个模拟器,那么很难论证底层基质是真正通用的。
大多数现代CPU共享的一个特性是它们的大型缓存。在Itanium和POWER的情况下,这种趋势已经超出了专有的范畴,但即使是商品x86芯片现在也比386或486拥有更多的RAM缓存。
缓存,与暂存存储器不同,(顾名思义)对软件是隐藏的,其存在是为了加速对主存储器的访问。只有当软件具有可预测的访问模式时,这种隐藏能力才有可能实现。最常用于CPU缓存的策略是针对引用局部性进行优化。缓存被分成通常为64或128字节的行。从主存储器加载单个字节将填充整个缓存行,从而使对相邻内存的访问变得廉价。通常,缓存在未命中时会填充多个相邻行,从而进一步优化这种情况。
这种设计是工作集假说的结果,该假说认为,计算的每个阶段都访问程序可用总内存的子集,并且这个集合变化相对缓慢。2 相比之下,GPU针对一组非常不同的算法进行了优化。例如,Nvidia Tesla GPU上的内存控制器具有少量固定的访问模式(包括一些递归的Z形,常用于存储体数据);它以这些模式在纹理中获取数据,并将其流式传输到处理元件。现代英特尔CPU也可以识别一些规则的访问模式并有效地流式传输内存,但吞吐量较低。
Jae Min Kim等人表明,某些程序在ARM big.LITTLE架构的慢速内核上运行得更快。4 他们的解释是,慢速Cortex A7内核对其一级缓存具有单周期访问,而快速Cortex A15内核具有四周期惩罚。这意味着,如果工作集假说成立——工作集适合L1缓存,并且性能由内存访问主导——那么A7将能够饱和其流水线,而A15将花费大部分时间等待数据。
这突出了一个事实,即即使在来自同一制造商和同一代的商品微处理器中,不同的缓存拓扑结构也会使性能偏向于特定类别的算法。
软件中的并行性以多种形式和粒度出现。对于大多数CPU来说,最重要的形式是ILP(指令级并行性)。超标量架构专门设计用于利用ILP。它们将架构指令编码转换为更类似于静态单赋值形式的东西(具有讽刺意味的是,编译器花费了大量精力将这种形式转换为有限寄存器编码),以便它们可以识别独立的指令并并行分派它们。
尽管ILP的理论极限可能非常高,高达150条独立指令8,但实际可提取的数量要低得多。VLIW(超长指令字)处理器通过使编译器的工作来识别ILP和捆绑指令来简化其逻辑。
VLIW方法的问题在于ILP是一个动态属性。回想一下,RISC I启发式方法是分支平均每七条指令发生一次。这意味着编译器最多可以提供七路ILP,因为它无法识别跨越基本块的ILP:如果编译器静态地知道分支的目标,那么它很可能不会插入分支。
VLIW处理器在DSP(数字信号处理器)和GPU中取得了成功,但在“通用”处理器优化的代码类型中却没有成功,尽管英特尔(i860、Itanium)进行了多次尝试。这意味着,在这些处理器预期运行的代码中,静态可预测的 ILP相对较低。超标量处理器没有相同的限制,因为与分支预测器结合使用,它们可以从跨越基本块甚至跨越函数的动态流程控制中提取ILP。
值得注意的是,尽管ARM Cortex A15(三发射、超标量、乱序执行)的芯片面积和功耗是A7(双发射、顺序执行)的四倍,但时钟频率相同时,其性能仅比A7高出75-100%,尽管(理论上)能够利用更多的ILP。
关于大多数CPU中并行性的另一个隐含假设是,并行执行线程之间的通信既不频繁又是隐式的。后一个属性来自C共享一切的并发模型,其中线程通信的唯一方式是修改一些共享状态。缓存一致性需要大量的逻辑才能使其高效,但这仅与共享内存并行性相关。
在这种处理器上实现消息传递(早期语言如Occam和Actor Smalltalk,以及较新的语言如Erlang和Go都体现了消息传递)通常涉及在内核之间反弹缓存行所有权并涉及大量总线流量的算法。
今天的通用处理器是高度专业化的,专为运行从低级类C语言编译的应用程序而设计。它们使用时间分复用进行虚拟化,包含大致每七条指令一次的大多数可预测分支,并表现出高度的引用局部性和低程度的细粒度并行性。虽然这描述了很多程序,但这绝非详尽无遗。
由于针对这些情况优化的处理器一直是消费者可以购买的最便宜的处理元件,因此许多算法已被迫表现出其中一些属性。随着廉价可编程GPU的出现,这种情况开始发生变化,自然地,数据并行算法越来越多地在GPU上运行。现在GPU的每FLOPS(每秒浮点运算次数)成本低于CPU,趋势越来越倾向于强制非自然数据并行的算法在GPU上运行。
暗硅问题(芯片中必须保持不通电的部分)意味着,在同一芯片上拥有许多不同的内核将变得越来越可行,只要它们中的大多数不是 постоянно 通电的。在这种世界中,高效的设计将需要承认不存在一刀切的处理器设计,并且存在一个很大的频谱,不同的权衡点对应不同的权衡。
1. Baumann, A., Barham, P., Dagand, P.-E., Harris, T. L., Isaacs, R., Peter, S., Roscoe, T., Schüpbach, A., Singhania, A. 2009. 多内核:可扩展多核系统的新操作系统架构。 SIGOPS第22届操作系统原理研讨会论文集:29-44。
2. Denning, P. J. 1968. 程序行为的工作集模型。通信 11(5): 323-333。
3. Gschwind, M., Hofstee, H. P., Flachs, B., Hopkins, M., Watanabe, Y., Yamazaki, T. 2006. Cell多核架构中的协同处理。IEEE Micro 26(2): 10-24。
4. Kim, J. M., Seo, S. K., Chung, S. W. 2014. 深入异构性:何时简单更快。第二届移动平台并行性国际研讨会。
5. Patterson, D. A., Sequin, C. H. 1981. RISC I:精简指令集VLSI计算机。第八届计算机体系结构年度研讨会论文集:443-457。
6. Popek, G. J., Goldberg, R. P. 1974. 可虚拟化第三代架构的形式化要求。通信 17(7): 412-421。
7. SPARC International Inc. 1992. SPARC架构手册:版本8。新泽西州上萨德尔河:Prentice-Hall Inc.
8. Wall, D. W. 1993. 指令级并行性的限制。技术报告,数字设备公司西部研究实验室。
喜欢还是讨厌?请告诉我们
David Chisnall 是剑桥大学的研究员,他在那里从事编程语言设计和实现工作。在完成博士学位并到达剑桥大学之间,他花了几年时间进行咨询,在此期间他还撰写了关于Xen以及Objective-C和Go编程语言的书籍,以及大量文章。他还为LLVM、Clang、FreeBSD、GNUstep和Étoilé开源项目做出了贡献,并且他跳阿根廷探戈。
© 2014 1542-7730/14/1000 $10.00
最初发表于Queue vol. 12, no. 10—
在数字图书馆中评论本文
David Chisnall - 如何设计ISA
在过去的十年中,我参与了几个项目,这些项目设计了ISA(指令集体系结构)扩展或针对各种处理器的全新ISA(你甚至可以在RISC-V规范的致谢中找到我的名字,可以追溯到第一个公开版本)。当我开始时,我对什么构成好的ISA知之甚少,而且据我所知,这在任何地方都没有正式教授。
Gabriel Falcao, João Dinis Ferreira - 选择PiM还是不选择PiM
随着人工智能成为数十亿边缘IoT(物联网)设备的普及工具,数据移动瓶颈对这些系统的性能和自主性施加了严格的限制。PiM(内存内处理)正在兴起,成为一种缓解数据移动瓶颈的方法,同时满足依赖CNN(卷积神经网络)的边缘成像应用的严格性能、能源效率和准确性要求。
Mohamed Zahran - 异构计算:大势所趋
在过去的几年中,对流行词异构计算的提及一直在增加,并且将在未来几年继续被听到,因为异构计算是大势所趋。什么是异构计算,为什么它正在成为常态?我们如何从软件端和硬件端处理它?本文为其中一些问题提供了答案,并对其他问题提出了不同的观点。
Satnam Singh - 无处理器计算
从程序员的角度来看,硬件和软件之间的区别正在变得模糊。随着程序员努力满足当今系统的性能要求,他们将面临越来越大的需求,需要利用替代计算元件,例如GPU(图形处理单元),它是为数据并行计算而颠覆的显卡,以及FPGA(现场可编程门阵列)或软硬件。