我知道这是很久以前的事了,但这里有一些我写的库中关于定点数学的笔记。
使用定点数学概述 在量化或 DSP 类型数学领域的实现中出现的关键思想包括如何表示小数单位以及当数学运算导致整数寄存器溢出时该怎么做。
浮点(C、C++ 中的浮点或双精度类型)最常用于表示小数单位,但它们并不总是可用(没有硬件支持或在低端微控制器上没有库软件支持)。如果硬件支持不可用,则可以通过软件库提供浮点,但是这些库通常至少比硬件实现慢一个数量级。如果程序员很聪明,可以使用整数数学而不是软件浮点数,从而获得更快的代码。在这些情况下,如果使用比例因子,小数单位可以用整数寄存器(short、int、long)表示。
缩放和环绕 在创建数字的缩放整数表示时出现的两个常见问题是
缩放 - 程序员必须手动跟踪小数缩放因子,而在使用浮点数时,编译器和浮点库会为程序员执行此操作。
溢出和环绕 - 如果添加两个大数,则结果可能大于整数表示形式,导致环绕或溢出错误。更糟糕的是,根据编译器警告设置或操作类型,这些错误可能会被忽视。例如取两个 8 位数字(通常是 C/C++ 中的字符)
字符 a = 34, b= 3, c;
//并计算
c=a*b;
//将它们相乘,结果为 102,仍然适合 8 位结果。但是 // 如果 b = 5 会发生什么?
c=a*b; // 真正的答案是 170,但结果是 -86
这种类型的错误称为溢出或环绕错误,可以通过多种方式处理。我们可以使用更大的整数类型,例如 short 或 int,或者我们可以预先测试这些数字以查看结果是否会产生溢出。哪个更好?这取决于具体情况。如果我们已经在使用最大的自然类型(例如 32 位 CPU 上的 32 位整数),那么我们可能必须在执行操作之前测试该值,即使这会导致一些运行时性能损失。
精度损失 真正的浮点表示可以在一系列操作中保持相对任意的精度,但是当使用定点(缩放)数学时,缩放过程意味着我们最终将一部分可用位专用于我们想要的精度. 这意味着小于比例因子的数学信息将丢失,从而导致量化误差。
一个简单的简单缩放示例让我们尝试用一个整数来表示数字 10.25,我们可以做一些简单的事情,例如将该值乘以 100 并将结果存储在一个整数变量中。所以我们有:
10.25 * 100 ==> 1025
如果我们想添加另一个数字,比如 0.55,我们将取 1.55 并将其放大 100。所以
0.55 * 100 ==> 155
现在要将它们加在一起,我们将整数化数字相加
1025+55 ==> 1070
但是让我们用一些代码来看看这个:
void main (void)
{
int myIntegerizedNumber = 1025;
int myIntegerizedNumber2 = 155;
int myIntegerizedNumber3;
myIntegerizedNumber3 = myIntegerizedNumber1 + myIntegerizedNumber2;
printf("%d + %d = %d\n",myIntegerizedNumber1,myIntegerizedNumber2,myIntegerizedNumber3);
}
但现在是几个挑战中的第一个。我们如何处理整数和小数部分?我们如何显示结果?没有编译器支持,我们作为程序员必须将整数和小数结果分开。上面的程序将结果打印为 1070 而不是 10.70,因为编译器只知道我们使用的整数变量,而不是我们预期的放大定义。
以 2 的幂(基数)思考 在前面的示例中,我们使用了 base10 数学,虽然它对人类有用,但并不是对位的最佳使用,因为机器中的所有数字都将使用二进制数学。如果我们使用 2 的幂,我们可以用位而不是 base10 来指定小数和整数部分的精度,我们还可以获得其他几个优点:
易于表示 - 例如,对于一个 16 位有符号整数(通常是 C/C++ 中的缩写),我们可以说该数字是“s11.4”,这意味着它是一个用 11 位整数和 4 位分数精度。事实上,一位不用于符号表示,但数字表示为 2 的补码格式。然而,有效地使用 1 位从表示的精度点进行符号表示。如果一个数字是无符号的,那么我们可以说它的 u12.4 - 是的,同一个数字现在有 12 个整数位精度和 4 个小数位表示。如果我们要使用 base10 数学,就不可能有这样简单的映射(我不会讨论所有会出现的 base10 问题)。更糟糕的是,需要执行许多除以 10 的操作,这些操作很慢并且可能导致精度损失。
易于更改基数精度。使用 base2 基数允许我们使用简单的移位(<< 和 >>)从整数更改为定点或从不同的定点表示进行更改。许多程序员认为基数应该是一个字节的倍数,比如 16 位或 8 位,但实际上只使用足够的精度而不是更多(如小型图形应用程序的 4 或 5 位)可以允许更大的空间,因为整数部分将获得剩余的位。假设我们需要一个 +/-800 的范围和 0.05 的精度(或十进制的 1/20)。我们可以将其放入 16 位整数,如下所示。第一个比特分配给符号。这留下了 15 位的分辨率。现在我们需要 800 个范围计数。Log2(800) = 9.64.. 所以整数动态范围需要 10 位。现在让我们看看小数精度,我们需要 log2(1/(0.05))= 4。四舍五入的 32 位是 5 位。因此,我们可以在此应用程序中使用 s10.5 的固定基数或带符号的 10 位整数和 5 位小数分辨率。更好的是它仍然适合 16 位整数。现在有一些问题:虽然小数精度是 5 位(或 1/32 或大约 0.03125),优于 0.05 的要求,但它并不相同,因此通过累积运算我们会得到量化误差。这可以通过移动到更大的整数和基数(例如,具有更多小数位的 32 位整数)来大大减少,但通常这不是必需的,并且对于较小的处理器来说,处理 16 位整数在计算和内存方面效率更高。应谨慎选择这些整数大小等。关于定点精度的一些注意事项当将定点数相加时,对齐它们的小数点很重要(例如,您必须添加 12.4 和 12.4 数字,甚至 18.4 + 12.4 + 24.4 将起作用,其中整数部分表示使用的位数而不是声明的整数寄存器的物理大小)。现在,这里开始一些棘手的问题,结果,纯粹来说是一个 13.4 的数字!但这变成了 17 位 - 所以你必须小心注意溢出。一种方法是将结果放入一个更大的 28.4 精度的 32 位宽寄存器中。另一种方法是测试并查看是否实际设置了任一操作数的高位 - 如果没有,则尽管存在寄存器宽度精度限制,但可以安全地添加数字。另一种解决方案是使用饱和数学 - 如果结果大于可以放入寄存器的结果,我们将结果设置为最大可能值。也一定要注意标志。添加两个负数可以像两个正数一样容易引起回绕。
设计固定基数管道时出现的一个有趣问题是跟踪实际使用了多少可用精度。虽然您可能正在使用这对于傅里叶变换等操作尤其如此,这可能会导致一些数组单元具有相对较大的值,而另一些则具有接近于零的数学能量。
一些规则: 添加 2 M 位数字会导致 M+1 位精度结果(无需测试溢出) 添加 NM 位数字会导致 M + log2(N) 位精度结果(无需测试溢出)
将 M 位数乘以 N 位数会得到 N+M 位精度结果(无需测试溢出)
饱和在某些情况下可能很有用,但可能会导致性能下降或数据丢失。
添加... 当添加或减去固定基数时,必须事先对齐小数点。例如:加一个A是s11.4号码,B是9.6号码。我们需要做出一些选择。我们可以先将它们移动到更大的寄存器,比如 32 位寄存器。导致 A2 是 s27.4 号码,B2 是 s25.6 号码。现在我们可以安全地将 A2 上移两位,使得 A2 = A2<<2。现在 A2 是 s25.6 数字,但我们没有丢失任何数据,因为前 A 的高位可以保存在更大的寄存器中而不会丢失精度。
现在我们可以将它们相加并得到结果 C=A2+B2。结果是一个 s25.6 数字,但精度实际上是 12.6(我们使用来自 A 的较大整数部分,即 11 位和较大的小数部分,来自 B 的 6 位,加上加法运算的 1 位)。所以这个 s12.6 数字有 18+1 位的精度,没有任何精度损失。但是,如果我们需要将其转换回 16 位精度数,我们将需要选择要保留多少小数精度。最简单的方法是保留所有整数位,因此我们将 C 向下移动足够多的位,使符号位和整数位适合 16 位寄存器。所以 C=C>>3 产生一个 s12.3 数字。只要我们跟踪小数点,我们就可以保持准确性。
乘法... 乘法不需要在执行操作之前对齐小数点。假设我们有两个数字,就像我们在加法示例中一样。A 是一个 s11.4 精度数,我们将其移至 A2 现在是一个 s27.4 大寄存器(但仍在使用 s11.4 位数)。B 是一个 s9.6 数字,我们现在将其移至 B2 一个 s25.6 大寄存器(但仍在使用 s9.6 位数)。C=A2*B2 导致 C 是 s20.10 数字。请注意,C 将整个 32 位寄存器用于结果。现在,如果我们想要将结果压缩回 16 位寄存器,那么我们必须做出一些艰难的选择。首先,如果整数精度,我们已经有 20 位 - 所以任何尝试(不查看实际使用的位数)将结果适合 16 位寄存器必须导致某种类型的截断。如果我们取结果的前 15 位(符号精度 +1 位),我们程序员必须记住,这按 20-5 = 5 位精度放大。因此,即使我们可以将前 15 位放入 16 位寄存器中,我们也会失去低 16 位的精度,并且我们必须记住结果是按 5 位(或整数 32)缩放的。有趣的是,如果事先测试 A 和 B,我们可能会发现,虽然他们通过 write 获得了规定的传入精度,但它们实际上可能不包含该数量的 live、set 位(例如,如果 A 是程序员约定的 s11.4 数字,但它的实际值是整数 33 或固定基数 2and1/16 那么我们可能没有那么多位要在 C 中截断)。因此,即使我们可以将前 15 位放入 16 位寄存器中,我们也会失去低 16 位的精度,并且我们必须记住结果是按 5 位(或整数 32)缩放的。有趣的是,如果事先测试 A 和 B,我们可能会发现,虽然他们通过 write 获得了规定的传入精度,但它们实际上可能不包含该数量的 live、set 位(例如,如果 A 是程序员约定的 s11.4 数字,但它的实际值是整数 33 或固定基数 2and1/16 那么我们可能没有那么多位要在 C 中截断)。因此,即使我们可以将前 15 位放入 16 位寄存器中,我们也会失去低 16 位的精度,并且我们必须记住结果是按 5 位(或整数 32)缩放的。有趣的是,如果事先测试 A 和 B,我们可能会发现,虽然他们通过 write 获得了规定的传入精度,但它们实际上可能不包含该数量的 live、set 位(例如,如果 A 是程序员约定的 s11.4 数字,但它的实际值是整数 33 或固定基数 2and1/16 那么我们可能没有那么多位要在 C 中截断)。
(这里还有图书馆:https ://github.com/deftio/fr_math )