Arm浮点特性(四)浮点特殊值处理规则

在上一篇中,我们梳理了规范浮点数的计算规则。不过,浮点数不只有规范数一类编码,还包括NaN、无穷大和非规范数这样的特殊值。对于特殊值的处理,规则非常繁琐而且实现代价不小。对于Arm架构尤其如此,主要原因有两点:

  1. 为了与其他架构(主要是x86架构)兼容,Arm提供了两种模型对于特殊值进行处理,即FEAT_AFP特性。这样做可以直接获得与其他架构相同的浮点数计算结果,而不需要软件额外处理。但是使得硬件实现难度很大。
  2. 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.FZFPCR.FZ16FPCR.FIZ。只有在AArch64状态而且实现了FEAT_AFP的情况下,FPCR.FIZ的有效值才会对浮点计算行为产生影响,但是不要求使能FEAT_AFP。

清零非规范数的控制和行为

FPCR.FZFPCR.FZ16FPCR.FIZ都会控制清零非规范数输入的行为。

不使能AH使能AH
FPCR.FZ清除DP/SP/BF16格式的非规范输入。
清除DP/SP/BF16格式的非规范输出。
清除DP/SP/BF16格式非规范数输出。
FPCR.FZ16清零HP数据处理指令的非规范输入,但是不清除从HP到其他格式转换指令的非规范输入。
清除HP格式非规范输出。
同左
FPCR.FIZ清零DP/SP/BF16格式的非规范输入。
不控制非规范数输出的行为。
同左

不服从这三个比特控制的情况:

  • FABSFNEG指令不会将非规范数的输入和输出清零。FABSFNEG直接操作符号位,不关心其他位置。
  • FMAX{P|V}FMIN{P|V}BFMAX和BFMIN不会清除非规范输出。因为这些指令的结果与输入操作数之一是相同的。如果没有清零非规范数输入,那么也不需要清除非规范数输出;如果清除了非规范数输出,那么结果就不会是非规范数。
  • FPCR.AH=1时,FRECPEFRECPSFRECPXFRSQRTEFRSQRTS指令总是清除非规范输入和输出,就好像FPCR.{FZ, FIZ}全是1。
  • FPCR.AH=1时,BFCVT{N|N2|NT}指令以及BFML[A|S]L{B|T}指令总是清除非规范输入和输出,就好像FPCR.{FZ, FIZ}全是1。
  • FPCR.EBF=0时,BFDOTBFVDOTBFMMLABFMOP[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指令BFDOTBFVDOTBFMMLABFMOP[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指令产生特殊值的规则

FABSFNEG指令只会处理符号位,对于输入的数值并不关心。如果输入是零、无穷大或NaN,那么输出也是零、无穷大或NaN,但是符号位可能不同。FABS结果的符号固定为0;FNEG结果的符号是输入符号取反。这可能是唯一一个浮点指令返回sNaN的情况。

不过,当FPCR.AH=1时,FABSFNEG指令会保持NaN的符号位不变。此外,零和无穷大的符号还是按照操作功能变化。因此,当FPCR.AH=1时,FABSFNEG指令只能用来操作DP/SP/HP,不能扩展到BFloat16格式。

最大值/最小值指令产生特殊值的规则

最大值和最小值函数返回的是输入本身,不产生新的数,因此对于特殊值的处理方式与算数指令的处理过程不同。

对于FMAX/FMINBFMAX/BFMIN指令:

  • FPCR.AH=0时,

    • 如果两个输入都是零,那么正零大于负零。
    • 如果输入之一是NaN,

      • 如果FPCR.DN=0,结果是由输入NaN派生的qNaN;
      • 如果FPCR.DN=1,结果是默认NaN。
  • FPCR.AH=1时,

    • 如果两个输入都是零,那么不管输入的符号关系,总是选择第二个输入。
    • 如果任一输入是NaN,不管FPCR.DN,总是选择第二个输入。同时触发无效操作异常(IOE)。

对于FMAXNM/FMINNMBFMAXNM/BFMINNMFCLAMPBFCLAMP指令,行为不受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=0FPCR.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浮点特性(三)浮点数运算操作
下一篇:
推荐阅读
关注数
7
文章数
7
编程、模型、手工
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息