移位实现常量乘除的简单优化
来源:互联网 发布:淘宝客服话术下载 编辑:程序博客网 时间:2024/04/28 23:03
在一些没有硬件实现乘除法指令的CPU中,我们常用移位操作来取代常量的乘除操作,运算结果向下取整。因此,位移的次数和运算的精度成为我们关注的重点。这里来谈谈几个显而易见,但是你也许会忽略的问题。
整数乘法和小数乘法
用位移实现常量整数乘法、小数乘法,都不会遇到精度问题,这是根据二进制数本身的原理来的。运算次数则根据转换算法的不同而不同。
[算法1:二进制数展开,只用加法]
最简单的方法就是把乘数展开,逐位相加。
步骤1:乘数转换成二进制
步骤2:凡是为“1”的位置,作为展开后的一项,移位的方向和次数与“1”的位置相同
步骤3:相加
例:9 = 0000 1001. => (x << 3) + (x << 0)例:0.625 = 0.1010 => (x >> 1) + (x >> 3)
[算法2:加减法逐次逼近]步骤1:从最高位(MSB)开始逐次递减,考察该项与乘数的距离最高位(MSB)开始逐次递减,考察该项与乘数的距离
步骤2:选取最接近当前值得位(Bit)
步骤3:如果该位大于当前值,符号位选取“+”号,否则选取“-”号
步骤4:求和
例:7 = 8 - 1(按算法1则是1 + 2 + 4) => (x << 3) - (x << 0)例:0.21875 = 0.25 - 0.03125(按算法1则是0.125 + 0.0625 + 0.03125) => (x >> 2) - (x >> 5)
[算法1与算法2的评价]直观的看过去,算法2利用了四舍五入的原理,因此优于算法1。我们来验证一下:
用例1:0到65535,计算位宽uint16_t。
Better: 46736
Worse : 0
Equal : 18800
用例2:1/1到1/65535,计算位宽uint16_t。
Better: 4847
Worse : 0
Equal : 60688
整数除法和小数除法
通常认为除法是乘法的逆运算,通过取得除数的倒数与原数相乘,就相当于除法。但是这一规则并不完全适用于整数除法。单独利用算法2实现除法会遇到运算精度问题,这是由于求整数的倒数会丢失精度。
[精度问题]
例如我们要计算x / 800。利用算法2,在uin16_t下我们得到:
y = (x >> 10) + (x >> 12)
但这不是x / 800的精确解。原因在于1 / 800的完整展开式为:
0.000000000101000111101011100001010001111010111000010100011110101110000...
这个误差是多大呢?以0-65535的uint16_t来说:
Error 0: 6752
Error 1: 27328
Error 2: 25984
Error 3: 5472
Error>3: 0
当然对于精度要求不高的场合,这也足够了。
[算法3:预先左移]
好在完成除法之后,我们只需要整数部分,所以过高的运算精度毫无意义。那么多高的精度能够满足要求呢?事实上只需要预先左移适当的位数,就能大大提高运算精度。
步骤1:除数转换为二进制
步骤2:以第1次出现1的位置作为预处理阶段左移的数量
步骤3:除数左移
步骤4:使用算法2
步骤5:运算结果右移
以x / 800为例,首次出现1是在小数点后第10位,所以选择左移9位(如果左移10位就溢出了)。然后回到算法2即可。运算结果:
x = (x >> 1) + (x >> 3) + (x >> 6) - (x >> 11) - (x >> 13); x >>= 9;
此时在0-65535以及uint16_t下的误差有所下降:
Error 0: 65524
Error 1: 12
Error>1: 0
你也许会问,预先左移是否导致被除数溢出?这个问题太幼稚回家自己想,想不出来打屁股。
[算法4:循环节优化]
我们用膝盖想也知道有理数除法 + 二进制转换不可能产生无理数。细心的你也许会发现,我上面举的1 / 800的例子并非毫无规律:
1 / 800 = 0.000000000
10100011110101110000
10100011110101110000
10100011110101110000...
[作用1:提高运算精度]
我们来试着处理uint32_t的情况。使用算法3得到:
x = (x >> 1) + (x >> 3) + (x >> 6) - (x >> 11) - (x >> 13) - (x >> 16) + (x >> 21) + (x >> 23) + (x >> 26) - (x >> 31); x >>= 9;
在0-4294967295之中的误差数目是:Error 0: 4293047518
Error 1: 1919778(万分之4)
Error>1: 0
由于意识到循环节的存在,我们改为
x = (x >> 1) + (x >> 3) + (x >> 6) - (x >> 11) - (x >> 13) - (x >> 16); x = (x << 0) + (x >> 20); x >>= 9;
现在0-4294967295之中的误差数目是:Error 0: 4294567686
Error 1: 399610(十万分之9)
Error>1: 0
看来 精度提高了4.8倍。
[作用2:缩短移位次数]
循环节越短,缩短就越明显。以1 / 3为例,在uint_16的情况下:
1 / 3 = 0.010101010...
使用算法3得到:
x = (x >> 1) + (x >> 3) + (x >> 5) + (x >> 7) + (x >> 9) + (x >> 11) + (x >> 13) + (x >> 15); x >>= 1;
现在利用算法4可以二分展开: x = (x >> 1) + (x >> 3); x = x + (x >> 4); x = x + (x >> 8); x >>= 1;
在这个例子中,算法4的精度同样高于算法3,但我想这应该是运气因素。如果你问我为什么不直接(x >> 2) + (x >> 4),那么上一节你显然没看。[关于人肉优化的必要性]
由于人肉优化不具有一般性,不是这篇文章讨论的范畴。但是考虑到但凡需要使用移位来模拟乘除的场合,往往是性能关键的部分,所以还是很有必要的。
- 移位实现常量乘除的简单优化
- jdk没有对可以移位操作的乘除做优化
- 移位运算-2高效的乘除运算
- 移位与乘除关系
- 最简单的字符串加密C#实现-移位加密
- [原创]汇编实现大数乘除运算的雏形
- 循环移位的实现
- 循环移位的实现
- 从汇编代码的角度观察switch与if...else,以及乘除与移位的性能差别。
- 循环移位的宏实现
- 移位实现的乘除法
- 字符串移位的算法实现
- 移位实现的乘除法
- 算术/逻辑移位的实现。
- 大数乘除的汇编代码
- 二进制整数的乘除运算
- C语言实现只用加法和减法实现两个正整数的乘除运算
- 无聊,发个移位实现最简单加密的小程序
- Java--------注解
- poj 1655
- 运行地址和加载地址
- 浅析互联网场景的身份认证方法(全本)
- 基于C#的2D太阳、地球、月亮运动轨迹模拟实现
- 移位实现常量乘除的简单优化
- MFC绘制简单折线图
- Java----代理
- HDU 1425 ( sort )
- php定时自动执行需要触发一次(后台执行)
- semantic search examples
- 自己写strstr
- 对象思想,对象分析,对象设计,迭代,敏捷建模
- iOS: install App via OTA