当我们设计一个专用处理器的时候我们在干什么?(微结构)

StarryHeavensAbove2019-02-10 14:24:23

在上一篇介绍指令集的文章中,我们设计了一个只有4个指令的指令集{00:fft;01:load:10;store;11:loop},并且用这个指令集写了一个汇编小程序。

今天看看它的硬件实现,微结构(microarchitecture)。wiki对微结构的定义如下:

“In electronics engineering and computer engineering, microarchitecture, also called computer organization and sometimes abbreviated as µarch or uarch, is the way a given instruction set architecture (ISA) is implemented in a particular processor. A given ISA may be implemented with different microarchitectures; implementations may vary due to different goals of a given design or due to shifts in technology.”


如果说指令集是一个处理器的功能规范,那么微结构可以认为是实现ISA的硬件架构。对于不同的优化目标,一个ISA可以通过不同的微结构来实现。换句话说,微结构是最终实现性能指标要求的途径。当然,一个优秀合理的ISA的在设计的时候肯定也考虑了微结构实现的问题。在我们设计一个专用处理器的时候,ISA和微结构的设计和优化往往是一个交织进行的过程。先设计一个ISA,然后在做微结构实现的过程中再修改ISA也是很常见的。

微结构的设计和优化又是一个巨大的话题,也涉及很多知识。我还是先通过FFT专用处理器的例子来说明一下基本概念。为了实现上一篇文章设计的ISA,我们可以设计这样一个处理器微结构。


如果读者您一看就明白了这个图的意思,请跳过下面这段简要说明,直接看微结构优化的讨论。


首先,我们要执行的关键指令是fft指令,这里假设fft指令就是做一次蝶形运算(buffterfly)。所以我们要有一个做蝶形运算的硬件单元(图中的4)。而这个功能单元FU(Functional Unit)需要输入和输出数据。数据了来源可能是通用寄存器堆RF(Register File,图中的3),也可能是memory或者流水线寄存器。同样,数据的输出也有很多可能。因此,需要一些MUX来进行选择。简单说,图中的3和4就构成了处理器中的数据通道(datapath),也就是处理数据的通路。另外,为了把数据从数据存储器(data memory)中读进来进行处理(load),或者将处理的结果再写回到存储器,还需要一个“load store单元”(图中的5)。

但是,数据通道要正确运行,需要很多控制信息。比如,在寄存器堆中倒底哪个存放的是输入数据;哪个应该存放运算结果?FU的数据来源倒底来自RF还是memory;结果要写回哪里?等等。而这些信息实际上就包含在程序指令里。我们假想汇编程序,每行指令都包含对datapath的控制信息。因此,在一个处理器里还需要有一条控制通路(control path),根据程序指令实现对datapath的设置和控制。

我们先要把指令从程序存储器(PM:program memory)读进来。这需要一个取指令的功能模块(fetch);取指模块的功能是向PM发出地址,执行“”操作。这个地址是根据一个特殊的寄存器:程序计数器(PC:program counter)产生的。PC也可以看作是指向程序存储空间的一个指针,它实际控制着程序执行的流程。如果程序按正常顺序执行,则PC = PC + 1。如果需要改变程序流,比如跳转,则需要改变PC的值,指向要跳转的新地址,PC = PC + offset。这样取指模块读出的就是跳转目标位置的指令。

上篇文章已经介绍了,指令经过编码以后形成一个二进制的机器码。取指模块读进来的正是这个机器码。要确定这条指令要执行的具体操作,就需要进行译码(decode)。比如,在咱们的例子中,根据机器码的头两个bit就可以判断倒底是那一条指令。

分辨出具体是什么指令,就可以执行该指令的操作了。通常这个过程称为指令发射(issue)或者执行(execution)。其结果包括,对数据通路的控制,比如“fft”指令;对PC的修改,比如指令“loop”和对访的控制“load和store”指令,等等。

到此为止,我们已经有了一个workable的硬件架构了,在这个硬件上可以运行前面说的汇编程序并且输出结果。但实际上,这是一个“极简”微结构,忽略了很多重要内容。为了后面讨论的方便,下面介绍几个和微结构相关的名词。

指令周期(Instruction cycle):

一条指令一般会经历“取指”,“译码”,“发射/执行”和“写回”这些操作。处理器执行程序的过程就是不断重复这几个操作。

指令流水线(Instruction pipeline):

当一条指令,完成了“取指”操作,开始进行“译码”的时候,取指模块就可以取下一条指令了,这样可以让这些模块不至于闲着没用。wiki对指令流水线的示例如下(IF:取指;ID:指令译码;EX:执行;MEM:访存;WB:写回):

 指令级并行(Instruction-level parallelism):

同时执行多条指令。比如,一边从memory读数据,一边进行fft处理。我们经常听到的超标量(Superscalar),超长指令字(VLIW),乱序执行( Out-of-order execution)等等技术都是发掘指令级并行的技术。

数据并行(Data parallelism):

同时处理多个数据。我们常听到的向量处理器(vector procesor),张量处理器(Tensor processor)多数都是利用了SIMD(一条指令可以处理多个数据,比如一个向量乘法)技术。

存储层次(memory hierarchy):

处理器相关的存储实际是由多种类型的存储器组成。一般访问速度越快(离datapath的“距离”越近),成本越高;相应的容量也越小。按从快到慢的顺序,包括芯片内的存储器:寄存器(Register),TCM(Tightly Coupled Memory),L1 cache,L2 cache和芯片外的存储器,DDR,硬盘等等。



实际上,对处理器微结构的研究到今天为止已经非常非常成熟,想有很大的创新几乎不太可能了(存储方面例外)。做一个专用处理器无非是怎么针对应用的特点,利用好这些经验的问题。当然,这也是一种创新。下面谈一些做专用处理器的个人感受吧。


1. 通用处理器的背景知识

既然专用处理器只是一种特殊的处理器,那么处理器的一般性知识还是非常重要的。如果你对处理器设计的常用技术和技巧都非常熟悉,那么你设计专用处理器肯定也是游刃有余。比如指令级并行和数据并行是微结构设计的两个重要方向,你是否能准确的了解每一种并行技术的优势,劣势和代价呢?最好你能够在脑子里就有一个对比的表格,随时可以拿出来和目标应用放在一起做评估。另外,还是要跟踪这个领域的最新进展,也许能给你带来很大的启发。


2. 突破通用处理器的思维

虽然做专用处理器要以处理器的一般知识为基础。但也要敢于做出突破。实际上,我们看到的处理器设计经典知识往往针对通用处理器。毕竟它的应用范围广,讨论的也比较多。而面向某个领域的专用处理器通常都是by design的优化,可能就只有你自己或者很少的人做,讨论的也比较少。这种时候就要相信自己对应用的理解,敢于做出一些“奇怪”的设计。当然,前提是我们有严谨的定量分析做支撑。这一点我会在后续介绍方法学和工具链的文章里进一步说明。


其实,这两点说起来简单,如果你能做到就可以达到庄子说的“达人”境界了。


最后,附赠一些小的tips。微结构里面可以优化的点很多,我随便说几个一下能想到的地方吧。

第一,如果你的设计对功耗很敏感,可以好好优化一下寄存器堆。它往往是处理器内部功耗最大的地方。看看能不能尽量减少读写端口的数量;或者采用分簇的寄存器堆,等等;

第二,对cache要慎用。在很多应用里,cache不一定能起到提升性能的好处。可能一块儿同样大小的TCM要有效的多。这里最好还是把你的应用在你设计的微结构上做一下仿真,看一看cache profiling的结果再决定。另一方面,cache的思想还是很有帮助的,不妨琢磨一些是不是可以设计一个更能发掘应用特点的cache。

第三,一般来说,如果能让datapath不停顿的工作,说明你的设计运行效率很高。但对大多数设计来说,做到这一点并不容易,瓶颈往往在存储结构和访存机制上。也就是说你的datapath处理能力没问题,但经常供不上数。这种情况下,如果不能增加访存的带宽(或者效率),就干脆降低datapath的处理器能力,还能降低一些功耗。

第四,再强调一下定量分析重要性。专用处理器设计的一个优势是比较容易对应用做定量分析和仿真,一定要把握好这一点。做一个基本的ISA和微结构,就马上把应用在上面跑跑仿真,然后根据结果做进一步优化,是最靠谱的方法。


最后还得再说一遍,由于这个话题实在太大,很多地方只能简单带过,很不严谨,请大家见谅。感兴趣的同学就把这几篇文章当做一个进一步学习的引子吧。

T.S.

Copyright © 古田计算器虚拟社区@2017