定点数学中的溢出

时间:2011-06-27 16:15:39

标签: math signal-processing fixed-point

作为部分学习练习,部分爱好项目,我正在使用定点数学在AVR上实现我自己对Cooley-Tukey FFT算法的解释。我之前没有处理过定点数学,并且想知道如何最好地实现部分实现。我想这个问题的要点是要求确认我正在考虑正确的问题。

C-T算法的核心是以下列方式(在pseduocode中)对复值数据进行一系列乘法和加法:

temp1 = cosine(increment)*dataRealPart[increment1] 
-sine(increment)*dataImaginaryPart[increment1]

temp2 = cosine(increment)*dataImaginaryPart[increment1] 
+ sine(increment)*dataRealPart[increment1]

dataRealPart[increment1] = dataRealPart[increment2] - temp1

etc.

余弦和正弦数据将是形式为S.XXX'XXXX的8位有符号二进制分数,输入数据也将是SXXX.XXXX形式的8位有符号二进制分数,乘法将产生16位签名分数产品。在我看来,对于正弦和余弦的特别“坏”值以及数据的实部和虚部,temp1或temp2将非常接近16位有符号整数的极限。
如果数据的实部和虚部都是b0111.1111,那么Wolfram Alpha中的一点工作表明,对于正弦和余弦的“坏”值,输出可以比简单的1.4倍大将正弦的最大值乘以输入的最大值。

例如,如果sine参数为b0111.1111且输入值为b0111.111,则输出为b0111111.00000001,或十进制为16129。这将不会溢出有符号16位int的正范围,但在下一行中,这些乘积将从输入数据中加上和减去,并假设这里的输入数据转换为16位,很可能会发生溢出。

在我看来,重要的是:要么增加数据的内部处理分辨率,这会增加处理时间,要么确保输入数据保持低于导致溢出的幅度,降低信噪比比。这是关于事物的大小吗?

2 个答案:

答案 0 :(得分:1)

一种选择是将正弦和余弦值减少到Q6(十进制右边6位)这将使它们+/- 64。请注意,通过更精确一点,-1表示但+1不表示(即+128)。此外,在乘法后,结果中将有2个符号位,这意味着可以添加2个产品的总和,没有任何问题。使用降低分辨率的额外位,您应该能够避免溢出。另一点 - 如果你的复数值被限制为1(真实*真实+ img * img< = 1)那么结果总和不会超过1,而不是你找到的1.4数字 - 那是因为当正弦是1,余弦为零。你基本上是采用单位向量(cos,sin)和复数向量的点积。除此之外,您可以在添加它们之前将16位产品移位几位,可能需要进行舍入 - 无论如何您都没有16位精度,因为数据和触发函数都被舍入到7位。

最后一点。如果您正在使用多个数字的总和,并且您知道结果将始终在您的可表示范围内,则无需担心中间结果的溢出。它最终都会解决(无论如何,你扔掉的所有数据总和为零)。

答案 1 :(得分:0)

我知道这是很久以前的事,但这是我编写的库中有关定点数学的一些注释。

使用定点数学的概述 在实现量化或DSP类型数学区域时出现的关键思想包括如何表示小数单位,以及在数学运算导致整数寄存器溢出时该怎么做。

浮点数(C,C ++中的浮点数或双精度类型)最常用于表示小数单位,但并非始终可用(不支持硬件,或者在低端微控制器上不支持库软件)。如果没有硬件支持,则可以通过软件库提供浮点,但是这些库通常比硬件实现至少慢一个数量级。如果程序员很聪明,则可以使用整数数学代替软件浮点运算,从而使代码更快。在这些情况下,如果使用比例因子,则分数单位可以用整数寄存器(short,int,long)表示。

缩放和环绕 创建数字的比例整数表示时遇到的两个常见问题是

缩放-程序员必须手动跟踪分数比例因子,而当使用浮点数时,编译器和浮点库将为程序员执行此操作。

溢出和环绕-如果将两个大数相加,结果可能会大于整数表示所能容纳的范围,从而导致环绕或溢出错误。更糟糕的是,根据编译器警告设置或操作类型,可能不会注意到这些错误。例如,取两个8位数字(通常在C / C ++中为char)

char a = 34,b = 3,c;

//并计算

c = a * b;

//将它们相乘,结果为102,仍然适合8位结果。但 //如果b = 5会发生什么?

c = a * b; //实际答案是170,但结果将是-86

这种类型的错误称为溢出错误或环绕错误,可以通过多种方式处理。我们可以使用更大的整数类型,例如短裤或整数,也可以事先测试数字以查看结果是否会产生溢出。哪个更好?这要视情况而定。如果我们已经在使用最大的自然类型(例如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位宽寄存器中。另一种方法是测试并查看是否实际设置了两个操作数的高位-如果没有设置,尽管寄存器宽度精度受到限制,也可以安全地添加数字。另一种解决方案是使用饱和数学运算-如果结果大于寄存器中可以存储的值,则将结果设置为最大可能值。另外请务必当心标志。加两个负数可以使换行和两个正数一样容易。

设计固定基数管道时会出现一个有趣的问题,即跟踪实际使用了多少可用精度。当您可能正在使用时,对于像Fourier变换之类的操作尤其如此,它可能导致某些数组单元具有相对较大的值,而另一些数组单元具有接近零的数学能量。

一些规则:相加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和B进行了测试,我们可能会知道我们可以保留更多的小数位。

相乘... 乘法运算不需要在执行操作之前将小数点对齐。让我们假设我们有两个数字,就像我们在“添加”示例中那样。 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位精度。因此,即使我们可以将16位寄存器中的高15位适合,我们也将失去16位低位的精度,并且我们必须记住,结果将按5位缩放(或整数32)。有趣的是,如果事先测试A和B,我们可能会发现,尽管它们通过写有指定的输入精度,但它们实际上可能并不包含该有效位数(例如,按照程序员的约定,A是s11.4的数字,而是其实际值)是整数33或固定基数2and1 / 16,那么在C中我们可能没有足够的位数截断)。

(也在此处的图书馆:https://github.com/deftio/fr_math