- Arm Helium技术指南:Cortex-M系列处理器的矢量运算扩展
- (英)乔恩·马什
- 4324字
- 2024-04-25 19:59:14
2.2 浮点数和定点数
所有计算机软件都必须处理数字,因此需要一些方法来表示数字。本节将简要介绍定点运算和浮点运算。具有DSP或浮点经验的程序员也许可以跳过此部分。
可以使用几种不同的格式来表示DSP系统中的信号数据。
1.整数
这是一种处理所有正数和负数的数字表示法。
2.定点数
这是一种可以处理实数或小数的数字表示法,但只能处理小数点(基10标记法中的十进制小数点,或计算中的二进制小数点)之后(通常也可以是之前)固定数量的数字。这避免了处理浮点数复杂度带来的硬件开销。Q格式通常用在定点表示的硬件实现中。在Q格式中,可以指定小数部分的位数,也可以选择指定整数部分的位数。例如,Q31表示有31位小数位;Q1.14表示有1个整数位和14个小数位。Helium提供对Q15和Q31的支持。Q格式中具有一个符号位,用它表示数值的正负,紧跟着一个二进制小数点,然后还有15位或者31位二进制数。16位Q15格式或者32位的Q31格式小数值表示的范围为-1到1。例如,十进制值0.75用带符号的Q15格式的定点数来表示,其对应的16位二进制数值为0x6000。定点算术允许只使用整数硬件来处理小数运算。
在Q15格式中,实际上是使用16位来表示[-1,+1)的范围,而不是表示[-32 768,+32 767]的常规整数。定点表示是非对称的,所以可以表示的最小值是-1.0,但可以表示的最大值比+1.0小一位(例如32 767/32 768)。
可以通过简单的乘法将值从十进制表示转换为Q15格式。例如,可以使用如下代码初始化Q15格式的变量,而不必计算0.123在二进制小数中的值。
3.浮点数
这是一种允许以类似于科学记数法的形式处理实数的数字表示法,以便用尾数和指数表示数字(例如,12345存储为1.2345×104),其中1.2345是尾数,4是指数。浮点数可以处理比定点数范围更宽的值,从而能够以不同的准确性处理非常小的数值和非常大的数值。IEEE-754标准通常用于指定浮点运算的表示和处理。
除了使用的格式之外,还必须注意用于表示一个数字的位数,8位、16位、32位和64位数值很常见,在科学和密码学应用中甚至可以使用更大的数值。
IEEE-754标准是计算机浮点数学实现的参考,包括Arm浮点系统。该标准精确定义了每个浮点运算在所有可能的输入值范围内将产生什么结果。
ANSI/IEEE-754标准定义了一组用于表示浮点数的格式。它在原始(1985)版本的规范中描述的主要格式是:
• 32位数——单精度。
• 64位数——双精度。
该规范的最新版本增加了其他几种格式,包括16位(半精度),本章后面将更详细地介绍这部分内容。
• 单精度。图2-1显示了如何使用单精度格式的32位。
图2-1 单精度浮点数格式
■ 位31表示符号位(0表示正数,1表示负数)。
■ 位[30:23]表示指数。
■ 位[22:0]表示尾数。
8位指数域用于使用二进制偏移来存储-127和128之间的值。换句话说,存储在8位域中的值要减去127。例如,指数值为0存储为0111 1111(127)。
尾数由23位二进制小数组成。浮点数被归一化,所以二进制小数点的左边只有一个非零数字。换句话说,总是有一个隐含的二进制“1.”在尾数值前面。
因此,由32位二进制数据表示的实际值是
(-1)符号位×2指数-127×1.尾数
IEEE规范使用某些特定的位模式来表示一些特殊情况:
■ 0定义为一个数值的尾数位和指数位均为0。
■ 一组非常小的“非规范化”数值是通过删除尾数中的前导数字是1的要求得到的。非规范化数值是一种特例。如果将指数位设置为0,就可以通过设置尾数位来表示非常小的非零数值。因为规范化数值有一个隐含“1.”作为前导数,所以最接近0的标准化数值可以表示为±2-126。为了获得更小的数值,1.m尾数值释义被替换为0.m的释义。真正的软件中很少使用这么小的数值,在许多应用程序中可以将其忽略或者清零。
■ 一组称为非数值(Not a Number,NaN)的位模式。
■ 一组表示负无穷和正无穷的位模式。
• 双精度。这类算术只是给尾数和指数增加了更多的位,因此位63是符号位,位[62:52]存储指数(这次偏移量为1023而不是127),位[51:0]存储尾数。Helium不支持对双精度浮点数做矢量运算。
IEEE-754标准现在也定义了一个半精度浮点运算,称为binary16,它有1个符号位、5个指数位和10个尾数位。指数编码的偏移量为15(即二进制值00001表示-14,二进制值11110表示15)。编码00000和11111具有特殊含义(分别为零/次规范化数值和无穷大或NaN)。图2-2显示了半精度浮点数的格式。
图2-2 半精度浮点数格式
• 半精度。它比单精度需要更少的内存存储(和带宽)。因为Helium矢量是固定的128位宽,所以它的运算量是单精度浮点每周期执行的运算量的两倍。这提供了显著的性能提升。这是以牺牲精度和范围为代价实现的,可能不适合某些算法。此外,C编译器通常不支持半精度算术类型的使用,因为标准C浮点类型(float、double)不可映射到半精度浮点类型的表示。
在许多算法中,损失一点精度是性能增益可接受的折中选择,这种性能增益来自将每条指令的浮点运算数量增加一倍。因此,半精度浮点最近在神经网络应用中受到青睐,也可以应用在许多其他信号处理算法中,例如,在频谱分析中运行峰值检测。
4.错误和舍入
当结果无法精确表示时,IEEE-754规范描述了合规实现应执行的四舍五入操作。用一个简单的例子说明它,即100.0/3.0。它需要用无穷多个数值(在十进制和二进制中都一样)才能准确地表示。该规范给出了不同的舍入选项来应对这个问题(向正无穷大舍入、向负无穷大舍入、向零舍入及就近舍入)。
IEEE-754还指定了当发生异常操作时的结果:
• 上溢出——结果太大而无法表示。
• 下溢出——结果太小以至于失去精度。
• 不精确——无法在不损失精度的情况下表示的结果。
• 无效——无法执行的计算,例如负数的平方根。
• 除零。
该规范还描述了当检测到这些异常操作时必须采取的措施。可能的结果包括生成NaN结果,或在下溢情况下生成非规范化数值。通常,DSP和机器学习算法不会使用这一类值,并且DSP硬件不支持使用它们。Helium矢量运算对此类异常情况不做检查。如果希望C编译器执行矢量化并生成Helium指令,则必须明确指定这些不需要考虑的情况,如第7章中介绍的。
存在一个可能隐藏的浮点表示方面的问题,那就是在32位整数和32位浮点数之间进行转换时的精度损失。32位浮点数具有23位尾数,这意味着存在着大量32位整数,如果将其转换为32位浮点数则无法准确表示。如果软件将这样的值转换为浮点型,然后又返回整型,结果将是一个不同但接近原来整型数值的值。
2.2.1 饱和运算
定点数的算术运算很简单。如果使用传统的整数运算将两个Q15值相乘,得到的结果将是一个30位的值,其中两位来自原始符号值。为了将其转换为Q31值,需要将结果加倍。事实上,有必要对结果进行加倍并饱和。
如果将Q15的值0x8000(-1)乘以0x8000(-1),则结果为0x40000000,即表示为Q31格式下的0.5。如果将其加倍,就会得到0x80000000,它表示Q31格式下的-1,而不是正确答案0x7FFFFFFF(最接近+1.0的值)。饱和可以防止这种从大的正值溢出到大的负值的情况,同样,也可以防止从小的负值溢出到小的正值。
有符号Q15计算的饱和意味着任何大于0x7FFF的结果都设置为0x7FFF,并且任何小于0x8000(-1)的结果都会饱和到0x8000。
为了将乘法的结果拟合到与其操作数相同的表示形式中,它必须被舍入或截断。尽管正如所见,丢失的小数位代表精度损失,但由于其结果不能超出-1到1的范围,因此不可能发生溢出。
但是,加法(或减法)有溢出的可能性。如果将两个定点数相加,每个都在-1和1之间,显然最大可能的结果是2,即不能以Q15或Q31格式表示。在某些情况下,算法可能需要将此标记为错误并触发某种异常。在另外一些情况下,可以简单地执行减半操作,或者对结果进行饱和操作,以使得任何正溢出都是最大的正数,负溢出都是最大的负数。减半操作允许在减半之前使用中间扩展累加器进行非溢出操作。
2.2.2 定点和浮点DSP
在编码DSP软件时,通常必须决定是使用定点数还是浮点数来表示信号。程序员使用浮点运算通常更简单。使用定点运算对程序进行重新编码可能要求很高并且需要时间。然而,浮点硬件通常比等效的定点硬件更昂贵、更慢且更耗电。定点值可能比浮点值占用更少的内存空间(尽管Helium上的半精度浮点数只需要16位值)。定点通常被老式的语音和音频编解码器强制要求使用,但正如将在本书后面看到的那样,它也应用于神经网络算法中。
浮点运算的使用提供了更大的动态范围,因为它允许处理小数和大数,这在处理非常大的数据集或范围不可预测的情况下非常有用。
但是,了解范围和精度之间的区别非常重要。使用定点表示法时,可以表示的相邻数字之间的间距始终相同。在浮点表示法中,相邻数字的间距不均匀,因为大数字之间的间距大于小数字之间的间距。在执行计算时,结果必须舍入到可以用其所用格式表示的最接近的值。
信号处理过程中数字的这种舍入或截断是量化误差或“噪声”的来源——模拟值和量化数字值之间的差异。
那些只需要低分辨率和低动态范围需求的应用可能会使用定点算术格式(例如,Q15算术)。
2.2.3 Helium浮点格式
Armv8.0-M架构支持标量单精度(32位)和双精度(64位)浮点,因此这也适用于Armv8.1-M。但是,Armv8.1-M还提供以下支持:
• 标量半精度(16位)浮点。
• 矢量半精度(16位)浮点。
• 矢量单精度(32位)浮点。
在CPU中对这些的支持是可选的。
前面已经介绍了利用矢量运算的优点,相比之下,添加半精度浮点支持的理由可能不太明显。它主要具备两大优势。其中一个是半精度与单精度相比,(使用Helium)处理器能够同时处理的数据量是单精度的两倍。在一条指令中可以执行8个半精度浮点计算或4个单精度计算。另一个是与单精度或双精度相比,半精度数据需要的内存空间更少。那些需要宽动态范围,而不是高分辨率的领域,可以使用半精度浮点运算。这方面的例子可能包括由麦克风输入,用于关键字识别或者语音指令场景的音频数据。
2.2.4 C数据类型和原语
Arm C语言扩展(Arm C Language Extension,ACLE)软件标准允许C/C++程序员以标准、可移植的方式利用Arm架构。它包括标准类型定义和原语函数。该规范可从以下位置下载:https://developer.arm.com/architectures/system-architectures/software-standards/acle。
许多使用Cortex-M CPU的程序员对Cortex微控制器软件接口标准(Cortex Microcontroller Software Interface Standard,CMSIS)都很熟悉(在第7章中介绍),它鼓励使用许多C语言的编码标准,特别是汽车工业软件可靠性协会指定的C语言编码标准(Motor Industry Software Reliability Association-C,MISRA-C)。这个C编码标准使用typedef来保证ANSI类型的表示一致性,例如,使用int8_t而不是带符号的char,使用uint16_t而不是无符号的short,以此类推。全书将遵循这一项惯例。
在用C语言编写代码时,可能希望使用原语函数来访问Helium指令。这些原语函数都是通过调用伪函数实现的,编译器将其原语替换为适当的指令或指令序列。这些内容将在本书后面更详细地介绍,但在这里介绍它们,有助于阅读下面的示例代码。
Arm编译器头文件arm_mve.h是ACLE标准的实现,它定义了一组不同大小的矢量类型,例如,float32x4_t是4个32位浮点数的矢量(可以保存在单个Q寄存器中)。
可以使用一个矢量寄存器中能允许的数据类型和大小来定义矢量。
相应地,对于一组需要2个寄存器的矢量:
或者需要4个寄存器的矢量:
编译器将矢量变量分配给Helium寄存器,并可以将矢量参数传递到这些寄存器中。泛型矢量类型的使用允许程序员以他们喜欢的方式解读这些矢量值。例如,寄存器中的一组整数可以被视为定点数值、复数、多项式等。