解决求平均值出现加和导致的溢出问题
试问求平均值能玩出什么样的花样来?
微软大神@Raymond发表了一篇长文,讲述的是关于求平均值的算法。
附上长文链接:https://devblogs.microsoft.com/oldnewthing/20220207-00/?p=106223#comments
求平均值这很简单,小学时候的知识,无非就是N个数的加和在除以N,再简单不过了。
对于无符号的两个数求平均,我们通常这么写:
(注:这里uint32_t以32位表示)
uint32_t average(uint32_t a, uint32_t b)
{
return (a + b) / 2;
}
但是这样就存在一个问题,当a和b两个值都比较大的时候,a加b的结果会超出uint32_t的最大表示范围,例如:
!!!!!!!!!average(0X80000000, 0X80000000) = 0
!!!!!!!!!!!!!
所以为了避免这种情况,进行优化成这样:
方法一
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
此方法需要知道大的那个值,当知道相加的两个无符号整数中的较大值时,减去较小值再除二,就可以以提前减少长度,从而避免了数值溢出。
方法二
还有另一种算法不依赖于知道哪个值更大,已经被申请为美国专利于2016年到期:
uint32_t average(uint32_t a, uint32_t b)
{
return (a / 2) + (b / 2) + (a & b & 1);
}
这个方法的诀窍在于将两个数先进行除法缩小,然后同时对最低位进行位与
,从而保证当两个数都为奇数时,(a & b & 1)的结果能得到前面除法的一个补正。
方法三
该方法被称为SWAR技术,它代表"寄存器中的SIMD"。
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + (a ^ b) / 2;
}
方法四
作者还提出了第二种思路,如果无符号32位整形的本身寄存器大小为64位的时候,或者编译器支持的多字节运算,就可以使用强制转换的方式:
uint32_t average(uint32_t a, uint32_t b)
{
// Suppose "unsigned" is a 32-bit type and
// "unsigned long long" is a 64-bit type.
return ((unsigned long long)a + b) / 2;
}
不过要注意一点,使用这种方式要保证64位寄存器的前32位为0才可以,要不然计算出来的还是不对的。
方法五
该方法是该长文评论区的一位网友提出的:
uint32_t avg(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) / 2);
}
这篇博客长文真就是微软及各路大神的讨论区,学无止境!
ends…