带有向量指令的微处理器将是未来的大趋势。为什么?因为自动驾驶、语音识别、图像识别都是基 于机器学习,并且机器学习都是关于矩阵和向量的。 但这不是唯一的原因。自从我们半官方地宣布摩尔定律结束以来,我们一直在拼命寻找更多的性 能。在微处理器设计的黄金旧时代,我们可以容易地每年将CPU的频率翻倍,每个人都很开心。这 个绝妙的老把戏结束了。
性能的提高一直停滞不前,因此需要以不同的方式利用更多的晶体管进行并行处理,无论是多核、向量处理还是无 序执行。
现在我们耍了几千种不同的把戏找寻更多的性能,无论是增加更多的CPU核心、还是更高级的分支 预测亦或是SIMD指令。 所有的把戏都归结为一个中心思想:试图找到并行工作的方法。每当你遍历数组元素并对每个元素 进行一些计算时,都有机会进行数据并行。这个循环可以由聪明的编译器编译成一束SIMD或者向量 指令。
SIMD指令例如Neon、MMX、SSE2以及AVX在多媒体应用中发挥了极大作用。例如做视频编码 等。但是我们需要在更多的领域中挤出更多的性能。向量指令在处理几乎任何循环并将其转换为向 量指令方面提供了更大的灵活性。可是,有许多不同的方式来实现以上的内容。
我在这篇文章里介绍了RISC-V的向量指令集(RVV):RISC-V Vector Instructions vs Arm and x86 SIMD。
之后我在这篇文掌中介绍了ARM的向量指令:ARMv9:What is the Big Deal?
在写最后这篇文章的时候,我很纠结。似乎没有什么能像我所学的那样起作用。我想我在第一篇文 章中研究过后,应该算是了解向量指令了。因此,在完成最后一个故事后,我开始比较笔记。
这使我意识到ARM和RISC-V实际遵循一种完全不同的策略。
这个值得一谈,特别是因为它涉及一些 我钟爱的话题。我喜爱简单、优雅且高效的技术:简约的价值。
RISC-V向量扩展与ARM SVE相比而言,是更加优雅简约的研究。
ARM可伸缩向量指令(SVE)的问题
在研究SVE时,我还不是很清楚为什么我很难理解它,但是当我拿起我的RISC-V书籍并且重读向量 扩展章节时,我开始明白了。
平心而论,ARM相对与复杂的intel x86汇编代码来说是一个巨大的进步。我们不能忽视这一点。然 而我们也不能忘记,ARM也不是那么年轻了,已经有相当多的历史遗留了。当处理ARM时,基本有 三种不同的指令集:ARM-Thunder、ARM32和ARM64。当你Google教程并且试着读时,会有一 些障碍。人们并不总是能预先知道它所涵盖的指令集。
Neon SIMD指令基本上有两种类型:32位和64位。不,位长度不是这里的议题,而是对于64位架 构,ARM实际上重新设计了它们的整个指令集,并且改变了相当多的东西。甚至改变了CPU寄存器的命名约定。
第二个问题是ARM太过庞大了。ARM有超过1000条指令。相比之下,RISC-V基础指令集仅仅有48 条指令。这意味着读ARM汇编代码并不容易。看看以下SVE指令: LD1D z1.D, p0/Z, [x1, x3, LSL #3] 这做了很多事情。有一些汇编经验的话,你可以猜到这个LD 前缀意味着LoaD 。但是1D 是啥意 思?你得查一查。接下来,寄存器名上有一些奇怪的后缀,例如:.D 和/Z 。这些后缀是啥意思?还 有更多要读的。然后你看到括号[]。你可能可以猜到是为了组成一个地址,但是里面有奇怪的后缀, 像LSL#3 ,这意味着逻辑左移三次。但是移位什么?整个内容?仅仅是x3 寄存器里的内容?你还得 查找更多的资料。
ARM SVE指令有许多不太清楚的概念,需要你花时间思考。我们会有更深入的对比,但是先让我说 几句RISC-V。
RISC-V向量指令之美
RISC-V向量扩展指令(RVV)的所有的指令的概述可以放在一页上。它们并不多,而且与ARM SVE 不同,它的语法非常简单。以下是RISC-V的向量加载指令:
VLD v0, x10
该指令将整数寄存器x10 内存储的内存地址处的数据加载到向量寄存器v0 中。但是加载多少?对于 SIMD指令集,例如ARM Neon来说,这由向量寄存器的名称决定。
LD1 v0.16b, [x10] # Load 16 byte values at address in x10 有其他的方式做这件事。我认为这也是一个达到类似种结果的方式。
LDR d0, [x10] # Load 64-bit value from address in x10 这将会加载128位v0 寄存器的低64位的部分。对于SVE2我们得到了另一个变体。
LD1D z0.b, p0/z, [x10] # Load ? number of byte elementsLD1D z0.d, p0/z, [x10] # Load double word (64- bit) elements 在本例中,断言寄存器p0 精确地确定了我们要加载多少个元素。如果p0 = 1110000 ,那么我们加 载三个元素。v0 是z0 的低128位。
同名寄存器?
原因是d 、v 和z 寄存器位于同一位置。我来澄清一下。每个中央处理器内部都有一个叫做寄存器文 件的内存块。或者更具体的说,一个CPU可以有多个寄存器文件。寄存器文件是保存寄存器的存储 器。所以你不能像普通主存一样访问寄存器文件中的存储单元。相反,您可以使用寄存器名称来引 用它的各个部分。
ARM向量指令复杂度
我只能接触到ARM向量指令的表层,因为它们太多了。仅仅定位Neon和SVE2的典型加载指令就已 经是相当耗时的了。我查阅了大量ARM文档和blog。而为RISC-V做同样的事情是微不足道的。几乎 所有RISC-V指令都可以放在一张双面纸上。只有三个向量加载指令:VLD、VLDS和VLDX 。
我干脆放弃算ARM有多少了。他们似乎有一吨重,我没有成为一名专业的ARM汇编代码开发人员的 计划。
ARM和RISC-V如何处理可变长度向量
这是一个非常有趣的部分,因为ARM和RISC-V使用非常不同的方法,我认为RISC-V解决方案的简 单性和灵活性真的很棒。
RISC-V可变长度
要开始向量处理,您需要做两件事:
VSETDCFG - Vector SET Data ConFiGuration.。向量集数据配置。这将设置每个元素的位大小。类型, 无论是浮点型、有符号型还是无符号型整数。它还指定要启用多少个向量寄存器。
SETVL - SET Vector Length。设置向量长度。说明你需要多少元素。有一个MVL(最大向量长度)元素的最 大数量,你不能超过。
这就是有趣的地方。不像ARM SVE,我可以用任何我想用的方式来划分向量寄存器文件。假设寄存 器文件有512字节的内存。我可以说我只需要两个向量寄存器。这给了我每个向量寄存器256字节。 接下来我可以说,我想使用32位元素。换句话说,每个元素都是4字节。这给了我: Two registers: 512 bytes / 2 = 256 bytes per register256 bytes / 4 bytes per element = 128 elements 这意味着我可以用一条指令添加或组合128个元素。使用ARM SVE,你无法做到这一点。寄存器的 数量是固定的,如果固定的话,为每个寄存器分配的内存也是固定的。RISC-V和ARM都允许您最多 使用32个向量寄存器,但RISC-V允许您禁用寄存器,并将这些寄存器原本使用的内存分配给剩余的 寄存器,从而增加它们的容量。
计算最大向量长度(MVL) 让我们来看看这在实践中是如何运作的。CPU当然知道它的寄存器堆有多大。程序员不知道,也不 应该知道。
当程序员使用VSETDCFG来设置元素类型和启用寄存器的数量时,CPU将使用该信息来计算最大向 量长度(MVL)。
LI x5, 2<<25 # Load register x5 with 2<<25VSETDCFG x5 # Set data configuration to x5 这个例子做了两件事: 使能两个寄存器v0 和v1 。 将元素类型设置为64位浮点值 让我们将其与ARM Neon进行比较,后者的每个寄存器都是128位的。这意味着使用Neon,你可以 并行计算其中的两个值。但是使用RISC-V,其中16个寄存器的内存将被合并到一个寄存器中。因 此,你可以并行计算32个值。
事实上,这并不完全正确。在后台,浮点乘法器、算术逻辑单元等的最大数量,限制了你可以并行 执行的计算通道的数量。然而,这将是一个实现细节。
无论如何,这将导致MVL 值为32。然而,作为一个开发人员,你不能直接处理这个值。SETVL指令 是这样工作的:
SETVL rd, sr ; rd ← min(MVL, sr), VL ← rd 因此,如果你试图设置向量长度(VL)为5,那么这是可行的。然而,如果你试图把它设置为60,你将 得到32。因此,这对于获得最大向量长度(MVL)很重要,当CPU被制造时,它不是硬连线到一个数 字。相反,它是由CPU根据您的数据配置(元素类型和使能寄存器)计算得到的。
ARM可变长度
使用ARM,你不需要专门设置向量长度。相反,你可以通过使用断言寄存器来间接设置向量长度。 这些是位掩码,用于启用和禁用向量寄存器中的元素。断言寄存器也存在于RISC-V上,但不像在 ARM上那样具有相同的中心作用。 要在ARM上执行与SETVL相同的操作,您可以使用一个名为WHILELT的指令,它是While小于的缩 写:
WHILELT p3.d, x1, x4 这个指令纯粹用文字来解释有点难,我就用一些Julia伪代码来演示一下。
为什么叫这个奇怪的名字?这是科学工作中流行的BLAS线性代数库中的一个简单函数。对某些 BLAS来说,这个函数被命名为daxpy,这恰好是一个非常流行的演示各种SIMD和向量指令实现的 例子。这是一个数学等式的实现:
aX + Y 其中a是标量,X和Y是向量。如果没有向量指令,我们将不得不对每个被处理的元素进行循环。但 是有了智能编译器,这可以在RISC-V上向量化成这样的代码。这里有一个注释,解释了什么寄存器 对应什么变量:
daxpy(size_t n, double a, double x[], double y[]) n - a0 int register (alias for x10) a - fa0 float register (alias for f10) x - a1 (alias for x11) y - a2 (alias for x12 这是代码:
LI t0, 2<<25 VSETDCFG t0 # enable two 64-bit float regsloop: SETVL t0, a0 # t0 ← min(mvl, a0), vl ← t0 VLD v0, a1 # load vector x SLLI t1, t0, 3 # t1 ← vl * 2³ (in bytes) VLD v1, a2 # load vector y ADD a1, a1, t1 # increment pointer to x by vl*8 VFMADD v1, v0, fa0, v1 # v1 += v0 * fa0 (y = a * x + y) SUB a0, a0, t0 # n -= vl (t0) VST v1, a2 # store Y ADD a2, a2, t1 # increment pointer to y by vl*8 BNEZ a0, loop # repeat if n != 0 RET # return
这是我复制的示例代码。请注意,我们没有对浮点和整数寄存器使用f 和x 名称。为了帮助开发人员 更好地记住约定,RISC-V汇编程序定义了许多别名。例如,对于一个函数,参数在寄存器x10 到 x17 中传递。但不是不需要记住这样的任意数字,我们得到了函数参数的别名a0到a7。
t0 至t6 是临时寄存器的别名。这意味着它们不会在函数调用之间被保存。 作为比较,我们得到了下面的ARM SVE代码。让我来概述一下什么寄存器存储什么变量。
daxpy(size_t n, double a, double x[], double y[]) n - x0 register a - d0 float register x - x1 register y - x2 register i - x3 register for the loop counter 这是代码: daxpy: MOV z2.d, d0 // a MOV x3, #0 // i WHILELT p0.d, x3, x0 // i, nloop: LD1D z1.d, p0/z, [x1, x3, LSL #3] // load x LD1D z0.d, p0/z, [x2, x3, LSL #3] // load y FMLA z0.d, p0/m, z1.d, z2.d ST1D z0.d, p0, [x2, x3, LSL #3] INCD x3 // i WHILELT p0.d, x3, x0 // i, n B.ANY loop RET
ARM代码略短,因为ARM指令可以做很多事情。这是我认为使RISC-V代码更容易阅读的一件事。 指令往往只做一件事。没有很多特殊的语法要处理。只需注意一些简单的事情,比如加载向量寄存 器,ARM有多复杂:
LD1D z1.d, p0/z, [x1, x3, LSL #3] 方括号部分计算加载数据的地址:
[x1, x3, LSL #3] = x1 + x3*2³ = x[i * 8] 所以你可以看到x1 代表x 变量的基址。x3 是i 计数器。通过3次左移,我们得到8,这是一个64位浮 点数的字节数。
总结
作为向量编码的初学者,我必须说ARM太复杂了。不是因为ARM不好。我还看了英特尔AVX指令, 它看起来糟糕了10倍。考虑到把握SVE和Neon所需要的精力,我绝对不会花时间去了解AVX。
对我来说,很明显,对于任何想学习汇编代码的人来说,你真的应该从RISC-V开始。对于初学者来说,它只是更容易理解。这并不奇怪。它是专门为在大学里教授而设计的。
由于历史原因,像英特尔x86这样的体系结构非常复杂。它已经存在了几十年,并试图保持向后兼容 性。相比之下,ARM是一个更干净的设计,但变得复杂只是因为工业主要决定设计,而不是可教性 或初学者友好性。
如果你像我一样是一个业余爱好者,只是想跟上技术的发展,比如向量处理是什么,那就给自己找 很多麻烦,读一本RISC-V的书。
人们可能会说ARM或英特尔或其他什么更容易,因为有更多的书和资源。没门。我可以从我过去几 天的亲身经历中告诉你,所有这些文档往往是一个障碍,而不是帮助。这意味着你需要挖掘更多的 材料。你得到了很多基于旧的做事方式的矛盾的东西。
如果你想入门汇编编码,你可以阅读我的一些文章和教程:
* RISC-V Assembly for Beginners
* RISC-V Assembly Code Examples ARM,
* x86 and RISC-V Microprocessors Compared
2、Arm C Language Extensions for SVE 中前端指令接口存在_z,_m,_x 三种形态
_z: setting inactive to zero --> 中端IR 没有后缀的版本
_m: merging with first input --> 中端IR没有后缀的版本
_x: setting inactive to unknown --> 中端IR变成_u后缀的版本