VLIW+SIMD架构学习
一、VLIW
1、引入
程序执行时间 = T o t a l i n s t r u c t i o n s × C y c l e s i n s t r u c t i o n s × S e c o n d s C y c l e s = 程序总指令数 × 每条指令所需要的周期数 × 每个周期所对应的时间 程序执行时间 = Total instructions \times \frac{Cycles}{instructions}\times \frac{Seconds}{Cycles} = 程序总指令数 \times 每条指令所需要的周期数 \times 每个周期所对应的时间程序执行时间=Totalinstructions×instructionsCycles×CyclesSeconds=程序总指令数×每条指令所需要的周期数×每个周期所对应的时间
根据上面的式子可以看出来,有三个因素影响了程序的执行时间。
- 该程序总共需要执行的指令个数;(由算法决定)
- 每条指令所需要的周期数(简称CPI);(微架构决定)
- 每个周期所对应的时间;(工艺决定)
CPI全称Clock Per Instruction,即每个时钟周期能够执行的指令个数,它反映了处理器并行处理指令个数的能力。有时候我们也用其倒数形式,即IPC(Instruction Per Clock)。
对于非流水线的多周期处理器,我们需要多个Cycle才能执行完一条指令,对于普通的流水线处理器,我们一个周期最多也就执行一条指令,IPC最大也就只能为1了,还有没有办法让IPC进一步提高呢?
实际上在程序的指令流中,很多指令相互之间是独立的,只要处理器有足够的硬件资源,理论上它们就可以被同时执行,即实现指令级别的并行(ILP,Instruction Level Parallesim),最典型的ILP有以下几种方式:
- 流水线(有时候提ILP不会提流水线,因为处理器设计默认采用流水线);
- 超标量(Superscalar);
- 超长指令字(Very Long/Large Instruction Word,VLIW);
超标量处理器设计是个非常大的话题,也是目前高性能CPU的主流技术。本篇文章不讨论超标量处理器,而是围绕VLIW展开,去思考为何VLIW曾一时风光无限,又为何在通用处理器市场败下阵来,又是怎么在DSP/NPU等领域重新大放异彩的,带着这些问题,我们正式进入VLIW的学习。
VLIW于1983年由美国计算机科学家Josh Fisher提出,并发表于体系结构顶会ISCA上,根据VLIW的名字我们就可以知道,其指令一定很长。实际也确实如此。
VLIW是将多条互相独立的指令,通过软件(编译器)的方式打包(Pack)在一起,我们将打包好的多条指令,称为instruction bundle。取指模块根据打包好的指令,送入各自独立的功能部件,并行执行,如下图所示,取指模块从指令存储器取出了Instruction Bundle,共包含四条指令,然后同时发送给后级模块,从而实现了指令级并行。
此外我们可以看出来其一共有两个浮点部件、三个整数部件、两个存储部件、一个转移部件,因此对应的指令Bundle长度为32*8=256。根据上面的例子我们不难发现,指令一旦送出以后,各个功能部件是互相独立的,各个指令也是天然并行的。即VLIW不需要通过硬件检查指令与指令之间的依赖关系,而是由软件(编译器)去静态的调度互相独立的指令,其优点就是硬件设计将对简单(为什么?),但对于编译器开发工作者而言,任务非常艰巨(为什么?),也导致了一系列的软件兼容问题(为什么?)。
2、VLIW设计哲学
VLIW的主要思想是:
- 将多个相互无依赖的指令封装到一条超长的指令字中
- CPU中有对应数量的ALU完成相应的指令操作
- 指令之间的依赖性和调度由编译器来完成
对于传统的VLIW,其特点为:
- 一次取多个指令(是指令,而不是指令bundle,记得区分这两个东西),具备多个功能部件(MIMD架构);
- 对于同一个bundle里面的指令而言,基于lock step的方式执行(后面解释什么是lock step);
- 同一个bundle里面的指令静态对齐,送给功能单元;
有了以上的概念,我们看一个典型五级流水线与VLIW相结合的例子,如下图所示。Bundle包含两条指令,因此理想情况下可以实现IPC=2。此外我们还可以看出来,VLIW通常具有集中的存储器结构,以及独立的功能单元。
所谓的Lock-step,即要么全都带走,要么一无所有。只要VLIW中任意一个指令stall住了,为了维持并排走的原则,所有与之并行的指令都需要停住。(有点像两人三足的感觉)
对于VLIW机器而言,是这么处理依赖关系的:
- 由编译器处理所有依赖相关的stalls;
- 硬件不做任何的依赖检查(无条件信任软件);
- 如果是可变延时操作又该怎么处理呢?Memory堵住了怎么办呢?
既然VLIW是由软件即编译器处理所有和依赖相关的问题,那能够获得的信息就比较少,实际上是缺乏运行时(硬件)信息的,因为一旦发出去,就没有回头路了,由此会有一系列的问题。
假设我们取出了某条指令,比如a d d t 1 , s 1 , s 2 add\ t1,s1,s2addt1,s1,s2,剩下的指令都需要用到t1当做源操作数,如果硬件不做任何检查,那岂不是一定拿到旧的数据?整个就出错了?
又比如对于同一个程序而言,假设我们更换了不同的处理器,对于不同的硬件而言,功能部件也有可能不同,依赖关系自然也不同,如果还用之前那一套编译器,那结果岂不是有可能对不上?没错,这些都是VLIW潜在的问题。
到目前为止,即使我们对VLIW编译器的细节不清楚,但我们应该已经领略了VLIW的设计哲学了,让我们暂时总结一下VLIW的优点和缺点:
首先谈一下优点,其实优点无非就是硬件设计简单,我们具体展开讲讲:
1、无需动态调度硬件->因此硬件设计起来相对简单。之所以不用动态调度硬件,实际上是我们相信编译器,所发的指令都是没有依赖关系的,因此硬件各自执行各自的,不会有任何问题。如果是超标量处理器,是需要硬件动态的检查各个指令之间是否存在依赖关系,如果有的话,可能需要阻塞,可能需要旁路,可能需要重命名等等,这些在VLIW中都不用。(静态就是不涉及运行时,动态与之相反,可以简单的理解,硬件跑起来以后提供的信息就是动态信息)
2、同一个Bundle内的指令天然对齐,因此取指令以后直接往功能单元发即可,简化了硬件设计;
然后我们再看一下VLIW的缺点:
1、编译器需要在每个周期找到N条互相独立的指令,组成相应的bundle往功能单元发。如果找不到呢?那也只能硬着头皮发,总比不发好,但这样的话,必然某个槽或某几个槽(slot,比如上面的例子中,一个bundle中就有8个slot)需要插入NOP指令。这样就减少了并行性,并且导致代码size变大(总归是要执行完所有指令的,插入无意义的指令越多,自然总的size就越大)。
2、当执行宽度(N,slot个数)、指令延迟、功能单元发生改变的时候,都需要重新编译(而超标量处理器不需要),因此软件兼容性非常差,这也是VLIW在通用CPU领域走向失败的根本原因。
3、由于Lockstep执行的原因,导致某个指令阻塞住了,整个bundle都会阻塞住,即使它们彼此之间互相独立的。就好像两人三足中队友摔倒了,即使你是好好的,也需要等待你的队友。
VLIW就是这么一套设计哲学,将原本硬件应该干的事情,全部交给编译器去做,这也就导致了编译器开发极为困难,因为要通过有限的代码信息,去找到互相独立的指令,即充分挖掘并行性,但很多时候纯靠静态信息,并行性是很难挖掘的,而编译器为了避免出错**,都是按照最保守的方式,假设最坏的情况去做**,即对似是而非的独立,默认不独立,这样就会导致大量的NOPs指令。
但由于VLIW对硬件设计非常友好,因此当并行性非常容易挖掘的时候(这种情况编译器开发就没那么困难了),比如DSP、GPU、NPU等,很多指令天然就是彼此独立的,不存在依赖关系的,这种场景VLIW就具有非常大的优势。
3、干脏活累活的VLIW编译器
首先我们看一下VLIW编译器都干了啥:
- 通过静态调度,尽可能的并行化(填满Bundle中的slot);
- 保证同一个Bundle内的所有指令是互相独立的(必须要保证,因为硬件是不会去做检查的);
- 通过静态调度,尽可能的避免数据冒险;(因为Lockstep的存在,一个堵住了,整个就堵住了);
3.1、循环操作优化
当我们谈并行,谈VLIW,就一定要谈循环级别的并行,因为很多时候循环是天然并行的。并且对于编译器而言是很好发现的(Loop-Level Parallelism is normally analyzed at the source level or close to it)。
我们假设编译器无法发现循环的并行性,如下图所示,可以看到编译以后的汇编代码,对于VLIW机器而言,可以挖掘的并行性很少,并且由于fadd需要用到fld以后的f1做运算,因此编译器为了避免出现数据冒险,会晚几个周期再送fadd指令到功能组件(再强调一次,依赖关系由编译器保证,这里对应图中的fld->fadd的红线),整体的性能非常的差。跑一个浮点加法需要8个周期(即使我们认为浮点加是单周期指令)。
实际上写过C/C++的朋友,应该都知道这种循环实际上是可以展开的(Loop Unrolling),就比如下面的例子,编译器就可以发现这一点,我们假设按照4的粒度进行展开,这样就可以在一次循环中执行四次迭代了。(当然如果N不是4的整数倍,需要把剩下的几次额外处理一下)
编译器完成该优化以后,我们再看展开以后对应的汇编代码,以及VLIW机器的流水线占用情况。可以看到流水线排布比之前满了一些,整体的FLOPS也有了显著的提高。
可不可以进一步提高FLOP?实际上是可以的,因为我们实际上可以发现,两次循环之间是没有依赖关系的,即上述的例子中,我们原本是0,1,2,3为一组,然后4,5,6,7。组与组之间是没有重叠的,但实际上它们就是独立的,因此可以进一步,将它们Overlap起来,这称之为Software Pipelineing,软件流水线(其实跟硬件流水线非常的像,本质上没太大区别)。如下图所示:
可能上面的这个VLIW指令排布还不够Makesense,我们再看下面这幅图。这幅图相比大家能理解单纯的Loop Unrolled和Software Pipelined的区别了。核心在于Overlapping,让独立的东西尽可能重叠起来。
3.2、循环之外
上面讲的是针对循环并行性的优化,如果没有循环呢?
Josh Fisher在提出VLIW的时候,同时也提出了一种名为Trace Scheduling的技术,该方法的关键思想是将基本块组合起来,使它们形成一个单入口多出口的较大块,可能作为直线代码执行。如下图所示,此外我们做如下的定义:
所谓的basic block其实就是一段连续的代码,其中没有分支或者跳转指令,具有单一的入口和出口;
由于控制密集型指令的存在(比如分支指令),basic block的size不可能太大;
对于单个basic block内部而言,很难挖掘出ILP(单个Basic block内部,认为是顺序执行的,指令与指令之间依赖关系较大);
Trace内部的指令都是单周期指令,通过重新安排基本块的顺序,使得Trace中的指令尽可能地利用处理器的功能单元,同时避免指令之间的冲突和依赖关系,进而提高程序的执行效率(如下图的灰色路径就是一条Trace,实际上Trace对应的是分支或跳转指令执行最频繁的一条路径)。
我们来看一个实例,左边是所有的基本块,以及某个基本块到另外一个基本块对应的概率,其实就是个决策树。右边是我们根据概率最大选出来的trace,是不是非常的清晰?
但是,直接用Trace是有问题的,跟踪调度的主要缺点之一是跟踪中间的进入和退出会导致严重的复杂性(实际上trace也没有中间进入或退出,但实际是的指令是可能有的),需要编译器生成和跟踪补偿代码,并且通常使得评估此类代码的成本变得困难。 超级块的形成过程与用于跟踪的过程类似,但它是扩展基本块的一种形式,仅限于单个入口点,但允许多个出口。
我们直接采取尾部复制的方式,就可以实现单入口多出口的superblock,大家看下图,我们只是在尾部加了F的复制,我们可以看到原本C到F以及D到F的入口没了,全部转移了。这就是我们所需要的超级块。
我们看一下具体的例子,左边是原始代码。右边是经过Superblock变换以后的代码,通过该机制实现了单入口多出口。下面是Superblock变换以后的代码进一步优化的结果,通过更大的代码块,有机会找到更多的并行执行机会,编译器会相对激进的在superblock内部进行code reorder以及代码优化。
总而言之,超级块是一种复杂的编译器优化技术,需要综合考虑指令之间的依赖关系、处理器的特性和约束条件等因素。它可以在静态编译阶段对程序进行优化,以提高程序的执行效率和性能。通过合并基本块并利用指令级并行性,超级块可以减少指令之间的冲突和依赖关系,从而提高程序的吞吐量和执行效率。
这里也只是说了个superblock是什么,到底如何根据superblock进行优化,优化策略细节就是非常大的话题了,涉及到编译器的很多知识,这里就不讲了(实际上我也不会)。
4、RISC、CISC和VLIW的区别
RISC、CISC和VLIW架构区别
| 架构特征 | CISC | RISC | VLIW |
|---|---|---|---|
| 指令长度 | 变长 | 固定,通常32 bits | 固定 |
| 指令格式 | 字段布局可变 | 字段布局固定一致 | 字段布局固定一致 |
| 指令语义 | 指令从简单到复杂变化,每条指令可能有许多依赖操作 | 每个指令的语义几乎总是一个简单的操作 | 多数指令是简单、独立的操作 |
| 寄存器 | 较少,部分为专用寄存器 | 较多通用寄存器 | 较多通用寄存器 |
| 内存引用方式 | 与许多不同类型指令中的操作捆绑在一起 | 不与操作捆绑在一起,如,Load/Store架构 | 不与操作捆绑在一起,如,Load/Store架构 |
| 硬件设计 | 利用微码实现 | 采用单条流水线、不使用微码 | 采用多条流水线、不使用微码和复杂的指令调度逻辑 |
从原理上看一下CISC、RISC和VLIW的区别,注意,实际上的代码不会是这样的,这里只是说明三种架构的区别。
一个C语言函数:
function( long j) { long i; j = j + I; }CISC代码
RISC代码
VLIW代码
二、SIMD学习
1、引入
1966年,Michael.J.Flynn提出根据指令流、数据流的多倍性特征对计算机系统进行了分类(通常称为Flynn分类法),有关概念的定义如下。
(1)指令流:指机器执行的指令序列。
(2)数据流:指由指令流调用的数据序列,包括输入数据和中间结果,但不包括输出数据。
(3)多倍性:指在系统性能瓶颈部件上同时处于同一执行阶段的指令或数据的最大可能个数。
Flynn根据不同的指令流和数据流组织方式,把计算机系统分成如下4类。
(1)单指令流单数据流(Single Instruction stream and Single Data stream,SISD)。SISD其实就是传统的顺序执行的单处理器计算机,其指令部件每次只对一条指令进行译码,并只对一个操作部件分配数据。流水线方式的单处理机有时也被当作SISD.早期的计算机都是SISD机器,如冯诺.依曼架构,如IBM PC机,早期的巨型机和许多8位的家用机等。(一条指令处理一个数据项。程序指令按顺序执行,每条指令从内存读取或写入一个数据。)
- 硬件设计:经典的冯·诺依曼架构。包含一个控制单元、一个处理单元、一个内存模块。现代CPU通过流水线、分支预测、多级缓存等技术提高效率,但核心执行模型仍是SISD。
- 优点:
- 控制简单,编程模型直观(顺序执行)。
- 适合复杂的控制逻辑和任务调度。
- 缺点:
- 纯串行,无法直接利用数据并行性,对大规模数据计算(如科学计算、图形处理)效率低下。
- 典型代表:早期的单核CPU,如Intel 8086。
(2)单指令流多数据流(Single Instruction stream and Multiple Data stream,SIMD)。SIMD以并行处理机(阵列处理机)为代表,并行处理机包括多个重复的处理单元,由单一指令部件控制,按照同一指令流的要求为它们分配各自所需的不同数据。相联处理机也属于这一类。(一条指令同时作用于一组数据(一个向量)。例如,一条“加法”指令完成8对数据的加法。)
数据级并行
- 硬件设计:
- 向量处理器:拥有专门的向量寄存器(很宽,如256/512位)和向量功能单元。指令解码后,控制器将同一操作广播到多个并行ALU上执行。例如,Intel的AVX、ARM的NEON/SVE指令集。
- 阵列处理器:由多个相同的处理单元在统一控制下同步工作。
- 优点:
- 能效高:一条指令解码开销可服务大量数据计算。
- 吞吐量大:非常适合规则的数据并行计算(如矩阵运算、图像/视频处理、物理模拟)。
- 缺点:
- 数据对齐要求高。
- 不适合控制密集型或数据依赖性高的任务(如分支众多的代码)。
- 编程相对复杂,需要显式调用特殊指令或编译器自动向量化。
- 典型代表:CPU的SIMD指令扩展(SSE, AVX),早期的Cray超级计算机。
(3)多指令流单数据流(Multiple Instruction stream and Single Data stream,MISD)。MISD具有n个处理单元,按n条不同指令的要求对同一数据流及其中间结果进行不同的处理。一个处理单元的输出又作为另一个处理单元的输入。这类系统实际上很少见到。有文献把流水线看作多个指令部件,称流水线计算机是MISD.
这是一个理论存在但几乎没有实际商业化应用的模型。
- 概念:多个不同的指令同时作用于同一个数据流。
- 硬件设计:难以构建实用化的设计。一种解释是容错系统,多个处理器执行相同计算以校验结果,但指令流可能略有不同(严格说也不是纯MISD)。另一种是流水线处理,数据流经不同阶段执行不同操作,但这通常被视为SISD的时空重叠。
- 优缺点:缺乏实用案例,讨论意义不大。
- 典型代表:无广泛认可的商业产品。某些文献将飞行控制中的冗余系统作为例子。
(4)多指令流多数据流(Multiple Instruction stream and Multiple Data stream,MIMD)。MIMD是指能实现作业、任务、指令等各级全面并行的多机系统。多处理机属于MIMD.当前的高性能服务器与超级计算机大多具有多个处理机,能进行多任务处理,称为多处理机系统,不论是大规模并行处理机还是对称多处理机,都属于MIMD.
这是任务级并行和数据级并行的通用模型,也是现代多核、多处理器系统的主流架构。
- 概念:多个处理器/核心独立执行不同的指令流,处理不同的数据。
- 硬件设计:
- 共享内存多处理器:所有核心通过总线或交叉开关共享同一物理内存(UMA/NUMA)。需要硬件/软件同步机制(锁、原子操作)。例如,多核CPU(如Intel Core i7, AMD Ryzen)、大型SMP服务器。
- 分布式内存多处理器:每个处理器有自己的本地内存,通过高速网络互联。通信通过消息传递进行。例如,计算机集群、超级计算机。
- 优点:
- 灵活性最高:可同时处理任务并行(不同程序)和数据并行(同一程序的不同部分)。
- 扩展性强:可以通过增加节点(分布式内存)或核心数(共享内存)来提升性能。
- 缺点:
- 通信和同步开销大:共享内存存在缓存一致性问题,分布式内存存在通信延迟。
- 编程复杂:需要处理线程/进程同步、通信、负载均衡等问题(如使用Pthreads, OpenMP, MPI)。
- 典型代表:现代多核CPU、GPU集群、TOP500超级计算机。
(5)SIMT - 单指令流多线程
这是NVIDIA GPU采用的核心执行模型,是SIMD的一种更灵活、对程序员更友好的变体。
- 概念:一条指令被多个线程(通常32个为一组,称为Warp)同时执行。这些线程在同一个程序计数器下运行,但拥有自己的寄存器和数据地址。关键点:允许同一Warp内的线程有不同的执行路径(通过分支),但会导致性能下降(线程分化)。
- 硬件设计:
- 流多处理器:SM包含多个标量处理核心、共享内存/缓存、寄存器堆。
- Warp调度器:负责将32个线程打包成一个Warp进行调度和执行。如果所有线程走相同路径,效率接近SIMD;如果分支,会串行执行不同路径。
- 优点(相对于传统SIMD):
- 编程模型更灵活:程序员以多线程的角度思考(CUDA/OpenCL),硬件负责将线程分组和调度,隐藏了底层SIMD宽度。
- 更好地处理不规则并行和分支:虽然分化有代价,但模型本身支持。
- 拥有层次化的存储结构(寄存器、共享内存、全局内存),便于优化数据局部性。
- 缺点:
- 线程分化和内存访问非对齐/不连续会显著降低实际吞吐量。
- 需要大量线程来隐藏内存访问延迟。
- 不适合任务并行或控制密集型任务。
- 典型代表:NVIDIA GPU的CUDA架构,AMD GPU的GCN/RDNA架构也采用类似思想。
总结与异同对比
| 架构 | 核心思想 | 并行粒度 | 硬件关键特征 | 编程模型/难点 | 主要应用场景 |
|---|---|---|---|---|---|
| SISD | 一次一指令一数据 | 指令级(非真正并行) | 单核,复杂控制单元 | 顺序编程,简单直观 | 通用计算,控制密集型任务 |
| SIMD | 一次一指令多数据 | 数据级并行 | 宽向量寄存器,多ALU受控于同一指令 | 需数据对齐,避免分支 | 媒体处理,科学计算,规则算法 |
| SIMT | 一次一指令多线程 | 数据级并行(线程级呈现) | Warp调度,标量核心阵列,层次化存储 | 大规模多线程(CUDA),需避免Warp分化 | GPU通用计算,图形渲染,AI训练/推理 |
| MISD | 多指令一数据 | 理论模型,无实用主流 | 难以有效实现 | 无主流编程模型 | 特殊容错或信号处理(极少) |
| MIMD | 多指令多数据 | 任务级与数据级并行 | 多独立核心,共享或分布式内存 | 线程/进程同步与通信(OpenMP/MPI) | 多核CPU、服务器、超级计算机 |
2、SIMD架构
在学习SIMD之前,我们首先需要了解两个概念。
浮点运算指令分为两大类:Packed(矢量) 和Scalar(标量)。
Packed指令是一次对寄存器中的四个浮点数(即DATA0 ~ DATA3)均进行计算,而Scalar一次则只对寄存器中的DATA0进行计算。如下图所示:
SIMD 的工作依赖于向量寄存器,这些寄存器可以存储多个数据元素。例如,如果一个寄存器宽度是 128 位,并且每个数据元素是 32 位的整数,那么该寄存器一次就可以存储 4 个整数。在 SIMD 操作时,一条指令对这 4 个整数同时执行相同的计算(如加法、乘法等)。
2.1 整体架构
┌─────────────────────────────────────────────┐ │ 控制单元 (Control Unit) │ │ 取指→解码→广播→向量寄存器控制→存储控制 │ └──────────────┬──────────────────────────────┘ │ 广播相同操作 ▼ ┌─────────────────────────────────────────────┐ │ 向量处理单元阵列 │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ ALU │ │ ALU │ │ ALU │ │ ALU │ ... │ │ │ 0 │ │ 1 │ │ 2 │ │ 3 │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │ │ │ │ │ └──────┼────────┼────────┼────────┼───────────┘ │ │ │ │ ┌──────┼────────┼────────┼────────┼───────────┐ │ ┌───▼──┐┌───▼──┐┌───▼──┐┌───▼──┐ │ │ │向量寄││向量寄││向量寄││向量寄│ ... │ │ │存器 0││存器 1││存器 2││存器 3│ │ │ │(通道0)│(通道1)│(通道2)│(通道3)│ │ │ └──────┘└──────┘└──────┘└──────┘ │ │ 向量寄存器文件 │ └─────────────────────────────────────────────┘ │ │ │ │ ┌──────┼────────┼────────┼────────┼───────────┐ │ 内存加载/存储单元 (Load/Store Unit) │ │ 支持:对齐访问、跨步访问、聚集/散播 │ └─────────────────────────────────────────────┘2.2 详细组件说明
2.2.1 向量寄存器文件 (Vector Register File)
- 结构特点:
- 宽寄存器:128/256/512/1024位宽度
- 多通道:每个寄存器划分为多个"通道"(lane)
- 数量:通常16-32个向量寄存器
- 数据组织:
text
256位向量寄存器示例: ┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ │ 通道0 │ 通道1 │ 通道2 │ 通道3 │ 通道4 │ 通道5 │ 通道6 │ 通道7 │ │ 32位 │ 32位 │ 32位 │ 32位 │ 32位 │ 32位 │ 32位 │ 32位 │ └────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘2.2.2 处理单元阵列 (Processing Element Array)
- 并行ALU设计:
- 多个相同的ALU并行工作
- 每个ALU处理一个数据通道
- 操作数来自向量寄存器的对应通道
- 结果写回向量寄存器的对应通道
- 功能单元类型:
- 算术单元:加法器、乘法器、乘加单元(FMA)
- 逻辑单元:与、或、非、移位
- 比较单元:产生掩码(mask)
- 特殊函数单元:倒数、平方根等
2.2.3 控制单元 (Control Unit)
- SIMD指令解码:
- 识别向量指令操作码
- 确定操作数宽度和数量
- 生成控制信号广播到所有处理单元
- 广播机制:
- 标量广播:将标量值复制到所有通道
- 立即数广播:将立即数复制到所有通道
- 寄存器广播:将向量寄存器的一个元素广播到所有通道
2.2.4 内存子系统 (Memory Subsystem)
对齐访问:
- 要求数据地址对齐到向量宽度
- 非对齐访问需要特殊处理(性能损失)
访问模式支持:
text
1. 连续访问 (Contiguous) 内存:[A0][A1][A2][A3][A4][A5][A6][A7] 加载:一次性加载8个连续元素 2. 跨步访问 (Strided) 内存:[A0][X][A1][X][A2][X][A3][X] (stride=2) 加载:每隔一个元素加载 3. 聚集-散播 (Gather-Scatter) 内存:[A0][B0][C0][D0][A1][B1][C1][D1] 索引:[0][4][8][12][16][20][24][28] 聚集:根据索引从不同位置收集数据 散播:根据索引分散数据到不同位置