对于一个数,如果我们无需那么高的位宽(精度),那么就可以对其修约。修约代表着用一个精度更低(但是更易用)的数来替代它。也许世界上最著名的数值修约的例子就是 $\pi\approx3.14$ 了。

实数(连续值)

从小学开始,老师就会开始教“有效位”、“四舍五入”的规则。对于大部分的数,处理方式都不会有问题。例如 7.3 比起 8,还是更接近于 7。但是 7.5 呢?它在数轴上距离 7 和 8 一样近(0.5),为何我们一般都将其四舍五入到 8 呢?如果是一个负数,例如 -7.5 又应该如何呢?

实际上修约的主要艺术就在于如何花式修约 0.5,常见的方法包括:

  1. Round half up: 7.5 -> 8, -7.5 -> -7

  2. Round half down: 7.5 -> 8, -7.5 -> -8

  3. Round half away from 0: 7.5 -> 8, -7.5 -> -8

  4. Round half towards 0: 7.5 -> 7, -7.5 -> -7

  5. Round half to even: 7.5 -> 8, -7.5 -> -8, 6.5 -> 6, -6.5 -> -6

  6. Round half to odd: 7.5 -> 7, -7.5 -> -7, 6.5 -> 7, -6.5 -> -7

一般来说,最常用的规则是 3,即四舍五入。这也和 Matlab 中的 round 函数行为一致。另一个在计算机中常用的规则是 5,它是 IEEE 754 中规定的修约方法。

也许上面的修约方法还不够花式,有些地方还能见到更复杂的方式:

  • Stochastic (random) rounding: 随机修约(取整),7.5 有 50% 的概率修约到 7,有 50% 的概率修约到 8。

实际上,在实数(连续)的世界里,修约规则也许影响并不到大,但在一个定点的 DSP 系统里,或许影响就比较明显了。

定点数(离散值)

FPGA 中的定点数一般采用二进制补码来表示(Two's complement),我们记为 numerictype(s,w,f)。三个参数依次为:符号位宽(即有无符号数),总位宽、小数位宽。例如 numerictype(1,8,4)。整数部分的位宽可以用 w - s - f 算出来。举一些例子:

  • ${+000.0000}_2 = 0$

  • ${+000.0001}_2 = 0.0625$

  • ${+000.0111}_2 = 0.4375$

  • ${+000.1000}_2 = 0.5$

  • ${+111.1111}_2 = 7.9375$

  • ${-000.0000}_2 = -8$

  • ${-000.0001}_2 = -7.9375$

  • ${-000.0111}_2 = -7.5625$

  • ${-000.1000}_2 = -7.5$

  • ${-111.1111}_2 = -0.0625$

不是很在意运算结果的细节的时候,最简单的处理方式是直接截位(Truncation)而不进行修约。例如在上例中直接截掉小数部分。对于正数和负数,值都会变小,例如:0.5 -> 0-0.5 -> -1。这和 Matlab 中的 floor 函数的行为是一致的。

在 DSP 系统中,直接截位会为输出信号增加一个直流分量。在一些计算敏感的系统,例如通信系统中,这是会是一个致命错误。忽略“离散”的影响,floor 造成的误差的均值与标准差(假设待修约的数随机均布在每一个可能的值上)分别为:

$m_e = -\frac{1}{2}$

$\sigma_e^2 = \frac{1}{3}$

一种非常容易实现的修约方法是,为待修约的数加上 0.5( $+000.1000_2$ ​)之后进行直接截位。可以很容易知道,这种修约方法属于类型 _1. Round half up_。如果同样忽略“离散”的问题,Round half up 造成的误差的均值与标准差分别为:

$m_e = 0$

$\sigma_e^2 = \frac{1}{12}$

考虑到离散的问题,$m_e$ 是一个比较小的正数(这正是 up 的意义)。

$m_e = \frac{1}{2^{f+1}}$

可以看到即便是一种简单的修约,误差的均值(直流分量)和标准差就已经减小了很多。

类似的,我们可以简单的实现 _2. Round half down_,只需加上 $+000.0111_2$ 后截位即可。

甚至,7. Stochastic rounding 也比较容易实现,只需要随机(伪随机)的加上 $+000.1000_2$ 或 $+000.0111_2$ 即可。

对于 Xilinx 的 FPGA,如果是乘法之后的修约,可以使用 DSP48Ex 中乘法器后的 ALU 来完成,这样可以为 CLB 资源节约一个加法器:

dsp48e1.png

对于其它类型的修约(3、4、5、6),在 FPGA 中进行实现时就需要乘法结果的反馈。例如根据乘法结果的 MSB(符号位)来决定加数,即可实现 3 或 4 类型的修约。但这可能就无法设计成为流水线式结构,造成性能的损失。但好处在于,如果待修约的信号正负数出现的几率相同,那么进行 3 、4 类型的修约后信号是没有直流的。

此外需要注意的是,修约的过程中是有可能溢出的,例如:7.5 -> 8。Matlab 中的 round 函数会为 fi 类型的数补一个高位。但在 FPGA 中这么做可能就不值得了。

参考

《Rounding》on wikipedia

《Rounding Mathods》on mathsisfun

《PG104》of Xilinx Documentation

《PG108》of Xilinx Documentation

《Rounding Algorithms 101》on clivemaxfiel