在一些没有硬件实现乘除法指令的CPU中,我们常用移位操作来取代常量的乘除操作,运算结果向下取整。因此,位移的次数和运算的精度成为我们关注的重点。这里来谈谈几个显而易见,但是你也许会忽略的问题。 整数乘法和小数乘法用位移实现常量整数乘法、小数乘法,都不会遇到精度问题,这是根据二进制数本身的原理来的。运算次数则根据转换算法的不同而不同。 [算法1:二进制数展开,只用加法] 最简单的方法就是把乘数展开,逐位相加。 步骤1:乘数转换成二进制 步骤2:凡是为“1”的位置,作为展开后的一项,移位的方向和次数与“1”的位置相同 步骤3:相加
[算法2:加减法逐次逼近]
步骤1:从最高位(MSB)开始逐次递减,考察该项与乘数的距离最高位(MSB)开始逐次递减,考察该项与乘数的距离 步骤2:选取最接近当前值得位(Bit) 步骤3:如果该位大于当前值,符号位选取“+”号,否则选取“-”号 步骤4:求和
[算法1与算法2的评价]
直观的看过去,算法2利用了四舍五入的原理,因此优于算法1。我们来验证一下: 用例1:0到65535,计算位宽uint16_t。 Better: 46736 用例2:1/1到1/65535,计算位宽uint16_t。 Better: 4847 整数除法和小数除法通常认为除法是乘法的逆运算,通过取得除数的倒数与原数相乘,就相当于除法。但是这一规则并不完全适用于整数除法。单独利用算法2实现除法会遇到运算精度问题,这是由于求整数的倒数会丢失精度。 [精度问题] 例如我们要计算x / 800。利用算法2,在uin16_t下我们得到:
但这不是x / 800的精确解。原因在于1 / 800的完整展开式为: 0.000000000101000111101011100001010001111010111000010100011110101110000... 这个误差是多大呢?以0-65535的uint16_t来说: Error 0: 6752 当然对于精度要求不高的场合,这也足够了。 步骤1:除数转换为二进制 以x / 800为例,首次出现1是在小数点后第10位,所以选择左移9位(如果左移10位就溢出了)。然后回到算法2即可。运算结果:
此时在0-65535以及uint16_t下的误差有所下降: Error 0: 65524 你也许会问,预先左移是否导致被除数溢出?这个问题太幼稚回家自己想,想不出来打屁股。 1 / 800 = 0.000000000 [作用1:提高运算精度] 在0-4294967295之中的误差数目是:
Error 0: 4293047518 由于意识到循环节的存在,我们改为 现在0-4294967295之中的误差数目是:
Error 0: 4294567686 看来 精度提高了4.8倍。 1 / 3 = 0.010101010... 使用算法3得到: 现在利用算法4可以二分展开:
在这个例子中,算法4的精度同样高于算法3,但我想这应该是运气因素。如果你问我为什么不直接(x >> 2) + (x >> 4),那么上一节你显然没看。
[关于人肉优化的必要性]由于人肉优化不具有一般性,不是这篇文章讨论的范畴。但是考虑到但凡需要使用移位来模拟乘除的场合,往往是性能关键的部分,所以还是很有必要的。 |
移位实现常量乘除的简单优化
最新推荐文章于 2021-01-19 13:52:45 发布
移位实现常量乘除的简单优化