在上一篇中,我们梳理了规范浮点数的计算规则。不过,浮点数不只有规范数一类编码,还包括NaN、无穷大和非规范数这样的特殊值。对于特殊值的处理,规则非常繁琐而且实现代价不小。对于Arm架构尤其如此,主要原因有两点:
- 为了与其他架构(主要是x86架构)兼容,Arm提供了两种模型对于特殊值进行处理,即FEAT_AFP特性。这样做可以直接获得与其他架构相同的浮点数计算结果,而不需要软件额外处理。但是使得硬件实现难度很大。
- Arm对于浮点数的扩展通过多个扩展集逐步增加。FEAT_FP只包含FP64和FP32两种浮点格式,对于FP16、BF16和FP8格式的支持则分别通过FEAT_FP16、FEAT_BF16和FEAT_FP8等扩展集实现。这使得不同浮点格式对于特殊值的处理规则不同。这样做虽然可以简化硬件设计,但是增加了学习成本。
对于浮点特殊值的处理,包括下面三个要点:
- 如果输入和输出是非规范数,可以选择清零非规范数。
- 如果输入是NaN,那么根据输入NaN产生输出NaN,称为NaN广播(NaN propagation)。
- 如果输入是无穷大或零,则可能产生无穷大、零或者NaN。
预警:本篇充满了枯燥的规则和支离破碎的细节,建议配合零食快乐水食用。
清零非规范数
在IEEE-754标准中,非规范数也是作为数值参与计算的。处理非规范数比处理规范数会增加一些步骤。在计算处理前,需要根据尾数部分中前导零的数量,调整指数和尾数,使得尾数也是与规范数的尾数对齐的定点数格式。这样的调整还会使得指数部分出现负数。类似地,处理完成后,如果输出结果是非规范数,还需要调整尾数来对齐指数到0。
在很多算法和应用中,将非规范数直接当作零处理,不会引入明显的误差,但是可能减少处理的复杂度,从而获得更快的运算速度。因此,Arm架构引入了非规范数清零机制,分为清零非规范数输入和清零非规范数输出。X86架构同样引入了清零非规范数机制,称为Flush-To-Zero和Denormals-Are-Zeros。
注意:非规范数清零与IEEE-754标准并不兼容,在IEEE-754中完全没有相关的定义或建议。如果严格要求计算结果与IEEE-754兼容时,不能使用非规范数清零机制。
FPCR
寄存器中有三个比特与非规范数清零机制的控制有关:FPCR.FZ
、FPCR.FZ16
和FPCR.FIZ
。只有在AArch64状态而且实现了FEAT_AFP的情况下,FPCR.FIZ
的有效值才会对浮点计算行为产生影响,但是不要求使能FEAT_AFP。
清零非规范数的控制和行为
FPCR.FZ
、FPCR.FZ16
和FPCR.FIZ
都会控制清零非规范数输入的行为。
不使能AH | 使能AH | |
---|---|---|
FPCR.FZ | 清除DP/SP/BF16格式的非规范输入。 清除DP/SP/BF16格式的非规范输出。 | 清除DP/SP/BF16格式非规范数输出。 |
FPCR.FZ16 | 清零HP数据处理指令的非规范输入,但是不清除从HP到其他格式转换指令的非规范输入。 清除HP格式非规范输出。 | 同左 |
FPCR.FIZ | 清零DP/SP/BF16格式的非规范输入。 不控制非规范数输出的行为。 | 同左 |
不服从这三个比特控制的情况:
FABS
和FNEG
指令不会将非规范数的输入和输出清零。FABS
和FNEG
直接操作符号位,不关心其他位置。FMAX{P|V}
、FMIN{P|V}
、BFMAX和BFMIN
不会清除非规范输出。因为这些指令的结果与输入操作数之一是相同的。如果没有清零非规范数输入,那么也不需要清除非规范数输出;如果清除了非规范数输出,那么结果就不会是非规范数。- 当
FPCR.AH=1
时,FRECPE
、FRECPS
、FRECPX
、FRSQRTE
和FRSQRTS
指令总是清除非规范输入和输出,就好像FPCR.{FZ, FIZ}
全是1。 - 当
FPCR.AH=1
时,BFCVT{N|N2|NT}
指令以及BFML[A|S]L{B|T}
指令总是清除非规范输入和输出,就好像FPCR.{FZ, FIZ}
全是1。 - 当
FPCR.EBF=0
时,BFDOT
、BFVDOT
、BFMMLA
和BFMOP[A|S]
指令总是清除非规范数输入、输出以及中间结果,就好像FPCR.{FZ, FIZ}
全是1。 - FP8指令不会将非规范数的输入和输出清零,就好像
FPCR.{FZ, FIZ, FZ16}
全是0。
清零非规范数可能会触发非规范数输入异常(IDC),在下一篇中介绍浮点异常。
无穷大、零和NaN特殊值的规则
如果指令的输入是无穷大、零或NaN,则需要按照特定的规则产生输出。不同的指令适用不同的规则。
算数计算指令产生特殊值的规则
算数计算指令的处理过程,可以分为以下步骤(伪代码可以参阅Arm A-profile A64 Instruction Set Architecture中的FPADD):
- 如果使能,将输入非规范数清零。
- 如果输入中有NaN,根据NaN传播规则派生结果。如果派生出NaN,则直接返回,不再继续进行操作。
- 如果输入中有无穷大或零,则根据计算条件检查无效操作的条件。如果条件满足,则返回NaN并且发出IOE异常,不再继续进行操作。
- 检查生成无穷大的条件。如果条件满足,则返回无穷大,可能会产生DZE异常,不再继续进行操作。
- 检查生成零的条件。如果条件满足,则返回零,不再继续进行操作。
- 如果上面三组条件都成立,则按照无限范围和精度进行操作,并且舍入到目标格式。舍入过程可能产生无穷大、非规范数或零。
- 如果使能FPCR.AH,则检查是否触发IDE。
产生NaN的条件
虽然输入没有NaN,但是无法从数学上得出有效的结果,那么浮点操作产生qNaN。在Arm结构中,选择默认NaN作为返回结果。这通常标识计算无效,同时触发IOE异常。
- 加法:(+inf)+(-inf)=defaultNaN;(-inf)+(+inf)=defaultNaN
- 减法:(+inf)-(+inf)=defaultNaN;(-inf)-(-inf)=defaultNaN
- 乘法:inf0=defaultNaN;0inf=defaultNaN
- 除法:inf/inf=defaultNaN;0/0=defaultNaN
- 融合乘加:乘法的输入满足产生NaN的条件,产生默认NaN;加法的输入(累加数和乘积)满足产生NaN的条件,产生默认NaN。
- 规约加法:任意两个输入都是无穷大,而且两个输入符号相反,产生默认NaN。
- 规约乘加:任意一对乘法的输入满足产生NaN的条件,产生默认NaN;规约加法的输入(累加数和所有乘积)满足产生NaN的条件,产生默认NaN。
- 开方倒数和开方:x<0时,产生defaultNaN。
- 倒数和缩放:不会生成NaN。
产生无穷大的条件
产生无穷大的规则如下。
加法:
- (+inf)+y=+inf,y不是负无穷大(规范数、非规范数或正无穷大)
- x+(+inf)=+inf,x不是负无穷大(规范数、非规范数或正无穷大)
- (-inf)+y=-inf,y不是正无穷大(规范数、非规范数或负无穷大)
- x+(-inf)=-inf,y不是正无穷大(规范数、非规范数或负无穷大)
减法:
- (+inf)-y=+inf,y不是正无穷大(规范数、非规范数或负无穷大)
- x-(-inf)=+inf,x不是负无穷大(规范数、非规范数或负无穷大)
- (-inf)-y=-inf,y不是负无穷大(规范数、非规范数或正无穷大)
- x-(+inf)=-inf,x不是正无穷大(规范数、非规范数或负无穷大)
乘法:结果符号是两个输入的符号异或
- inf*y=inf,y不是零或无穷大(规范数、非规范数)
- x*inf=inf,x不是零或无穷大(规范数、非规范数)
- inf*inf=inf
除法:结果符号是两个输入的符号异或。只有第二个输入是0时,产生DZE异常。
- inf/y=inf,y不是零或无穷大(规范数、非规范数)
- x/0=inf,x不是零或无穷大(规范数、非规范数)
- inf/0=inf
- 融合乘加:首先,根据乘法的计算规则判断乘积的值(无穷大或零);然后,加法的输入(累加数和乘积)满足产生无穷大的条件。按照加法的规则产生无穷大。
- 规约加法:输入之一是正无穷大,其他输入都不是负无穷大,产生正无穷大;输入之一是负无穷大,其他输入都不是正无穷大,产生负无穷大。
- 规约乘加:根据乘法的计算规则判断所有乘积的值(无穷大或零);规约加法的输入(累加数和所有乘积)满足产生无穷大的条件。按照规约加法的规则产生无穷大。
- 开方倒数:rsqrt(+0)=+inf;rsqrt(-0)=-inf。
- 开方:sqrt(+inf)=+inf。
- 倒数:1/0=inf。结果符号与输入相同。产生DZE异常。
- 缩放:scale(inf,N)=inf。结果符号与输入相同。
对于除法、倒数和开方倒数操作,如果第二个输入(除数)是零,而且第一个输入(被除数)不是零,那么认为结果是无穷大的,并且触发DZE异常。
产生零的条件
产生零的规则如下。
- 加法:(+0)+(+0)=+0;(-0)+(-0)=-0。
- 减法:(+0)-(-0)=+0;(-0)-(+0)=-0。
乘法:结果符号是两个输入的符号异或
- 0*y=0,y不是零或无穷大(规范数、非规范数)
- x*0=0,x不是零或无穷大(规范数、非规范数)
- 0*0=0
除法:结果符号是两个输入的符号异或。只有第二个输入是0时,产生DZE异常。
- 0/y=inf,y不是零或无穷大(规范数、非规范数)
- x/inf=0,x不是零或无穷大(规范数、非规范数)
- 0/inf=0
- 融合乘加:首先,根据乘法的计算规则判断乘积的值(无穷大或零);然后加法的输入(累加数和乘积)满足产生零的条件。按照加法的规则产生零。
- 规约加法:所有输入都是零,且所有输入符号相同,产生与输入符号相同的零。
- 规约乘加:根据乘法的计算规则判断所有乘积的值(无穷大或零);规约加法的输入(累加数和所有乘积)满足产生零的条件。按照规约加法的规则产生零。
- 开方倒数:rsqrt(+inf)=+0。
- 开方:sqrt(+0)=+0;sqrt(-0)=-0。
- 倒数:1/inf=0。结果符号与输入相同。
- 缩放:scale(0,N)=0。结果符号与输入相同。
NaN广播
当浮点操作的输入中存在NaN时,浮点操作的结果是从输入的NaN派生出的静默NaN。在IEEE-754中没有规定选择哪一个NaN来派生静默NaN。Arm架构中提供了两套规则来确定从哪个输入NaN派生结果NaN。
如果
FPCR.AH=0
,按照从左至右的顺序检查指令的输入。- 如果输入中存在信号NaN,则从第一个信号NaN派生出的静默NaN。
- 如果输入中不存在信号NaN,则从第一个NaN派生出的静默NaN。
如果
FPCR.AH=1
,- 如果操作具有两个浮点输入,而且其中只有一个NaN输入,那么从这个NaN操作数派生出静默NaN。
- 如果操作具有两个浮点输入,而且具有两个NaN输入,那么从第一个输入派生静默NaN。
如果操作是具有三个浮点输入的乘累加和乘累减操作,
- 如果三个输入都是NaN,或者两个输入是NaN且乘法的第一个输入是NaN,那么这个NaN派生静默NaN。
- 如果有两个输入是NaN且乘法的第一个输入不是NaN,那么从乘法的第二个输入派生静默NaN。
FPCR.DN控制如何从选择的输入NaN派生出结果NaN。
如果
FPCR.DN=0
,那么根据选择的NaN派生静默NaN:- 如果输入和输出格式相同,那么将尾数最高位置1。
- 如果输出格式比输入格式宽,那么将尾数最高位置1,并且给尾数补零以匹配输出格式。
- 如果输出格式比输入格式窄,那么将尾数最高位置1,并且截断尾数以匹配输出格式。
- 如果
FPCR.DN=1
,那么直接返回默认NaN。
以下指令不受FPCR.DN
控制:
- 所有FP8指令总是按照
FPCR.DN=1
行为。 - BFloat16指令
BFDOT
、BFVDOT
、BFMMLA
、BFMOP[A|S]
总是按照FPCR.DN=1
行为。
静默NaN和信号NaN
虽然在第一篇中就介绍了sNaN和qNaN,但是直到这里我们才能比较清晰地解释qNaN和sNaN的区别和使用。
按照IEEE-754标准的设想,NaN的尾数部分是用来传递一些软硬件实现所需要的信息的。软硬件可以在尾数部分填入特定的编码,从而方便定位运行过程出现的错误。因此,NaN不是一个值,而是一个编码区间。
IEEE-754标准没有定义尾数部分的编码方式,但是对尾数的最高位进行了定义,即sNaN和qNaN。并且规定,当浮点操作的输入之一是sNaN时,浮点操作会触发IOE异常;当浮点操作的输入之一是qNaN,但是没有sNaN时,浮点操作不需要触发IOE异常。显而易见,sNaN可以用来上报输入的错误。
另一方面,浮点操作不会产生sNaN。在前面列出的产生NaN的规则中,浮点操作返回的都是默认NaN,也都是NaN(FP8 E4M3除外)。同时,按照NaN传播的规则,所有的输入sNaN都会被派生成qNaN。这就保证了上一条浮点操作的结果,不会在下一条浮点操作触发异常。
综上,可以得出这样的结论:sNaN只能作为在一串浮点操作的输入出现,所有的中间结果和最终的结果都只能出现qNaN。
舍入过程产生无穷大或零
如果无限范围和精度的计算结果就是精确的零,那么根据舍入方式确定符号:
- 如果舍入方式是RM,那么产生负零,此外都产生正零。
如果无限范围和精度的计算结果不是零,那么需要将结果舍入到目标格式。此时,可能根据格式转换指令的规则产生无穷大或零。
如果输出是非规范数,那么FPCR.FZ
可以控制将非规范数清零,这也会产生零作为结果。
ABS和NEG指令产生特殊值的规则
FABS
和FNEG
指令只会处理符号位,对于输入的数值并不关心。如果输入是零、无穷大或NaN,那么输出也是零、无穷大或NaN,但是符号位可能不同。FABS
结果的符号固定为0;FNEG
结果的符号是输入符号取反。这可能是唯一一个浮点指令返回sNaN的情况。
不过,当FPCR.AH=1
时,FABS
和FNEG
指令会保持NaN的符号位不变。此外,零和无穷大的符号还是按照操作功能变化。因此,当FPCR.AH=1
时,FABS
和FNEG
指令只能用来操作DP/SP/HP,不能扩展到BFloat16格式。
最大值/最小值指令产生特殊值的规则
最大值和最小值函数返回的是输入本身,不产生新的数,因此对于特殊值的处理方式与算数指令的处理过程不同。
对于FMAX
/FMIN
和BFMAX
/BFMIN
指令:
当
FPCR.AH=0
时,- 如果两个输入都是零,那么正零大于负零。
如果输入之一是NaN,
- 如果
FPCR.DN=0
,结果是由输入NaN派生的qNaN; - 如果
FPCR.DN=1
,结果是默认NaN。
- 如果
当
FPCR.AH=1
时,- 如果两个输入都是零,那么不管输入的符号关系,总是选择第二个输入。
- 如果任一输入是NaN,不管FPCR.DN,总是选择第二个输入。同时触发无效操作异常(IOE)。
对于FMAXNM
/FMINNM
、BFMAXNM
/BFMINNM
、FCLAMP
和BFCLAMP
指令,行为不受FPCR.AH
控制。
- 如果两个输入都是零,那么正零大于负零。
- 如果一个输入是qNaN,另一个输入是不是NaN,那么结果是非NaN的输入。
如果一个输入是sNaN,或两个元素都是NaN:
- 如果
FPCR.DN=0
,结果是由输入NaN派生的qNaN; - 如果
FPCR.DN=1
,结果是默认NaN。
- 如果
转换指令产生特殊值的规则
转换指令只有一个输入,所以规则会简化很多。
- 如果输入是NaN,那么输出受FPCR.DN控制。
- 如果输入是无穷大,那么输出是无穷大。
- 如果输入是零,那么输出是零。
- 如果输入是规范数和非规范数,则需要将输入格式舍入到目标格式。
算数指令也计算出一个无限范围和精度的结果,然后舍入到目标格式。算数指令和转换指令的舍入共享相同的规则。Arm A-profile A64 Instruction Set Architecture中的FpRoundBase是Arm中最常用的舍入伪代码,是Arm-ARM中最长的几个伪代码函数之一。
使能或禁用FEAT_AFP时,可以将上面的伪代码拆分出如下流程:
FPCR.AH=0 | FPCR.AH=1 |
---|---|
如果使能了清零非规范数输出,那么检查舍入后的结果是否是非规范数。如果舍入结果是非规范数,那么直接返回零,并且可能触发下溢出异常(UFC)。 | |
如果舍入的输入小于目标格式的最小精度,那么可能触发下溢出异常(UFC)。 | |
计算指数、误差。 指数最小值为0。 | 计算指数、误差。 指数无范围限制可以为负数。 |
根据舍入规则进行舍入。 | 根据舍入规则进行舍入。 |
如果使能了清除非规范数输出,那么检查舍入后的结果是否是非规范数。如果舍入结果是非规范数,那么直接返回零,并且可能触发不精确异常(IXE)或下溢出异常(UFC)。 | |
如果舍入的结果超过了目标格式能够表示的范围,也可能会产生无穷大: - 如果舍入方式为RN,那么会产生正无穷大或负无穷大。 - 如果舍入方式是RP,那么只会产生正无穷大。 - 如果舍入方式是RM,那么只会产生负无穷大。 - 如果舍入方式是RA,那么会产生正无穷大或负无穷大。 - 如果舍入方式是RO,而且用于BFloat16指令,产生正无穷大或负无穷大。 | 如果舍入的结果超过了目标格式能够表示的范围,也可能会产生无穷大: - 如果舍入方式为RN,那么会产生正无穷大或负无穷大。 - 如果舍入方式是RP,那么只会产生正无穷大。 - 如果舍入方式是RM,那么只会产生负无穷大。 - 如果舍入方式是RA,那么会产生正无穷大或负无穷大。 - 如果舍入方式是RO,而且用于BFloat16指令,产生正无穷大或负无穷大。 |
检查误差,可能触发不精确异常(IXE)。 | 检查误差,可能触发不精确异常(IXE)。 |
显而易见,FEAT_AFP对于下溢出检测的位置不同。 当FPCR.AH=0
时,在舍入前进行下溢出检查;当FPCR.AH=1
时,在舍入后进行下溢出检查。
此外,从伪代码细节上可以发现,当FPCR.AH=0
时,计算的指数最小值就是0;当FPCR.AH=1
时,计算的指数可能为负数。这会导致一些特定输入的计算结果不同。
比较指令对于无穷大、零和NaN的处理
比较指令直接设置NZCV标志,不产生数值结果,也不会产生无穷大、零和NaN这些特殊值。当无穷大和NaN参与比较时,不同的谓词会采取不同的处理方式。
比较NE/EQ/GE/GT:
如果输入之一是NaN(包括qNaN或sNaN),比较结果为NE。
- 对于NE和EQ,如果输入之一是sNaN,触发IOE异常。
- 对于GE和GT,如果输入之一是sNaN或qNaN,就会触发IOE异常。
如果输入存在无穷大或零,直接比较数值。
- 无穷大也作为一个数值参与比较。如果两个输入都是正无穷大或负无穷大,认为结果是EQ。
比较UN:
- 如果输入之一是sNaN,触发IOE异常。
- 如果输入之一是NaN,比较结果为真。
上一篇:Arm浮点特性(三)浮点数运算操作
下一篇: