第六章 浮点数
所有的计算机程序都处理数字。然而,对于不熟悉其详细实现的程序员来说,浮点数有时可能显得反直觉。在查看ARM处理器上的浮点实现之前,包含了对浮点基础知识的简短概述。具有浮点数经验的程序员可能想要跳过以下部分。
6.1 浮点数基础与IEEE-754标准
IEEE-754标准几乎是所有现代计算机浮点数数学实现的参考,包括ARM浮点系统。原始的IEEE-754-1985标准在IEEE-754-2008的发布后进行了更新。该标准精确地定义了每个基本浮点操作对所有可能输入值产生的结果。它描述了一个合规实现应该如何处理无法精确表达的结果的舍入问题。一个简单的例子是这样的计算:1.0 ÷ 3.0,在十进制或二进制表示中需要无限多的数字才能精确表达。
IEEE-754提供了多种不同的舍入选项来应对这个问题(向正无穷大舍入,向负无穷大舍入,向零舍入,以及两种向最近值舍入的方式,参见第六章的舍入算法)。IEEE-754还规定了当发生异常操作时的结果。这意味着一个可能表示问题的计算。这些条件可以通过查询FPSCR(在ARM处理器上)或设置陷阱处理程序(在某些系统上)来测试。异常操作的例子包括:
溢出:结果太大而无法表示。
下溢:结果太小以至于失去精度。
不精确:结果无法在不损失精度的情况下表示。很明显,许多浮点计算都会落入这个类别。
无效:例如,尝试计算负数的平方根。
除以零:尝试除以零。
规范还描述了当检测到上述异常操作之一时应采取的行动。可能的后果包括为无效操作生成NaN(不是数字)结果,为溢出或除以零生成正或负无穷大,或者在下溢的情况下生成非规范化数。标准定义了如果后续的浮点计算操作NaN或无穷大时应产生什么结果。
IEEE-754定义的一件事是如何在硬件中表示浮点数。浮点数通常使用单精度(32位)或双精度(64位)表示。VFP支持硬件中的单精度(32位)和双精度(64位)格式。此外,VFPv3可以有半精度扩展,以允许使用16位值进行存储。
浮点格式使用可用空间来存储关于浮点数的三个信息片段:
符号位(S),显示数字是正(0)还是负(1)。
指数,给出其量级。
尾数,给出数字的二进制小数位。
例如,对于单精度浮点数,字的第[31]位是符号位[S],位[30:23]给出指数,位[22:0]给出尾数。参见下图。
然后,数字的值是±m × 2^exp,其中m从尾数派生,exp从指数派生。
尾数并不是通过直接取23位二进制值生成的,而是解释为位于二进制小数点右侧,同时左侧存在一个1。换句话说,二进制尾数必须大于或等于1且小于2。对于零的情况,这是通过将指数和尾数位全部设置为0来表示的。还有其他特殊情况的表示法,用于表示正无穷大和负无穷大,以及非数字(NaN)值。特殊情况之一是非规格化值。
符号位使我们能够区分正无穷大和负无穷大以及NaN表示法。同样,8位指数值用于给出+128到-127范围内的值,因此在编码中隐含了-127的偏移量。下表总结了这一点。
让我们考虑一个例子:
十进制值+0.5表示为单精度浮点数时,其十六进制值为0x3F000000。这表示符号值为0(正数)。
尾数的值是1.0,尽管整数部分(1)是隐含的,并未存储。指数值在位[30:23]中指定——即0b01111110,或126——减去127的偏移量,表示指数为-1。
因此,该值表示为:
(-1)^(sign) × 尾数 × 2^(指数) = 1 × 1 × 2^(-1) = 0.5(十进制)
非规格化数是一个特殊情况。如果将指数位设置为零,通过设置尾数位,你可以表示比零大得多的非常小的数字。由于正常值有一个隐含的前导1,最接近零的正常值为±2^(-126)。
为了表示更小的数字,尾数值的1.m解释被0.m解释取代。此时,数字的大小仅由位的位置决定。在使用这些极小的数字时,可用的精度不会随数值的大小而变化。由于尾数前面没有隐含的1,所有低于最低设置位的位都是前导零,因此最小的可表示数为1.401298464e-45,表示为0x00000001。
出于性能原因,这种非规格化值通常被忽略,并被冲零。这严格来说违反了IEEE-754标准,但在实际程序中非规格化值的使用频率足够低,因此性能提升比正确处理这些极小数值更为重要。具有VFP的Cortex处理器允许代码在冲零模式和完全支持非规格化数之间进行选择。
由于32位浮点数具有23位的尾数,因此如果将32位整数转换为32位浮点数,许多值无法被精确表示。这被称为精度损失。如果你将这些值之一转换为浮点数并再转换回整数,你会得到一个不同的、接近的值。在双精度浮点数的情况下,指数字段有11位(指数范围为-1022到+1023),尾数字段有52位。
6.1.1 取整算法
IEEE 754-1985标准定义了四种不同的结果取整方式,如下所示:
四舍五入到最接近值(偶数舍入):这种模式将结果取整到最接近的值。如果一个数字正好位于两个可能值的中间,则将其舍入到最低有效位为零的最接近值。
朝零方向取整:这种模式始终将数字向零方向取整(也可以视为截断)。
朝正无穷大方向取整(+∞):选择向正无穷大方向取整。
朝负无穷大方向取整(-∞):选择向负无穷大方向取整。
IEEE 754-2008标准增加了另一种取整模式。在四舍五入到最接近值的情况下,现在也可以将正好位于两个值中间的数字朝远离零的方向取整(换句话说,正数向上取整,负数向下取整)。这是在原有选择将其舍入到最低有效位为零的最接近值之外的新选项。目前,VFP不支持这种取整模式。
6.1.2 ARM VFP
VFP是ARMv7-R架构中符合IEEE 754标准的指令集的一个可选(但很少被省略)的扩展。它可以实现为32个或16个双字寄存器。VFPv3-D32和VFPv3-D16这两个术语用于区分这两种选择。VFPv3还可以通过半精度扩展来进行可选扩展,这些扩展提供了在半精度浮点(16位)和单精度浮点(32位)之间进行双向转换的功能。这些操作仅允许将半精度浮点数转换为其他格式或从其他格式转换为半精度浮点数。
VFPv4在VFPv3的基础上增加了半精度扩展和融合乘加指令。在融合乘加操作中,只在最后进行一次舍入。这是IEEE 754-2008规范的新特点之一。融合操作可以提高重复累积乘积的计算精度,如矩阵乘法或点积计算。各个Cortex-R系列处理器支持的VFP版本在下表中给出。
还有许多其他的VFP寄存器。以下是列表:
浮点系统ID寄存器(FPSID)
系统软件可以读取该寄存器,以确定硬件支持哪些浮点功能。浮点状态和控制寄存器(FPSCR)
该寄存器保存比较结果和异常标志。控制位选择舍入选项并启用浮点异常捕获。浮点异常寄存器(FPEXC)
FPEXC寄存器包含允许系统软件处理异常的位,以确定发生了什么。媒体和VFP功能寄存器0和1(MVFR0和MVFR1)
这些寄存器使系统软件能够确定处理器实现中提供了哪些高级SIMD和浮点功能。
用户模式代码只能访问FPCSR。这意味着应用程序无法读取FPSID以确定支持哪些功能,除非主操作系统提供此信息。例如,Linux通过 /proc/cpuinfo
提供此信息,但信息远不如VFP硬件寄存器提供的详细。
与ARM整数指令不同,没有VFP操作会直接影响APSR中的标志。这些标志存储在FPSCR中。在整数处理器可以使用浮点比较的结果之前,必须使用VMRS指令将浮点比较设置的标志转移到APSR中。这包括使用这些标志进行条件执行,甚至是其他VFP指令。下文示例展示了一段简单的代码以说明这一点。VCMP指令对VFP寄存器d0和d1中的值进行比较,并设置FPSCR标志。然后必须使用VMRS指令将这些标志转移到整数处理器APSR中,这样就可以根据这些标志有条件地执行指令。
标志的含义
整数比较标志支持浮点数不适用的比较。例如,浮点值总是有符号的,因此不需要无符号比较。另一方面,浮点比较可能会导致无序结果(意味着一个或两个操作数是NaN,即非数字)。IEEE-754定义了两个浮点值之间的四种可测试关系,它们对应于ARM条件码,如下所示:
与零比较
与整数指令不同,大多数VFP指令只能对寄存器进行操作,不能接受指令流中编码的立即数。VCMP指令是一个显著的例外,它有一个特殊的变体,允许快速、方便地与零进行比较。
解释标志
当标志在APSR中时,它们几乎可以像整数比较设置标志一样使用。然而,浮点比较支持不同的关系,因此整数条件码并不总是有意义的。表6-3描述了浮点比较而不是整数比较:
显然,条件码附加在读取标志的指令上,标志的来源对于被测试的标志没有影响。同样,很明显相反的条件依然成立。(例如,HS依然是LO的相反条件。)
当由CMP设置标志时,这些标志通常与由VCMP设置的标志具有类似的含义。例如,GT仍然表示大于。然而,无序条件以及有符号条件的去除可能会引起混淆。通常情况下,例如,使用LO(通常是无符号小于检查)代替LT是更可取的,因为LO在无序情况下不会匹配。
6.1.3 指令
VFP提供了执行算术和数据处理、内存加载和存储、以及寄存器传输(在VFP寄存器与ARM寄存器之间)的指令。这些指令使用ARM协处理器指令编码,但通常被视为主指令集的一部分,而不是协处理器操作。VFP提供了所有常见的算术操作、格式转换、一些复杂的算术操作(例如乘累加,VMLA,以及平方根,VSQRT),以及内存访问指令。
6.1.4 GCC中的VFP支持
GCC完全支持VFP的使用(尽管一些构建可以配置为默认假设没有VFP支持,在这种情况下,浮点计算将使用库代码)。
主要用于支持VFP的选项是:
-mfpu=vfp
指定目标具有VFP硬件。
其他选项可用于指定ARM Cortex-R系列处理器上特定VFP实现的支持:
-mfpu=vfpv3
-mfpu=vfpv3-d16
这些选项可用于仅在这些VFP实现上运行的代码,并且不要求与较旧的VFP实现向后兼容。
用于启用VFP的选项:
-mfloat-abi=softfp
-mfloat-abi=hard
softfp
使用与软件浮点兼容的过程调用标准,因此提供与遗留代码的二进制兼容性。这允许使用支持硬件浮点的新库运行较旧的软浮点代码,但仍然在函数调用之间使用硬件浮点寄存器。hard
则将浮点值传递到浮点寄存器中。这更高效,但与softfp
ABI变体不向后兼容。特别注意库的使用,包括C平台库。
C程序员应注意,如果传递了许多浮点值,使用-mfloat-abi=softfp
可能会导致显著的函数调用开销。
6.1.5 启用VFP
如果ARMv7处理器包含VFP硬件,必须在应用程序使用它之前显式启用它。
必须设置FPEXC寄存器中的EN位。
必须在协处理器访问控制寄存器(CP15.CACR)中启用对CP10和CP11的访问。这可以由操作系统按需完成。
6.1.6 Cortex-R处理器中的VFP
Cortex-R4F、Cortex-R5F和Cortex-R7处理器实现了VFPv3-D16浮点架构和通用VFP子架构v2。它们符合IEEE-754标准。在Cortex-R7处理器中,每个核心都有选择实现浮点单元(FPU)的选项。
Cortex-R4F处理器实现了一个浮点单元,支持单精度和双精度浮点数。
Cortex-R5F处理器可选择实现完整的FPU,支持单精度和双精度浮点数,或仅支持优化的单精度FPU。
Cortex-R7处理器可选择实现完整的FPU,支持单精度、半精度和双精度浮点数,或仅支持优化的单精度和半精度FPU。
6.2 ARM编译器中的VFP支持
ARM编译器完全支持VFP的使用(尽管一些构建可以配置为默认假设没有VFP支持,在这种情况下,浮点计算将使用库代码)。
在ARM编译器中用于支持VFP的主要选项是:
--fpu=name
允许你指定目标浮点硬件。
用于指定ARM Cortex-R系列处理器上特定VFP实现支持的选项有:
--fpu=vfpv3
--fpu=vfpv3_d16
这些选项可用于仅在这些VFP实现上运行的代码,并且不要求与较旧的VFP实现向后兼容。使用--fpu=list
可以查看支持的FPU的完整列表。
以下选项可用于链接支持:
--apcs=/hardfp
生成硬件浮点链接的代码。--apcs=/softfp
生成软件浮点链接的代码。
硬件浮点链接使用FPU寄存器传递参数和返回值。软件浮点链接意味着函数的参数和返回值是通过ARM整数寄存器R0到R3和堆栈传递的。--apcs=/hardfp
和--apcs=/softfp
与--fpu
的显式或隐式使用相互作用或覆盖。
要编译带或不带硬浮点的代码,ARM Compiler 5提供了以下编译开关:
--cpu=Cortex-R4
(无硬浮点)--cpu=Cortex-R4F
(硬浮点)--cpu=Cortex-R5
(无硬浮点)--cpu=Cortex-R5F
(硬浮点)--cpu=Cortex-R7
(硬浮点)--cpu=Cortex-R7.no_vfp
(无硬浮点)
6.3 浮点优化
本节包含了一些为开发者编写FP(浮点)汇编代码的建议。在应用这些建议时需要注意,因为这些建议可能特定于某一特定硬件。一段代码序列对某个处理器是最优的,但在不同硬件上可能并非如此。
避免在时间关键的循环中使用VFP系统控制寄存器的移动操作
与VFP系统控制寄存器(如FPSCR)的移动操作通常不出现在高性能代码中,可能不会被优化。因此,尽量不要在时间关键的循环中使用这些操作。例如,在Cortex-R7处理器上,对控制寄存器的访问是串行化的,如果在紧密循环或性能关键的代码中使用,将对性能产生显著影响。避免在时间关键的循环中进行整数寄存器和浮点寄存器之间的寄存器传输
在时间关键的循环中,同样应避免在整数处理器寄存器组和浮点寄存器组之间进行寄存器传输。优先使用多重加载/存储操作
相比于使用多个单独的浮点加载和存储操作,优先使用加载/存储多重操作,以便更高效地利用可用的传输带宽。