另辟蹊径,next power of two新解法
June 5, 2019
0x0 什么是next power of 2?
给定正整数n, 求比n大的最小的2的幂次的数m
例如:
3 ->4
5->8
19->32
0x01 常规解法
unsigned int next_power_of_two_log(unsigned int v)
{
double next = pow(2, ceil(log2(v)));
return (int)next;
}
通俗
易懂
,但是慢
。
0x02 快速解法
unsigned long upper_power_of_two(unsigned long v){
v--;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
v++;
return v;
}
仅使用了位移和位操作, 巧妙 ,
快速
。
0x03 float解法(新解法)
思路:利用float的数据存储特性,
直接提取指数部分
。
IEEE 754单精度浮点数格式
浮点数数值的计算方法
因为我们的条件是正整数, 因此sign可以不看, 其次小数(fraction)部分,并不会影响我们的计算结果因为
$$ 2^{(e-127)}\times \left ( 1+\sum_{i=1}^{23}b_{23-i}2^{-i} \right ) < 2^{(e-127 +1)} $$
因此我们只需要重点关注exponent中的数据,这里记录了浮点数的指数部分,实际指数的值是偏移了127的,因此实际指数是e-127(PS:有一个例外, 下面会讲到)。
实际代码实现
unsigned int next_power_of_two(unsigned int v)
{
if(v == 1) return 2; //1 is an exception
v--;
float f = (float)v;
unsigned int* pf = reinterpret_cast<unsigned int *>(&f);
*pf = *pf << 1; //remove sign bit
*pf = *pf >> 24; //move to least significant byte
*pf = *pf - 127 + 1;
return 1 << *pf;
}
为什么1是例外?
算法中需要先减一,因此实际操作的float值为0, 而IEEE754的float值为0的时候其指数部分(exponent)也为0,而不是127, 这就无法使用后面算法统一计算了。
0x04 运行效率大PK float解法是否能威胁到快速解法的擂主地位?!!
运行效率比较
- 快速方法: val:1000, times:100000000, dt:86
- float方法: val:1000, times:100000000, dt:156
- log方法: val:1000, times:100000000, dt:1543
float解法比快速解法慢了80.4%!!! logf解法最慢,比快速解法慢了889.1%!!!
g++ -O2 汇编代码比较
00000690 <float方法>:
690: 83 ec 14 sub $0x14,%esp
693: 8b 44 24 18 mov 0x18(%esp),%eax
697: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
69e: 00
69f: 83 e8 01 sub $0x1,%eax
6a2: 89 04 24 mov %eax,(%esp)
6a5: df 2c 24 fildll (%esp)
6a8: d9 1c 24 fstps (%esp)
6ab: 8b 04 24 mov (%esp),%eax
6ae: 83 c4 14 add $0x14,%esp
6b1: 8d 0c 00 lea (%eax,%eax,1),%ecx
6b4: b8 01 00 00 00 mov $0x1,%eax
6b9: c1 e9 18 shr $0x18,%ecx
6bc: 83 e9 7e sub $0x7e,%ecx
6bf: d3 e0 shl %cl,%eax
6c1: c3 ret
000006d0 <快速方法>:
6d0: 8b 44 24 04 mov 0x4(%esp),%eax
6d4: 8d 50 ff lea -0x1(%eax),%edx
6d7: 89 d0 mov %edx,%eax
6d9: d1 e8 shr %eax
6db: 09 d0 or %edx,%eax
6dd: 89 c2 mov %eax,%edx
6df: c1 ea 02 shr $0x2,%edx
6e2: 09 d0 or %edx,%eax
6e4: 89 c2 mov %eax,%edx
6e6: c1 ea 04 shr $0x4,%edx
6e9: 09 c2 or %eax,%edx
6eb: 89 d0 mov %edx,%eax
6ed: c1 e8 08 shr $0x8,%eax
6f0: 09 c2 or %eax,%edx
6f2: 89 d0 mov %edx,%eax
6f4: c1 e8 10 shr $0x10,%eax
6f7: 09 d0 or %edx,%eax
6f9: 83 c0 01 add $0x1,%eax
6fc: c3 ret
相比快速方法更慢的原因
- 更多的内存访问操作
- 浮点操作指令相对耗时
0x05 后记
快速算法在加载了数据之后全部都在寄存器内操作完成, 确实非常厉害。
float算法是刚毕业时做合图工具时想到的解法,虽然性能上比不上快速算法, 但是折腾起来倒还是挺有意思的:P