离线
该用户从未签到
有人预言,RISC-V或将是继Intel和Arm之后的第三大主流处理器体系。欢迎访问全球首家只专注于RISC-V单片机行业应用的中文网站
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
本文译自:Erik Engheim的ARM vs RISC-V Vector Extensions
仅作学习用途,转载说明出处!
原文副标题 :RISC-V向量扩展 (RVV) 和ARM可伸缩向量扩展 (SVE/SVE2) 的比较
封面及文中图片来自原文,原文链接 :https://erik-engheim.medium.com/ ... nsions-992f201f402f
能力一般,水平有限,大家凑合着看~~分割线以下为正文!
正文
带有向量指令的微处理器将是未来的大趋势。为什么?因为自动驾驶、语音识别、图像识别都是基于机器学习,并且机器学习都是关于矩阵和向量的。
但这不是唯一的原因。自从我们半官方地宣布摩尔定律结束以来,我们一直在拼命寻找更多的性能。在微处理器设计的黄金旧时代,我们可以容易地每年将CPU的频率翻倍,每个人都很开心。这个绝妙的老把戏结束了。
性能的提高一直停滞不前,因此需要以不同的方式利用更多的晶体管进行并行处理,无论是多核、向量处理还是无序执行。
现在我们耍了几千种不同的把戏找寻更多的性能,无论是增加更多的CPU核心 、还是更高级的分支预测 亦或是SIMD指令 。
所有的把戏都归结为一个中心思想:试图找到并行工作的方法。每当你遍历数组元素并对每个元素进行一些计算时,都有机会进行数据并行。这个循环可以由聪明的编译器编译成一束SIMD或者向量指令。
单指令多数据(SIMD)指令不同于普通的单指令单数据(SISD)指令,每个指令(绿色)处理多个独立的数据流(蓝色)。
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浮点寄存器重叠在同一个寄存器文件中(CPU中的内存保存寄存器)。
不同的寄存器可以映射到同一个寄存器文件的区域。因此,当使用标量浮点运算时,实际上是在使用向量寄存器的一部分。让我们考虑这四种向量寄存器,看看所有这些是如何相关的:
z3 - 可变长SVE2寄存器
v3 - z3最低的128位部分。一个Neon寄存器。
d3 - v3的最低64位。
s3 - d3的最低32位。
然而RISC-V不是这样工作的。RISC-V向量寄存器在一个单独的寄存器文件中,不与标量浮点寄存器共享。
x0-x31 标量整数寄存器
f0-f31 标量浮点寄存器
v0-v31 向量寄存器。长度不在ISA中。
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(最大向量长度)元素的最大数量,你不能超过。
RISC-V寄存器文件可以配置为少于32个寄存器,这是最大值。它可以有例如8个寄存器或2个更大的寄存器。寄存器会占用寄存器文件的所有空间。
这就是有趣的地方。不像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伪代码来演示一下。
i [backcolor=rgba(255, 255, 255, 0.5)]= [backcolor=inherit !important]0 [backcolor=inherit !important]while i [backcolor=rgba(255, 255, 255, 0.5)]< M [backcolor=inherit !important]if x1 [backcolor=rgba(255, 255, 255, 0.5)]< x4 p3[backcolor=inherit !important][ i[backcolor=inherit !important]] [backcolor=rgba(255, 255, 255, 0.5)]= [backcolor=inherit !important]1 [backcolor=inherit !important]else p3[backcolor=inherit !important][ i[backcolor=inherit !important]] [backcolor=rgba(255, 255, 255, 0.5)]= [backcolor=inherit !important]0 [backcolor=inherit !important]end i [backcolor=rgba(255, 255, 255, 0.5)]+= [backcolor=inherit !important]1 x1 [backcolor=rgba(255, 255, 255, 0.5)]+= [backcolor=inherit !important]1 [backcolor=inherit !important]end
从概念上讲,我们根据寄存器x1 是否小于x4 来翻转断言寄存器p3 中的位。因此,在这种情况下,x4 基本上包含向量长度。如果p3 看起来像这样,那么向量长度可能是3。
1110000
因此,我们处理可变向量长度的方式是通过所有操作都使用断言这一事实。考虑这个添加操作。你可以把v0[p0] 看作只从v0 中选取p0 为真的元素。
ADD v4.D, p0/M, v0.D, v1.D ; v4[p0] ← v0[p0] + v1[p0]
好了,现在我们已经做了一点介绍。让我们看一个更完整的代码例子来说明这些指令集在实践中是如何工作的。
DAXPY代码示例
我们将会看到这个C-函数如何以不同的向量指令结束:
[backcolor=inherit !important]void [backcolor=inherit !important]daxpy [backcolor=inherit !important]( size_t n,[backcolor=inherit !important]double a,[backcolor=inherit !important]double x[backcolor=inherit !important][ [backcolor=inherit !important]] ,[backcolor=inherit !important]double y[backcolor=inherit !important][ [backcolor=inherit !important]] [backcolor=inherit !important]) [backcolor=inherit !important]{ [backcolor=inherit !important]for [backcolor=inherit !important]( [backcolor=inherit !important]int [backcolor=inherit !important]64 _ t I [backcolor=rgba(255, 255, 255, 0.5)]= [backcolor=inherit !important]0 ;I [backcolor=rgba(255, 255, 255, 0.5)]< n;[backcolor=rgba(255, 255, 255, 0.5)]++ [backcolor=inherit !important]( I[backcolor=inherit !important]) [backcolor=inherit !important]{ y[backcolor=inherit !important][ I[backcolor=inherit !important]] [backcolor=rgba(255, 255, 255, 0.5)]= x[backcolor=inherit !important][ I[backcolor=inherit !important]] [backcolor=rgba(255, 255, 255, 0.5)]* a [backcolor=rgba(255, 255, 255, 0.5)]+ y[backcolor=inherit !important][ I[backcolor=inherit !important]] ; [backcolor=inherit !important]} [backcolor=inherit !important]}
为什么叫这个奇怪的名字?这是科学工作中流行的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
END
上一篇:
可扩展,免版税,开源,RISC-V有很多要做的 下一篇:
4年新增130家!RISC-V“出圈”稳了?
RISCV作者优文