RISCV舍入问题和中间结果存储问题/浮点并行加速的不稳定性问题以及传统并行算法研究步骤

RISC-V

Posted by Bruce Lee on 2024-01-14

先前处理算术运算时舍入问题和中间结果存储的存储问题

IEEE 754标准提供多种舍入方法,这是因为在连续整数之间的小数,浮点数所能精确表示的个数是有限的,单精度浮点所能精确表达的小数的有效位必须小于23位,双精度则是53位(包含隐藏位)
为了得到所能有的寄存器位数能表示的位,采取了额外添加两个位数来存储中间计算结果,分别是保护位/舍入位
IEEE754标准有四种舍入模式:向上舍入/向下舍入/截断/舍入到最接近的偶数,java仅支持最后一种模式

实际数在两个浮点表示之间的糟糕情况

中间结果的计算,我们提供了两个额外的寄存位来支持舍入,但是在浮点乘法中,我通常需要进行小数移位来对齐指数,尽管多了两个的额外位,但是还是有特殊情况的,这种特殊情况就是,将最小有效位移出了表示范围.
所以我们添加了粘滞位,用来标识是否有非0被移出(舍入位右边).
假设结果在两个浮点表示中间,如果粘滞位为0,表示该数字就是这个数,我们会进行舍入到最近的偶数.如果粘滞位为1,我们就知道实际的数比这个表示出来的数要大,所以向上舍入.

舍入的误差,在多次进行独立的浮点计算时累积问题

每一次浮点计算的舍入误差,在计算次数多的时候,会累加的越来越大.
减少舍入次数,便能减少误差.
IEEE 754-2008标准中添加了新操作:混合乘加:a=a + (b * c)
一条指令执行了乘法和加法,并且仅有一次舍入操作,在加法之后.
这条操作具有更高的浮点性能,当这种操作非常常见时(加速经常性事件)

非规格化浮点数在加速浮点运算单元方面的低性能

尽管非规格化浮点数是我们能够表示更小的小数,但是这种表示法还是超出了浮点表示的范畴,产生了差异,简单源于规整,这里是不规整的,所以许多计算机在遇见非规格化数时引发中断,交给软件来处理.这是低性能的表现

明确关于x86的AVX(YMM)加速计算迭代递增:子字并行

使得DGEMM计算,得到近4倍加速.
C = C + A*B
x86提供先进向量扩展,使得YMM寄存器达到了256位,能够同时支持四个64位浮点数加载到一个宽位寄存器中,进行数据级并行计算,说到底,不管是SSE还是YMM都是SIMD发展出来的,说到底都是宽位寄存器内部并行执行多个数据计算(子字并行)
同时对A进行4个浮点加载,对B也进行4个浮点加载,然后对应向乘,加到C中,每次迭代操作了4个操作数,所以每次最低级迭代是递增4,而不是最外层迭代递增4.
算术谬误和陷阱通常来源于计算机算术的有限精度和自然算术的无限精度之间的差异

正如左移指令可以代替一个乘以2的幂的整数,右移等同于除以一个2的幂的整数 (X)

如果在被处数是负数,我们就不能通过简单的右移来得出正确的结果:因为商的符号由被处数和除数决定,余数由除数决定.
但是,除数是2的幂,哪怕我们在执行算术右移,在某些情况下依然得不到正确结果(如:-5除以4)
所以右移不能简单的代替除法.

浮点加法不满足结合律

c + a + b != c + (a + b)
前提条件是c,a,b都是浮点,且c,a是大数,b是小数字.
1.
大数c + 小数b = 大数c
大树a + 小数b = 大数a
多大的数算大数,多小的数算小数字,并且能产生上面两个等式?
32位单精度浮点数来说,如果大的数和小的数在指数字段对齐之后,两个有效位位数的跨度超过23,则会发生上述情况(如果是边界条件,可能由于保护位/舍入位/粘滞位的存在,出现不一样的结果).
2.
上述的不等式,发生在大的浮点数是正负数.
典型情况是c=2^123 a=-2^123 b=1
前者=1,后者=0

浮点并行加速的不稳定性问题以及传统并行算法研究步骤

并行算法的可行性在很大程度上依赖于算术的结合律,这个定律使得我们无论从何处入手,都能得到最终的结果.
正是此,所以我们可以对一连串数的加法,同时从多处入手,同时计算,最后将中间结果相加.
很多并行原理差不多来源于此,但是我们需要知道,如果某一个计算不符合结合律,如浮点加法,那么很大程度上是无法并行计算的.
如果研究对于浮点加法的并行加速,我们还需要面临下面的问题:
在并行计算机上,调度器会调度处理器帮助我们并行计算,但是能调度的处理器数量每次都不相同(由于其他程序占用),所以我们在执行并行计算时,可能会从多处不同的地方入手,每次执行都不一样.这导致了每次执行结果的差异
如何验证并行计算的可信性,这里就属于数值分析领域.

我们应该都是向编写串行程序,来得出一个正确的答案,然后再研究并行程序,来尝试加速程序,获得更高性能,在研究并行程序时,我们需要串行程序的结果来验证并行程序的可信性.
只有理论数学家才关心浮点精度 (X)

处理器设计的一些想法

对于处理器设计,首先知道的不仅是需要面向哪种指令系统体系结构来设计,不同的指令系统,对于设计处理器硬件的实现有难易.
面向RISC-V指令系统设计,首先得益于最好的简单源于规整的原则,设计存取指令的数据通路单元时,我们首先将指令地址值存储在指令地址存储器中,然后交给指令寄存器,指令寄存器将地址中的值取出,分析对应字段的寄存器,来选择某几个寄存器进行操作.
然后面向指令类型来设计,因为不同的指令类型,对应于对寄存器或立即数不同的操作.
首先就是先形成一个小处理器,具备一个处理器的所有特征:算术逻辑运算(加减与或),分支指令(beq),存取指令(ld,sd)
存取指令用于取指令地址,算术指令用于计算,分支指令用于跳转.

最简单的处理器,最糟糕的地方在哪里

时钟周期长度受限于"大指令"

1.所有的指令中具有最长执行时间的指令,要小于等于时钟周期长度.
a.因为采用了边沿触发设计,这样我们确保了时钟同步,避免了读写发生冲突,写入发生在每一个有效沿,通常来说读是一直可读的.
b.所有的指令的执行,必须在一个周期内完成.如果无法完成(加入在执行写入操作,在这个周期内没有完成所有的计算,则在下一次有效沿来临时,无法更新值.),这是失败的设计
c.所有状态单元,无法在一个周期内部重复使用,这个其实是每一个处理器设计方法中都具有的.一般是将功能单元(数据通路单元)复制多个使用.
这些设计导致的后果就是,如果一个指令完成只需要小半个周期,那么个别几个"大指令"延长了时钟周期长度,由于amdhal定律,这在具有大量小指令的程序中,性能是极低的.

指令存储器和数据存储器分开的理由

计算机很重要的一个点就是,数位没有任何内在含义.我们对内存的值提取,然后给予不同的解读,它便有不同的含义.
我们可以很简单的将内存的值提取到一个数据通路单元中,不管它是指令还是数据.这样做的后果就是,这是一个失败的处理器,因为那单周期处理器,数据通路单元是不能够重复使用的.
所以我们需要将指令存储器和数据存储器分开.

遗留问题

时钟信号从来不在状态单元之外通过任何门电路,因为这样可能导致时序问题

多级传控实例:主控和ALU控制器

控制器通过操作码来计算控制变量,一般的对象就是指令类型,不同的指令类型,给出不同的控制线使能信号.
一般而言,对于一些无关项,需要设计者给出,这对于整个数据通路都需要深入理解.
通过多级传控,来达到缩小控制器的复杂度和空间,同时也为流水线加速贡献.
不同的指令,在一些数据通路单元中是共享的,这些就是要通过控制器来进行耦合.

关于总线

实现一个32位单周期的RISC-V处理器,总线宽度就是32位,总线从指令寄存器衍生出来后,被寄存器堆读取,或者被立即数生成数读取,但是这些都是读取总线中(总线中传输的数据就是指令二进制码)的对应的数位.并没有以前遐想的"指令数据分发",而是"全发",按需自取.

单周期处理器的淘汰

尽管上述也讲了主要的原因,但是其根源是违反了设计原则之一:加速经常性事件

流水线加速比为何是工序数

在单周期处理器中,假如一条指令的工序有4步,一个时钟周期内,每一个工序只工作一次.
如果此时使用流水线,将指令分解,那么在每一个任务周期内部,每一个工序都在工作,周期内工序工作了4次.
由于流水线建立和工序之间的不平衡问题,加速比是小于4的,但是当指令数量上来后,接近于4.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !