理解定点算术

时间:2016-06-27 13:56:13

标签: c fixed-point

我正在努力解决如何对不同精度的定点数进行算术运算。我看过the paper by R. Yates,但我还是迷路了。在下文中,我使用Yates的表示法,其中A(n,m)指定带有n整数位,m小数位和n + m + 1的带符号定点格式比特整体。

简短问题 A(a,b)*A(c,d)A(a,b)+A(c,d)ac的具体情况如何?和b!= d

长问题 :在我的FFT算法中,我生成一个随机信号,其值介于-10V和10V之间,有符号输入(in),缩放为{{1旋转因子(tw)缩放为A(15,16)。两者都存储为A(2,29) s。像这样:

int

同样适用于旋转因素。

现在我需要执行

  1. float temp = (((float)rand() / (float)(RAND_MAX)) * (MAX_SIG - MIN_SIG)) + MIN_SIG; int in_seq[i][j] = (int)(roundf(temp *(1 << numFracBits)));
    问题
    a)我该如何实现呢? b) res = a*tw的大小应该是64位吗? c)我可以制作&#39; res&#39; A(17,14)因为我知道resa的范围?如果是,我应该将tw缩放2 ^ 14以在a*tw中存储正确的值吗?

  2. res
    问题
    a)如何添加这两种不同的Q格式?
    b)如果没有,我该如何进行此操作?

2 个答案:

答案 0 :(得分:1)

也许最容易做出一个例子。

假设您要添加两个数字,一个格式为A(3, 5),另一个格式为A(2, 10)

您可以通过将两个数字转换为“通用”格式来实现 - 也就是说,它们在小数部分中应具有相同的位数。

保守的做法是选择更多的位数。也就是说,将第一个数字转换为A(3, 10),将其向左移5位。然后,添加第二个数字。

添加的结果具有更大格式的范围,加上1位。在我的示例中,如果您添加A(3, 10)A(2, 10),则结果的格式为A(4, 10)

我称之为“保守”方式,因为你不能丢失信息 - 它保证结果可以用定点格式表示,而不会丢失精度。但是,实际上,您需要使用较小的格式来计算结果。要做到这一点,请考虑以下想法:

  1. 您可以使用不太准确的格式作为常用表示。在我的示例中,您可以将第二个数字转换为A(2, 5),方法是将整数移位5位。这将失去精确度,通常这种精确度损失不会有问题,因为无论如何你都要添加一个不太精确的数字。
  2. 您可以为结果的整数部分使用少1位。在应用程序中,经常发生的结果不会太大。在这种情况下,您可以分配少1位来表示它。您可能想要检查结果是否太大,并clamp到所需的范围。
  3. 现在,乘法。

    可以直接乘以两个定点数 - 它们可以是任何格式。结果的格式是“输入格式的总和” - 所有部分加在一起 ​​- 并将1加到整数部分。在我的示例中,将A(3, 5)A(2, 10)相乘会得到格式为A(7, 15)的数字。这是一个保守的规则 - 输出格式能够存储结果而不会损失精度,但在应用程序中,几乎总是你想要削减输出的精度,因为它只是太多位。

    在您的情况下,所有数字的位数都是32,您可能希望以所有中间结果都有32位的方式丢失精度。

    例如,将A(17, 14)A(2, 29)相乘会得到A(20, 43) - 需要64位。你可能应该从中削减32位,然后扔掉其余的。结果的范围是多少?如果您的旋转因子是一个最多4的数字,结果可能会受到2 ^ 19的限制(需要上面的保守数字20以适应-1 << 31乘以-1 << 31的边缘情况 - 它几乎总是如此值得拒绝这种边缘情况。)

    因此请使用A(19, 12)作为输出格式,即从输出的小数部分删除31位。

    所以,而不是

    res = a*tw;
    
    你可能想要

    int64_t res_tmp = (int64_t)a * tw;      // A(20, 43)
    if (res_tmp == ((int64_t)1 << 62)) // you might want to neglect this edge case
        --res_tmp;                          // A(19, 43)
    int32_t res = (int32_t)(res_tmp >> 31); // A(19, 12)
    

答案 1 :(得分:0)

您的问题似乎假设有一种正确的方法来执行您感兴趣的操作,但您明确询问了一些指导操作应该如何执行的细节。也许这是你困惑的核心。

  
      
  1. res = a*tw
  2.   

a表示为A(15,16),tw表示为A(2,29),因此它的产品A的自然表示(18,45)。您需要更多的值位(与两个因子组合在一起的位数)以保持完全精度。 A(18,45)是您应该如何解释将int扩展为64位有符号整数类型(例如int64_t)并计算其产品的结果。

如果你实际上不需要或想要45位分数,那么你确实可以将其舍入为A(18,13)(或者对于任何非负的A(18 + x,13-x) x)不改变结果的大小。这确实需要扩展。我可能会这样实现它:

/*
 * Computes a magnitude-preserving fixed-point product of any two signed
 * fixed-point numbers with a combined 31 (or fewer) value bits.  If x
 * is represented as A(s,t) and y is represented as A(u,v),
 * where s + t == u + v == 31, then the representation of the result is
 * A(s + u + 1, t + v - 32).
 */
int32_t fixed_product(int32_t x, int32_t y) {
    int64_t full_product = (int64_t) x * (int64_t) y;
    int32_t truncated = full_product / (1U << 31);
    int round_up = ((uint32_t) full_product) >> 31;

    return truncated + round_up;
}

这避免了有符号整数算法的几个潜在问题和实现定义的特征。它假定您希望结果采用一致的格式(即,仅取决于输入的格式,而不是它们的实际值),而不会溢出。

  
      
  1. a + res
  2.   

如果你不能依赖操作数来最初具有相同的比例,那么实际上有点困难。您需要重新缩放以使它们匹配,然后才能执行添加。在一般情况下,如果不稍微精确一些,你可能无法做到这一点。

在你的情况下,你从一个A(15,16)和一个A(18,13)开始。您可以计算A(19,16)或更宽的中间结果(实际上可能是A(47,16))保留幅度而不会丢失任何精度,但如果您想以32位表示,则可以做到最好没有改变幅度的风险是A(19,11)。就是这样:

int32_t a_plus_res(int32_t a, int32_t res) {
    int64_t res16 = ((int64_t) res) * (1 << 3);
    int64_t sum16 = a + res16;
    int round_up = (((uint32_t) sum16) >> 4) & 1;

    return (int32_t) ((sum16 / (1 << 5)) + round_up);
}

通用版本需要接受操作数的缩放。表示作为附加参数。这样的事情是可能的,但上面的内容足以咀嚼它。

所有上述假设每个操作数和结果的定点格式是不变的。这或多或少是定点的显着特征,一方面区别于浮点格式,另一方面区别于任意精度格式。但是,您可以选择允许格式变化,并使用每个值的单独变量跟踪它们。这基本上是定点和任意精度格式的混合,而且会更加混乱。

此外,前述假设必须不惜一切代价避免溢出。也可以将操作数和结果放在一致的范围内;这会使加法更简单,乘法更复杂,并且它将提供算术溢出的可能性。如果你有理由相信你的特定数据不太可能出现这种溢出,那么这可能是可以接受的。