计算机体系结构.微结构概述

    xiaoxiao2022-07-03  125

    计算机体系结构.微结构概述

    随着工艺的进步,从集成电路发展到大规模集成电路,再到超大规模集成电路,相同面积内可集成的晶体管数目越来越多,这也就给了芯片设计人员极大的空间来发挥身手。

    在体系结构的发展历程中主要有两个阶段,第一个阶段是专注提升单核性能的阶段,受到摩尔定律的推动,处理器主频越来越高,出现了流水线、动态调度(乱序执行)、多发射等经典的设计,主要是提高ILP(Instruction Level Parallelism)。处理器主频的提升,也愈发凸显了存储墙问题,继而有缓存层次结构来缓解这个问题。第二阶段摩尔定律失效,单核性能难以持续增长,由此转向多核、多处理器乃至集群,出现了片上网络、多处理器互联、同时多线程等技术,主要是提高线程级的并行性。此时面临的一个大问题是存储一致性,这又可以分为两个视角来看:缓存一致性(coherence)和内存一致性(consistency)。而一致性问题在单核时期并不存在。

    2017年图灵奖颁发给了体系结构泰斗Patterson和Hennessy,以表彰他们对体系结构的量化研究作出的贡献。Patterson也做出了关于DSA(Domain Specific Architecture)的演讲后,我认为微结构的发展迎来了第三阶段,即面向领域的体系结构。如今最典型的领域,当属人工智能中的深度学习了,典型的案例有寒武纪DianNao系列、谷歌的TPU等等。华为甚至也舍弃了寒武纪的IP,选择了自主研发人工智能模块,以便更好的掌控其发展。

    以下针对每个技术点进行简略的概述。

    流水线 pipeline

    流水线,说到底就是一种分工合作,常常以工厂的生产流程来举例。比如生产一个产品P,需要分别经历三个步骤S1,S2和S3,每个步骤耗时为T。若只交给一个工人,那这个工人需要熟悉这三个步骤,而且需要 3T 的时间才能生产一个产品。倘若有三个工人A1,A2和A3,分别只需要完成其中的一个步骤,即对应S1,S2,S3,则当工人A1完成S1后,将半成品交给A2,A2负责S2的工作,然后交给A3。如此就是流水作业,结果就是每经过 T 时间都可以生产出一个产品P。

    对于处理器而言,其并不生产东西,而是需要完成每一条指令的执行。倘若处理器在完全完成一条指令后,再开始执行下一条指令,那这效率就太低了。

    首先指令的种类很多,所以至少需要对拿到的指令进行分析,来看这条指令到底需要完成怎样的操作,然后再执行。由此就至少有两个阶段,即“分析”和执行“。倘若不用流水线,那在分析一条指令的时候,负责执行的部分就空闲着;当开始执行的时候,负责分析的部分就空闲着。这就是很大的浪费了。题外话:所谓”分析“对应的术语为”译码“

    教科书中典型的流水线将指令的执行划分为五个阶段:

    取指(IF):从指令缓存中读取指令译码(ID):对指令进行解码,并从寄存器堆中读取数据执行(EX):指令和数据送入功能部件,进行运算访存(MEM):从数据缓存中读取数据写回(WB):将结果写回寄存器堆

    历史上为了追求高主频,曾将流水线划分至30多级,但过多的流水线切分并不会带来好处,因此目前流水线最多就是十几级。此外流水线的顺利运行也受到指令之间相关性的阻挠,相应地有寄存器重命名、分支预测等技术来解决。

    存储层次 memory hierarchy

    由于存储墙问题的存在,即处理器运行速度远大于访存速度,从而出现了存储的层次结构,来缓解这个问题。在现在的多核架构中,一个芯片内部有多个核心,通常也集成了一级缓存(L1 Cache)和二级缓存(L2 Cache),而且三级缓存(L3 Cache)也变成十分常见了。L1缓存为每个核心独有,又分为一级指令缓存(L1 I-Cache)和一级数据缓存(L1 D-Cache)。L2缓存常常作为L1缓存的后援力量,同样为一个核心服务。L3缓存则共享于芯片内的所有核心。

    为什么要这样分层呢?或者说,为什么这样分层就可以缓解存储墙问题呢。原因在于访存速度与容量和其与核心的距离有关。

    首先,一个很显然的事实就是,如果把数据都放在芯片外,通过总线来传输,那总线的速度无论如何也是跟不上处理器速度的。而放到芯片内,让处理器核心可以直接访问,则快很多。此外就是层次结构:L1缓存容量最小,速度最快,快到可以完全与处理器速度匹配,但由于容量小,无法装下所有的数据,必然会存在访存所需要的数据不在L1中的情况——此时就去访问L2,L2的容量比L1更大,速度也更慢。依次类推到L3以及芯片外的主存。 这种层次结构由此带来的一个结果就是:如果大部分访存都能在L1就被满足,那处理器几乎不需要等待访存了,也就缓解了存储墙问题。但由于L1容量太小,很难满足所有的访存需求,因此这里就有一个指标来衡量:命中率。

    层次结构只能缓解存储墙问题,而不是解决,因为从源头上讲,访存速度还是远远慢于处理器速度的。更源头一点说,这就是冯诺伊曼架构本身的瓶颈所在,即冯诺伊曼瓶颈。而如今我们只不过是把常用的数据放到了处理器核心的手边,并把它做得很快,但这本质上这只是缓存的速度,而不是内存的速度。

    层次结构之所以有效的原因在于,程序访存的局部性,被广为熟知为局部性原理。因此小而快的一级缓存,才能有足够高的命中率,才能真正发挥效果。

    乱序执行 out of order execution

    也叫动态调度,但称为乱序执行更常见。其背景是处理器在顺序执行指令的过程中,总有不得不停顿的时候,比如访存没有命中缓存时,需要花费更多的时间才能完成访存,而后续依赖于该访存结果的指令就需要等待该访存指令的完成;再比如遇到乘法、除法之类需要运行很多拍才能完成的指令时,那后续的依赖于其执行结果的指令就不得不等待。

    而一个很显然的事实是,如果后续其他的指令并不依赖于正在执行的指令的话,那这些指令就完全没必要等待其执行完成。乱序也就乱序在这里:倘若后续指令不依赖于正在执行指令的结果,则可以继续执行。这就需要对指令之间的相关性进行检查,让无关的指令越过相关的指令继续执行,不过最终还是要顺序完成,乱序的结果还是要顺序写回,才能不影响程序的正常结果。

    最经典的乱序结构是保留站,可以说是奠定了此后所有乱序技术的基础。而从取指、执行、写回三大方面而言,即为顺序取指、乱序执行、顺序写回的结构。此外,保留站结构也为寄存器重命名技术提供了很好的支持,从而消除了部分指令相关性,加大了乱序的程度,也就提高了性能。

    多发射结构 multi-issue

    多发射即一个时钟周期内“发射”(issue)多条指令。然而“发射”的概念并不统一,有些地方将指令完成译码后等待执行称为发射,有些地方将等待执行的指令送入功能单元进行执行的这一步称为发射。本文以后者为准。由此多发射即一拍内有多条指令进入功能单元开始执行。

    多发射是若干因素共同作用的结果:

    取指部件的带宽越来越大,可以在一个时钟周期内取来多条指令。显然如果一拍只能取一条指令的话,是做不到一拍内发射多条指令的。译码部件也需要越来越快,能够在一拍内解码多条指令,并分析其相关性。功能单元越来越多,以支持更多的指令同时运行。寄存器堆越来越大,来支持更多的指令在流水线中。

    分支预测 branch prediction 和 预取技术 prefetching

    分支预测和预取技术,本质上都是一种猜测。在流水线运行过程中,需要持续不断的指令供应和持续不断的数据供应,才能充分发挥流水线的效率。

    指令供应的一大影响因素是分支指令,因其会改变指令的流向,分支预测就是猜测指令的流向会向哪里改变,从而提前读取指令。数据供应的影响因素在于数据缓存的命中率,因此可以猜测后续可能访问哪些数据,提前读取(prefetch)到缓存中供后续使用。此外也有指令预取。

    分支预测用于解决分支指令带来的程序流向转变的问题,其目标是向流水线持续不断的提供指令,供流水线充分发挥作用。而当遇到分支指令的时候,程序流的流向就可能会改变。

    分支指令有条件分支指令和非条件分支指令。对于非条件分支指令,跳转是确定的,只需要知道跳转地址就可以了。有时候这是比较容易的,比如函数调用的返回指令,可以通过一个函数调用地址栈结构来保存返回地址,从而快速获取到跳转地址。有时候获取跳转地址也是比较麻烦的。而分支预测主要是针对条件分支指令。在分支条件的运算结果出来之前,流水线并不知道到底要不要跳转,如果因此而等待,则会造成流水线停滞。

    此外还有一个原因就是,程序运行中的分支指令是很多的,据统计每6到8条指令就会遇到一条分支指令,如果每次都要等待,那流水线的效率就无法充分发挥。

    目前的分支预测技术主要是基于历史推测未来,即记录每条分支的转移历史,来推测下一次遇到该分支的时候,是否需要跳转。当然分支预测也有猜错的时候,此时就需要取消流水线中正在运行的指令,从正确的地址中继续读取指令送入流水线。

    预取技术同样是基于历史推测未来。对于指令,其多数时间为顺序执行,因此最简单的预取就是顺着地址多读一点。对于数据,其访问就比较复杂,需要专门的结构来考察数据访问的规律,预测后面需要用到的数据,然后在其被用到之前读取到缓存中。

    多核结构 multi-core 与 片上网络 network on chip

    指令并行度的提升越来越困难时,自然就出现了多核结构。芯片之外还有多处理器的互联、多服务器的互联。多处理器的互联是通过主板上的连线和处理器提供的接口完成,多服务器的互联则是通过网络。芯片内的多个核心之间则是通过片上网络(NOC)完成。

    无论是片内多核还是多处理器互联,本质上都试图通过提高并行度来提高性能。然而这也受制于程序本身的并行度,即阿姆达尔定律(Amdahl‘s Law)。该定律表明了最终的性能取决于程序本身可并行执行的部分,而不是硬件提供越多的核心就意味着性能越高。

    对于片上网络,其实也是一种网络结构,即也有拓扑(topology)、路由(route)、流控(flow control)等问题,无非在网络上通讯的是处理器核心罢了。而网络上传送的消息,也有诸如消息头、消息体之类的格式。

    从技术的演进上看,最早是总线(Bus)来负责片内多个模块的互联,此后是交叉开关(crossbar),然后才是片上网络(Network On Chip),背后的驱动力则是多核技术的发展、处理器速度的提升、模块的多样化等等,对片内的通信需求越来越高。另一个原因则是片上网络的研究门槛相对较低,由于需要考虑的是通信本身,逻辑上不需要太多电路知识,因此多年来片上网络的研究一直在持续,近几年则逐渐开始商用普及。

    缓存一致性 cache coherence 和 内存一致性 memory consistency

    线程之间的通信有两种方式:消息传递(message passing)和共享内存(share memory)。对于消息传递而言,每次传输的信息有限,而且需要进行数据拷贝,且对于频繁的消息发送其处理耗时且不及时,因此不适合高度并发的程序。而共享内存则与其正好相反,其可以使用足够的空间来完成通信,无需进行数据拷贝,通信速度快,因此多线程之间的合作常常使用共享内存模式。

    然而共享内存面临的问题就是一致性问题。

    缓存一致性:该一致性解决的是,同一份数据位于多个核心的缓存中时的一致性问题。当处理器A和B读取了内存中的数据asd,该数据asd就位于A和B的缓存中。若A修改了数据asd而没有通知B,则B再读取的时候就读到了B缓存的旧数据。经典的缓存一致性协议为MSI协议,在此基础上有MESI、MOESI等协议。内存一致性:该一致性解决的是,多个核心之间写操作的顺序问题。倘若处理器A的多个写操作以不同的顺序被处理器B和C所见,就可能会发生与期望中不一致的情况。然而要完全保存一致的顺序非常不值得,不仅实现上十分复杂,而且会降低程序性能。实际上并不是所有的写操作都会出现问题,只有对共享内存的写才需要同步,因此硬件常常提供某种内存一致性模型,然后提供内存栅栏(memory barrier)指令来强制写操作的顺序,即将这个问题的解决分了一部分给软件。这样的好处有:硬件可以简化设计,并尽可能提高性能,而软件只需在关键位置插入内存栅栏指令,避免程序出现与预期不一致的情况即可。

    向量计算 vector execution 与 图形计算 graph process unit

    随着时代的发展,需要处理的数据也逐渐丰富,典型的如图像、视频。随着应用的丰富,硬件方面也在跟随者应用发展,后来逐渐发现很多时候需要对许多相同格式的数据进行同样的操作。由此硬件就提供了向量指令,即SIMD(Single Instruction Multiple Data)。对应的也需要有向量运算单元,来对多个数据完成相同的操作。向量部件不断发展,从64位到128位,再到256位乃至512位,使得可以并行处理的数据越来越多。但即使如此,对于某些数据的处理还是不够快,尤其是计算机图形和图像处理的需求越来越高,于是就出现了专门的负责图向处理器器件,即GPU。

    有个不严格的比喻就是,一个数学教授和1000个小学生。CPU对应数学教授,其精通各种高阶的数学知识,可以完成各种复杂的运算。而GPU则对应1000个小学生。其每个核心都很简单,只能完成简答的运算比如加法、乘法,但胜在数量多。倘若需要对1000个数据同时乘2,CPU需要一个一个数据的处理,即是使用向量指令也需要运行多次。而GPU有众多的处理核心,可以一拍完成所有数据的运算。

    GPU所具备的极高的并行度,很适合处理图像这种天生具有大规模数据并行的任务。但除此之外,还有许多任务同样有大规模的数据并行,因此就出现了GPGPU(General Purpose Computing on GPU) ,开放诸如CUDA之类的编程接口,供程序员编写可以在GPU上进行数据处理的程序。此时的GPU作为一种外设,需要与CPU进行通信:CPU把数据从内存传送到GPU的显存上,GPU处理完成后,将结果传回内存供CPU使用。

    人工智能处理器

    人工智能的热潮下,最火的当属深度学习了。深度学习属于人工神经网络的范畴,后者又是机器学习的子集,而机器学习又在人工智能这个范畴内占有一块天地。目前所谓的人工智能芯片,大多数针对的还是神经网络,如卷积层、池化层等等。

    已知最早是一款人工智能处理器,应该是2014年寒武纪的DianNao了,此后寒武纪更在体系结构的各大顶会上连续发表多篇文章。此后寒武纪与华为展开合作,在麒麟970和麒麟980中均即成了寒武纪的人工智能IP模块。华为在2019年6月发布的nove5中搭载了麒麟810,舍弃了寒武纪的IP模块,采用自主研发的达芬奇架构。谷歌则在2015年就宣布了自用的TPU,之后在2017年ISCA上正式发表文章来讲述TPU的架构、性能、功耗等等,之后陆续推出了TPU v2和TPU v3,并在自家的云平台上开放付费使用。英特尔也在2016年收购了人工智能公司Nervana。

    神经网络的运算最终可以用张量运算进行描述,且由于其本身的特点,对精度要求并不高,因此往往16 bit的浮点运算就已足够,在某些场景下完全可以只用定点运算。智能芯片中对张量的处理常常采用运算单元阵列的架构,同时伴有更高的片上存储空间,以满足连续不断的数据输入。谷歌的第一代TPU的运算单元采用了65536个运算单元组成的256行256列的阵列。而寒武纪则采用类似经典流水线的结构,如DianNao中完成乘法运算的NFU-1,完成选择和加法运算的NFU-2,完成激活函数运算的NFU-3,随后通过优化数据通路、增加运算单元等类似经典处理器的优化技术来提升性能。

    参考资料

    [1] John L. Hennessy, David A. Patterson.Computer Architecture:A Quantitative Approach,Fifth Edition[M].人民邮电出版社:北京,2012.

    [2] 胡伟武.计算机体系结构(第2版)[M].清华大学出版社:北京,2017.

    [3] Krste Asanović. CS252 Graduate Computer Architecture.[2019-07-02]. http://www-inst.eecs.berkeley.edu/~cs152/sp19/

    [4] Kaz Sato, Cliff Young, David Patterson. An in-depth look at Google’s first Tensor Processing Unit (TPU)[EB/OL].(2017-05-12)[2019-07-02]. https://cloud.google.com/blog/products/gcp/an-in-depth-look-at-googles-first-tensor-processing-unit-tpu

    [5] Chen T, Du Z, Sun N, et al. Diannao: A small-footprint high-throughput accelerator for ubiquitous machine-learning[C]//ACM Sigplan Notices. ACM, 2014, 49(4): 269-284.

    最新回复(0)