带有矢量指令的微处理器将成为未来的大事。为什么?因为自动驾驶,语音识别,图像识别都基于机器学习和机器学习都是关于矩阵和向量。
但这不是唯一的原因。自从我们半正式宣布的摩尔法律结束以来,我们一直在墙上敲打着墙上的脑袋,试图为多年来一直进行更多的表现。在微处理器设计的黄金时期,我们每年可以简单地加倍CPU的时钟频率,每个人都开心。那个美妙的老伎俩结束了。
今天我们玩了一千个不同的聪明游戏,以便在加上更多的性能,无论是通过添加更多的CPU内核,增加订单超出次数,更多的预先分支预测器或SIMD指令。
所有这些技巧都归结为一个中心的想法:试图找到并行工作的方法。无论何时何时何时循环多个元素并对每个元素进行一些计算,您都有机会进行数据并行性。此循环可以与巧妙的编译器变成一堆SIMD或矢量说明。
SIMD指令,如霓虹灯,MMX,SSE2和AVX在多媒体应用中工作得很好。做像视频编码的东西e.g.但我们需要在更多领域挤出更多的性能。矢量说明在采取几乎任何循环并将其转换为向量指令提供了更多的灵活性。然而,有很多不同的方式。
在写这篇文章时,我挣扎着。似乎没有任何教导的方式工作。我以为我有点知道矢量指令为我的第一篇文章进行了研究。因此,在完成最后一个故事后,我开始比较了笔记。
这让我意识到ARM和RISC-V实际上遵循了一种深刻的不同策略。这值得覆盖,特别是因为它涉及我珍惜的一些主题。我喜欢简单而优雅,高效的技术:简单的价值。
risc-v矢量延伸与ARM安全性对比时是一种优雅简洁的研究。
在研究方案时,对我来说,为什么我努力掌握它并不明显,但是在拿起我的RISC-V书并重新读取矢量扩展章节,它变得清晰。
要公平,ARM是对英特尔X86汇编代码的复杂混乱的巨大进步。让我们看不到那个。然而,我们无法通过它的事实,即手臂不是那么年轻,并建立了很多遗产。在处理ARM时,您基本上有三种不同的指令集,以涉及:ARM Thumb2,ARM32和ARM64。当您谷歌教程并尝试阅读时,这呈现了许多障碍。人们并不总是提前到他们正在覆盖的指令。
霓虹灯SIMD指令基本上有两个味道:32位和64位。不,位长度不是这里的问题,而是对于64位架构手臂实际上重新设计了他们的整个指令集并改变了相当多的东西。甚至是CPU寄存器的命名约定的东西。
第二个问题是,手臂很大。有超过1000个手臂指示。将其与MOSE 48指令的基础RISC-V指令集进行了对比。这意味着阅读ARM汇编代码并不容易。查看下面的SVE指令:
那里有很多。通过组装的一些经验,您可以猜测LD前缀意味着加载。但是1D是什么意思?你必须看起来。接下来,您在寄存器名称上获得奇怪的后缀,例如.d和/ z。那些人的意思是什么?更多要阅读。然后你有括号[]。您可能猜测这是要撰写地址,但它具有像LSL#3中的奇怪的东西,这意味着留下三次逻辑移位。但是转移了什么?整个东西?只是寄存器x3的内容?更多的东西要查找。
ARM SWE说明简单有很多概念,这不太明显需要时间来包裹头部。我们将有更深入的比较,但让我说risc-v的几句话。
所有RISC-V矢量扩展指令(RVV)的概述适合一本书页面。他们中没有多少,而且与ARM SVE不同,有一个非常简单的语法。以下是RISC-V的矢量负载指令:
这将向矢量寄存器V0加载,其中数据存储在常规整数寄存器X10中的存储器上找到的数据。但加载了多少钱?使用SIMD指令集,如ARM NEON,这由矢量寄存器的名称决定。
还有其他方式做到这一点。我认为这也是实现类似结果的一种方式:
这将加载128位V0寄存器的较低的64位部分。对于SVE2,我们得到另一个变体:
ld1d z0.b,p0 / z,[x10]#负载?字节元素数LD1D z0.d,p0 / z,[x10]#加载双字(64位)元素
在这种情况下,谓词寄存器P0确切地确定了我们加载多少元素。如果p0 = 1110000例如然后我们正在加载三个元素。 V0是Z0的128位下部。
其原因是D和V和Z寄存器位于同一位置。让我澄清。您在每个CPU中有一个名为寄存器文件的内存块。或者更具体地,您可以在CPU中拥有多个寄存器文件。寄存器文件是包含寄存器的内存。所以你不要像常规主内存一样' t访问寄存器文件中的存储器单元。相反,您将使用寄存器名称引用它的部分。
不同的寄存器可以映射到相同寄存器文件的区域。因此,在使用标量浮点操作时,您实际上使用了矢量寄存器的部分。所以让我们考虑第四个向量寄存器,看看所有这些相关的内容如何相关:
RISC-V但不像这样工作。 RISC-V向量寄存器位于未与标量浮点寄存器共享的单独寄存器文件中。
我只能划伤手臂矢量指令的表面,因为它们有很多。只是定位霓虹灯和SVE2的典型负载指令实际上非常耗时。我看了很多ARM文件和博客条目。对RISC-V来做同样的事情是微不足道的。几乎所有RISC-V指令都适合双面纸张。只有三个矢量负载指令:VLD,VLD和VLDX。
我只是放弃了弄清楚多少臂。他们似乎有一个吨,我没有成为专业的ARM汇编代码开发人员的计划。
这是一个非常有趣的部分,因为ARM和RISC-V使用非常不同的方法,我认为RISC-V解决方案的简单性和灵活性真的闪耀。
vsetdcfg - 向量设置数据配置。这为每个元素设置了位大小。类型,是否浮点,签名或无符号整数。它还指定启用有多少矢量寄存器。
setvl - 设置矢量长度。说你想要多少元素。有一个MVL(最大向量长度)的最大数量,您无法超过。
这是它变得有趣的地方。与ARM SVE不同,我可以以几乎无论我想要的方式分区矢量寄存器文件。说寄存器文件有512个字节的内存。然后我可以说我只想要两个矢量寄存器。这为我提供了256个字节。接下来我可以说我想使用32位元素。每个元素换句话说为4字节。这给了我:
两个寄存器:每寄存器512字节/ 2 = 256字节256字节/ 4个字节每个元素= 128个元素
这意味着我可以只使用单个指令添加或多个128个元素。带着胳膊窗扇,你不能这样做。寄存器的数量是固定的,并且如果固定,则为每个寄存器分配的内存。 RISC-V和ARM都允许您使用最多32个向量寄存器,但RISC-V允许您禁用寄存器并给出那些寄存器将习惯剩余的寄存器,从而将它们置于大小。
让我们看起来有点在实践中有效。 CPU知道其寄存器文件的课程是多大的。程序员不知道这一点,也不应该是它。
当程序员使用vsetdcfg来设置元素类型和启用寄存器的数量时,CPU将使用此信息计算最大矢量长度(MVL)。
Li x5,2<< 25#加载寄存器x5,其中2<< 25 vsetdcfg x5#设置数据配置到x5
让我们将此与ARM NEON进行比较,每个寄存器为128位。这意味着与霓虹灯,您可以并行计算两个这些值。但是对于RISC-V来说,这些寄存器中的16个记忆将合并为一个寄存器。因此,您可以通过并行计算32个值。
实际上这并不完全正确。在场景后面将有一些最大数量的浮点乘法器,算术逻辑单元等有限有多少路线,您可以并行执行。但这将是一个实施细节。
无论如何,这将导致MVL值为32.然而,作为开发人员,您可以直接处理此值。 setvl指令如下所示:
因此,如果您尝试将向量长度(VL)设置为5,则将其工作。但是,如果您尝试将其设置为60,则会获得32。因此,达到最大矢量长度(MVL)很重要,当CPU进行CPU时,它不硬连线。相反,它由CPU基于数据配置(元素类型和启用寄存器)计算。
用胳膊你没有特别设置矢量长度。相反,通过使用谓词寄存器,您可以间接设置向量长度。这些是位掩码,您可以在向量寄存器中启用和禁用元素。 RISC-V上也存在谓词寄存器,但没有与ARM上相同的中心作用。
要在ARM上执行相当于SETVL,而是使用一个名为WHILETT的指令,而不是小于:
这个指令有点难以纯粹以书面解释,所以我将使用一些朱莉娅伪代码来展示它。
i = 0,而我< m如果x1< x4 p3 [i] = 1其他p3 [i] = 0结束i + = 1 x1 + = 1结束
概念上,我们在谓词寄存器P3中翻转位,具体取决于寄存器X1是否小于X4。因此,在这种情况下,X4基本上包含矢量长度。如果p3看起来像这样,则传染媒介长度可以是3。
因此,我们处理变量向量长度的方式是通过所有操作使用谓词的事实。考虑此添加操作。您可以认为v0 [p0]只能挑选v0的元素,其中p0为true。
好的,现在我们有点介绍了什么。让我们看看这些指令集在实践中的工作方式更完整的代码示例。
我们将查看该C函数如何最终以不同的向量指令:
void daxpy(size_t n,double a,double x [],double y []){for(int64_t i = 0; i< n; ++ i){y [i] = x [i] * a + y [一世]; }}
为什么古怪的达蓬名字?这是Blas线性代数库中的一个简单功能,在科学工作中流行。对于一些Blas命名为此函数Daxpy,这恰好非常受欢迎,以演示在各种SIMD和Vector指令示例中的实现。这是此数学方程式的实现:
其中a是标量并且x和y是向量。没有矢量指令,我们必须为处理的每个元素循环。但是通过智能编译器,这可以矢量化到RISC-v上的代码。这是一个注释,它解释了什么寄存器对应于什么变量:
daxpy(size_t n,double a,double x [],双y [])n - a0 int寄存器(x10的别名)a - fa0浮动寄存器(f10的别名)x - a1(x11的别名)y - a2( X12的别名
Li T0,2 <2&lt;&lt;&lt; 25 VsetDCFG T0#启用两个64位浮动regs循环:SETVL T0,A0#T0←MIN(MVL,A0),VL←T0 VLD V0,A1#LOAD VECTOR X SLLI T1,T0, 3#T1←VL *2³(以字节为单位)VLD v1,A2#加载向量Y添加A1,A1,T1#增量指针到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添加a2,a2,t1#增量指针到y通过vl * 8 bnez a0,循环#重复如果n!= 0 RET#返回
这是我复制的示例代码。请注意,我们不使用f,以及浮点和整数寄存器的x名称。为了帮助开发人员更好地记住约定,RISC-V汇编程序定义了许多别名。例如。对于函数,参数通过寄存器x10到x17传递。但不必记住这样的任意数字,我们将A0到A7的别名进行函数参数。
T0至T6是寄存器的寄存器的别名。意思是他们不在函数调用之间保存。
为了比较,我们得到了下面的ARM SVE代码。让我概述寄存器存储什么变量。
Daxpy(size_t n,double a,double x [],double y [])n - x0寄存器a - d0浮动寄存器x - x1寄存器y - x2寄存器i - x3寄存器循环计数器
daxpy:mov z2.d,d0 // mov x3,#0 //i whilelt p0.d,x3,x0 //i,n循环: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 i whilelt p0.d,x3,x0 //i,n b.any循环ret
ARM代码略短,因为ARM指令可以多于一件事。这是我认为使RISC-V的一件事更易于阅读。指示往往只是做一件事。处理没有很多特殊的语法。只是注意到类似于加载矢量的简单寄存器,寄存器是如何复杂的手臂:
所以你可以看到x1表示x变量的基地址。 x3是我柜台。通过执行3左移,我们得到八个,这是64位浮点数中的字节数。
作为向量编码的初学者,我必须说手臂刚得太复杂。不是因为手臂很糟糕。我也看了Intel AVX的说明,看起来更糟。鉴于掌握SVE和霓虹灯所需的努力,我绝对不会花时间理解AVX。
对我来说,很明显,对于想要学习装配编码的人来说,你应该真正从RISC-v开始。对于初学者,它只是更容易遵循的大小。这并不令人惊讶。它专门设计用于在大学教授。
与英特尔X86这样的架构以遗留原因很复杂。它已经存在几十年,并试图保持向后兼容性。相比之下,手臂是一种更清洁的设计,但简单地制造了复杂的是,因为行业是主要决定设计,而不是教养或初学者友好。
如果你是像我这样的爱好者,那些只是想及时了解技术如何发展以及矢量处理的东西,然后安全自己有很多麻烦,只读了一个risc-v书。
人们可能会争辩说,武器或英特尔或其他任何更容易,因为有更多的书籍,更多的资源。没有人的方式!我可以在过去几天中从自己的经验中告诉你,所有这些文件的经常都是障碍而不是帮助。这意味着您需要更多地挖掘更多材料。你得到了很多矛盾的东西,基于旧的做事方式。
如果要进入装配编码,您可以阅读我的一些文章和教程: