6.Floating-Point
所有计算机程序都涉及数字。然而,对于不熟悉其详细实现的程序员来说,浮点数有时会显得直觉不清。在讨论ARM处理器上的浮点实现之前,首先提供一个关于浮点基础知识的简短概述。具有浮点经验的程序员可能想跳过
6.1 Floating-point basics and the IEEE-754 standard
IEEE-754标准是几乎所有现代计算机浮点数学实现的参考,包括ARM浮点系统。最初的IEEE-754-1985标准已随着IEEE-754-2008的发布进行了更新。该标准精确定义了在所有可能的输入值下,每个基本浮点操作将产生的结果。它还描述了符合标准的实现对于无法精确表示的舍入结果应该如何处理。一个简单的例子是1.0 ÷ 3.0,它将需要无限的数字才能在十进制或二进制表示中精确表达。
IEEE-754提供了许多不同的舍入选项来应对这种情况(舍入到正无穷大,舍入到负无穷大,舍入到零,和两种最接近的舍入方式,详见6-4页的舍入算法)。IEEE-754还规定了当发生异常操作时的结果。这意味着一个计算可能代表了一个问题。这些条件可以通过查询FPSCR(在ARM处理器上)或在某些系统上设置陷阱处理程序来检测。以下是一些异常操作的例子:
溢出:结果太大而无法表示。
下溢:结果太小以至于丢失了精度。
不精确:结果无法在没有精度损失的情况下表示。显然,许多浮点计算将属于此类别。
无效:例如,尝试计算负数的平方根。
除以零
该规范还描述了在检测到这些异常操作之一时必须采取的措施。可能的结果包括生成非数字(NaN)结果以应对无效操作、正或负无穷大、溢出或除以零,或在下溢的情况下生成非标准化数字。该标准还定义了如果后续浮点计算在NaN或无穷大上运行时应该产生的结果。
IEEE-754定义的其中一项内容是浮点数在硬件中的表示方式。浮点数通常以单精度(32位)或双精度(64位)表示。VFP(参见2-6页的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)^(符号位) × 尾数 × 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 Rounding algorithms
IEEE 754-1985 标准定义了四种不同的舍入方式,如下所示:• 四舍五入(偶数舍入)。这种模式会舍入到最近的值。如果一个数字恰好位于两个可能值的中间,它会舍入到最接近的具有零最低有效位的值。• 向0舍入。这会使数字始终向零舍入(这也可以视为截断)。• 向 +∞ 舍入。这选择向正无穷大舍入。• 向 -∞ 舍入。这选择向负无穷大舍入。
IEEE 754-2008 标准增加了一种额外的舍入模式。在四舍五入的情况下,现在也可以将恰好位于两个值之间的数字向远离零的方向舍入(换句话说,对于正数向上舍入,对于负数向下舍入)。这增加了将数字舍入到最近的具有零最低有效位的值的选项。目前,VFP 不支持这种舍入模式。
6.1.2 ARM VFP
VFP 是 ARMv7-A 架构中可选但很少省略的指令集扩展。它可以实现为三十二个或十六个双字寄存器。使用 VFPv3-D32 和 VFPv3-D16 来区分这两种选项。如果同时实现了高级 SIMD(NEON)扩展,则 VFPv3-D32 始终存在。VFPv3 还可以通过半精度扩展进行可选扩展,这些扩展提供了在半精度浮点数(16位)和单精度浮点数(32位)之间的双向转换功能。这些操作仅允许半精度浮点数与其他格式之间进行转换。
VFPv4 在 VFPv3 的基础上增加了半精度扩展和融合乘加指令。在融合乘加操作中,仅在最后进行一次舍入。这是 IEEE 754-2008 规范中的新特性之一。融合操作可以提高重复累积乘积的计算准确性,例如矩阵乘法或点积计算。
除了上述寄存器外,还有一些其他 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(和 NEON)指令只能对寄存器操作,不能接受指令流中编码的立即数。VCMP 指令是一个显著的例外,它有一个特殊变体,可以快速方便地与零进行比较。
解释标志
当标志位位于 APSR 中时,它们几乎可以像整数比较设置了标志一样使用。然而,浮点比较支持不同的关系,因此整数条件码并不总是适用。下表描述的是浮点比较,而不是整数比较。
很明显,条件码是附加在读取标志的指令上的,而标志的来源并不会影响被测试的标志。区别在于,当执行的是 vcmp 而不是 cmp 时,标志的含义有所不同。同样,反向条件仍然成立。例如,HS 仍然是 LO 的反向条件。
当由 CMP 设置标志时,这些标志通常与由 VCMP 设置的标志具有类似的含义。例如,GT 仍然表示大于。然而,无序条件和有符号条件的去除可能会使情况变得混乱。例如,通常在某些情况下,使用 LO(通常是无符号小于的检查)代替 LT 是更可取的,因为它在无序情况下不匹配。
6.1.3 Enabling VFP
如果 ARMv7-A 内核包含 VFP 硬件,则必须显式启用该硬件,应用程序才能使用它。启用它需要以下几个步骤:
必须设置 FPEXC 寄存器中的 EN 位。
如果在普通世界中需要访问 VFP,必须在非安全访问控制寄存器(CP15.NSACR)中启用对 CP10 和 CP11 的访问。这通常在安全引导加载程序中完成。
必须在协处理器访问控制寄存器(CP15.CACR)中启用对 CP10 和 CP11 的访问。这可以由操作系统按需完成。
6.2 VFP support in GCC
GCC 完全支持 VFP 的使用,尽管某些构建可以配置为默认不支持 VFP,在这种情况下,浮点计算将使用库代码。用于启用 VFP 支持的主要选项是:
-mfpu=vfp
指定目标设备具有 VFP 硬件,-mfpu=neon
选项也有同样的作用。
其他选项可用于在 ARM Cortex-A 系列处理器上指定特定的 VFP 实现支持:
-mfpu=vfpv3
或-mfpu=vfpv3-d16
(用于 Cortex-A8 和 Cortex-A9 处理器)。-mfpu=vfpv4
或-mfpu=vfpv4-d16
(用于 Cortex-A5 和 Cortex-A15 处理器)。
这些选项可用于仅在这些 VFP 实现上运行的代码,并不要求与较早的 VFP 实现向后兼容。
-mfloat-abi=softfp
(或hard
)指定使用哪种 ABI 来启用 VFP 的使用。softfp
使用与软件浮点数兼容的过程调用标准,因此提供与旧代码的二进制兼容性。这允许使用支持硬件浮点的新库运行旧的软浮点代码,但在函数调用之间仍使用硬件浮点寄存器。hard
则将浮点值传递到浮点寄存器中,这更高效,但与softfp
ABI 变体不向后兼容。
特别需要注意的是,包括 C 平台库在内的库,需要特别小心。有关高效参数传递的更多信息,请参阅相关内容
C 语言程序员应注意,当使用 -mfloat-abi=softfp
时,如果传递了大量浮点值,函数调用的开销可能会很大。
6.3 VFP support in the ARM Compiler
ARM 编译器完全支持 VFP 的使用(尽管某些构建默认可以配置为不支持 VFP,在这种情况下,浮点计算将使用库代码)。与 ARM 编译器一起使用 VFP 支持的主要选项是:
--fpu=name
,该选项允许您指定目标浮点硬件。
用于指定 ARM Cortex-A 系列处理器上特定 VFP 实现支持的选项是:
--fpu=vfpv3
或--fpu=vfpv3_d16
(适用于 Cortex-A8 和 Cortex-A9 处理器)。--fpu=vfpv4
或--fpu=vfpv4_d16
(适用于其他所有 Cortex-A 系列处理器)。
这些选项可用于仅在这些 VFP 实现上运行的代码,并不要求与较旧的 VFP 实现向后兼容。使用 --fpu=list
可以查看支持的 FPU 全部列表。
以下选项可用于链接支持:
--apcs=/hardfp
生成用于硬件浮点链接的代码。--apcs=/softfp
生成用于软件浮点链接的代码。
硬件浮点链接使用 FPU 寄存器传递参数和返回值。软件浮点链接意味着函数的参数和返回值使用 ARM 整数寄存器 R0 到 R3 和栈进行传递。--apcs=/hardfp
和 --apcs=/softfp
会与 --fpu
的显式或隐式使用进行交互或覆盖。
6.4 VFP support in Linux
使用 VFP 的应用程序(或调用使用 VFP 的库)对 Linux 内核提出了一些额外要求。为了使应用程序正常运行,内核必须在上下文切换期间保存和恢复 VFP 寄存器。如果没有 VFP 硬件,内核可能还需要解码并模拟 VFP 指令。
6.4.1 Context switching
除了保存和恢复整数寄存器外,内核在上下文切换时还可能需要保存和恢复 VFP 寄存器。为了避免浪费周期,只有当应用程序实际使用 VFP 时才会执行这一操作。由于 VFP 初始化代码使 VFP 处于禁用状态,当线程首次尝试访问浮点硬件时,会发生未定义异常。处理此异常的内核函数会检测到 VFP 被禁用并且新线程想要使用 VFP,此时内核会保存当前的 VFP 状态并恢复新线程的 VFP 状态。
在线程可以迁移到不同内核的集群中,这种简单的系统将无法正常工作。因此,如果前一个线程使用了 VFP,内核会保存其状态。
6.5 Floating-point optimization
本节为编写浮点汇编代码的开发人员提供了一些建议。在应用这些建议时需要注意,因为推荐内容可能针对特定硬件。对一个核心最优的代码序列在不同的硬件上可能并非最佳。
在 Cortex-A9 处理器上,避免混合使用 VFP 和 NEON 指令,因为在数据引擎之间切换会产生显著的开销。
对 VFP 系统控制寄存器(如 FPSCR)的移动操作通常不会出现在高性能代码中,也可能未经过优化。如果可能,这些操作不应放在时间关键的循环中。例如,在 Cortex-A9 处理器上,访问控制寄存器会导致序列化操作,若在紧密循环或性能关键代码中使用,会对性能产生重大影响。
同样,应避免在时间关键的循环中在整数处理器寄存器组和浮点寄存器组之间进行寄存器传输。对于 Cortex-A8 处理器来说,尤其应避免从 VFP 寄存器向整数寄存器的传输。
相比使用多个单独的浮点加载和存储操作,优先使用加载/存储多个操作,以便更高效地利用可用的传输带宽。
相关链接cortex-A:
【ARM中文手册】第二章 ARM Architecture and Processors
【ARM中文手册】第三章 ARM Processor Modes and Registers
【ARM中文手册】第四章 Introduction to Assembly Language
......
相关链接cortex-R:
《ARM Cortex-R 学习指南》-【第二章】-ARM 架构与处理器
《ARM Cortex-R 学习指南》-【第三章 】-ARM 处理器模式与寄存器
《ARM Cortex-R 学习指南》-【第四章】-汇编语言简介
《ARM Cortex-R 学习指南》-【第五章】-统一汇编语言指令
......
经典课程: