编译 | 马超       责编 | 苏宓

出品 | CSDN(ID:CSDNnews)

近日,微软神级人物Raymond Chen在个人博客上,发布了一篇关于《如何计算平均值》的博文。这个话题虽然看似平淡无奇,却意外引爆技术圈,并带来无数讨论。

看完这篇博客之后,也让我感叹于国外技术讨论氛围的浓烈,虽然这一话题切入点非常简单,但是最终能够升华至编程之道层面的举轻若重的文章,接下来,我们不妨一起来看看。

有关求平均数算法的最初版本

有关如何求平均数这个问题,Raymond Chen并没有从一开始就炫技,而是循序渐进先放了一段最普通的实现,如下:

unsigned average(unsigned a, unsigned b)
{return (a + b) / 2;
}

相信绝大多数程序员都能一眼看出这种方法中可能隐藏的错误,那就是无法处理值溢出的问题,在Raymond的原文当中用“if unsigned integers are 32 bits wide, then it says that average(0x80000000U, 0x80000000U) is zero.”一句话来总结。

也就是说一旦(a+b)已经溢出,也就是大于unsign类型所能表示的最大整改,那么其计算结果将是average(0x80000000U, 0x80000000U)=0。

不过笔者在这里需要指出0x80000000U是x86平台特有的一个溢出表示方法,即indefinite integer value(不确定数值),不过同样是溢出ARM等RISC架构处理则非常清晰和简单,在上溢出或下溢出时,保留整型能表示的最大值或最小值,对照比较如下:

CPU

溢出值转为long

变量保留值说明

x86

范围0x8000000000000000

indefinite integer value

x86

范围0x8000000000000000

indefinite integer value

ARM

范围0x7FFFFFFFFFFFFFFF

变量赋值最大的正数

ARM

范围0x8000000000000000

变量赋值最大的正数

因此这段代码在ARM平台上运行时,如果出现溢出情况也并不会返回0,而会是该类型表示最大整数的一半,当然这个最大整数根据处理器的字长不同可能会有所变化。

return (a + b) / 2

低调的改进版本

接下来Raymond又给出了几种考虑溢出处理,同时又兼顾空间复杂度的方案:

1、变形法:

也就是将(a+b)/2变形,首先找到a和b当中较大的值,设为high,较小的值设为low,然后把(a+b)/2变成high-(high-low)/2或者low+(high-low)/2,如下:

unsigned average(unsigned low, unsigned high){return low + (high - low) / 2;
}

这种方法所需要的运算量是先进行一次比较以确定两个输入的大小,然后还需要再做两次加法(在计算机运算中加法和减法其实是基本等效的)和一次除法,最终得到答案。

2、除法前置方案:

也就是先对两个输入进行除2操作,即把(a+b)/2转换为a/2+b/2,当然这种方法需要考虑个位丢失的问题,比如说1/2在整形运算当中的结果会是0,因此1/2+1/2的结果是0而不是1,此时需要把两个输入的个位提取出来进行修正,具体如下:

unsigned average(unsigned a, unsigned b){return (a / 2) + (b / 2) + (a & b & 1);
}

这个算法当中的计算量是两次除法,两次加法和一次与运算操作。

3.SWAR法

SWAR法也非常的巧妙,它的本质思路就是把求平均值变成位运算,位操作其实就是二进制的操作,如果我们按位考虑输入值与输出结果的对应关系,那么会有以下的需求要点:

  1. 输入都是0,输出结果是0

  2. 输入都是1,输出是1

  3. 输入是一个0一个1,那么输出结果就是1/2

而满足以上条件的位运算,是与运算加上异常运算除2的结果,即(a and b) + (a xor b )/2,如下:

unsigned average(unsigned a, unsigned b){return (a & b) + (a ^ b) / 2;// 变体 (a ^ b) + (a & b) * 2
}

至于(a and b) + (a xor b )/2这个等式为什么能满足求平均值的要求,大家根据各种输入的情况都列一下就一目了然了。在这种方案下的计算量是两次位运算、一次加法运算以及一次除法运算来完成。

空间换时间的改进版本

在算法设计当中有一个最基本的常识,空间复杂度与时间复杂度是对跷跷板,上一节的储多算法当中,基本都是牺牲时间复杂度为代价来换取对于溢出的正确处理,那么反过来讲也完全可以用空间换时间,比如现在我们大多数的终端电脑都是64位机了,没必要为了32位长的整形溢出问题而烦恼,直接把类型转换为Long再计算结果就可以了。

unsigned average(unsigned a, unsigned b)
{// Suppose "unsigned" is a 32-bit type and// "unsigned long long" is a 64-bit type.return ((unsigned long long)a + b) / 2;
}

但是只要涉及的转换就又要针对不同架构的处理器进行特殊处理了,比如x86的64位处理器在进行32位整形转换为64位长整形时会自动将高32位的值填为0:

// x86-64: Assume ecx = a, edx = b, upper 32 bits unknownmov     eax, ecx        ; rax = ecx zero-extended to 64-bit valuemov     edx, edx        ; rdx = edx zero-extended to 64-bit valueadd     rax, rdx        ; 64-bit addition: rax = rax + rdxshr     rax, 1          ; 64-bit shift:    rax = rax >> 1;                  result is zero-extended; Answer in eax// AArch64 (ARM 64-bit): Assume w0 = a, w1 = b, upper 32 bits unknownuxtw    x0, w0          ; x0 = w0 zero-extended to 64-bit valueuxtw    x1, w1          ; x1 = w1 zero-extended to 64-bit valueadd     x0, x1          ; 64-bit addition: x0 = x0 + x1ubfx    x0, x0, 1, 32   ; Extract bits 1 through 32 from result; (shift + zero-extend in one instruction); Answer in x0

Mips64等架构则会将32位的整形转换为有符号扩展的类型。这时候就需要增加rldicl等删除符号的指令做特殊处理。

// Alpha AXP: Assume a0 = a, a1 = b, both in canonical forminsll   a0, #0, a0      ; a0 = a0 zero-extended to 64-bit valueinsll   a1, #0, a1      ; a1 = a1 zero-extended to 64-bit valueaddq    a0, a1, v0      ; 64-bit addition: v0 = a0 + a1srl     v0, #1, v0      ; 64-bit shift:    v0 = v0 >> 1addl    zero, v0, v0    ; Force canonical form; Answer in v0// MIPS64: Assume a0 = a, a1 = b, sign-extendeddext    a0, a0, 0, 32   ; Zero-extend a0 to 64-bit valuedext    a1, a1, 0, 32   ; Zero-extend a1 to 64-bit valuedaddu   v0, a0, a1      ; 64-bit addition: v0 = a0 + a1dsrl    v0, v0, #1      ; 64-bit shift:    v0 = v0 >> 1sll     v0, #0, v0      ; Sign-extend result; Answer in v0// Power64: Assume r3 = a, r4 = b, zero-extendedadd     r3, r3, r4      ; 64-bit addition: r3 = r3 + r4rldicl  r3, r3, 63, 32  ; Extract bits 63 through 32 from result; (shift + zero-extend in one instruction); result in r3

不过这种向更高位类型转换的方案也有一定问题,那就是空间的浪费,因为我原本只需要1位去处理溢出就好了,但是做了转换之后我却用了白白消费了31位的空间没有利用。

利用进位处理溢出的改进版本

在现代CPU当中大多都带有Carry bit(这里指进位位,不是C位的意思)功能。通过读取Carry bit的信息,就能达到在不浪费空间的情况下处理溢出的问题。比如在X86-32位处理器的代码如下:

// x86-32mov     eax, aadd     eax, b          ; Add, overflow goes into carry bitrcr     eax, 1          ; Rotate right one place through carry// x86-64mov     rax, aadd     rax, b          ; Add, overflow goes into carry bitrcr     rax, 1          ; Rotate right one place through carry// 32-bit ARM (A32)mov     r0, aadds    r0, b           ; Add, overflow goes into carry bitrrx     r0              ; Rotate right one place through carry// SH-3clrt                    ; Clear T flagmov     a, r0addc    b, r0           ; r0 = r0 + b + T, overflow goes into T bitrotcr   r0              ; Rotate right one place through carry

而对于那些没有Carry  bit功能的处理器来说,也可以通过自定义carry bit变量的方式来解决这个问题。如下:

unsigned average(unsigned a, unsigned b)
{
#if defined(_MSC_VER)unsigned sum;auto carry = _addcarry_u32(0, a, b, &sum);return _rotr1_carry(sum, carry); // missing intrinsic!
#elif defined(__clang__)unsigned carry;auto sum = _builtin_adc(a, b, 0, &carry);return _builtin_rotateright1throughcarry(sum, carry); // missing intrinsic!
#elif defined(__GNUC__)unsigned sum;auto carry = __builtin_add_overflow(a, b, &sum);return _builtin_rotateright1throughcarry(sum, carry); // missing intrinsic!
#else
#error Unsupported compiler.
#endif
}

对应arm-thumb2的clang 汇编代码如下:

// __clang__ with ARM-Thumb2movs    r2, #0          ; Prepare to receive carry    adds    r0, r0, r1      ; Calculate sum with flags    adcs    r2, r2          ; r2 holds carry    lsrs    r0, r0, #1      ; Shift sum right one position    lsls    r1, r2, #31     ; Move carry to bit 31    adds    r0, r1, r0      ; Combine

Quake3中“神”一样的代码

可以看到Raymond的博客先从一个简单问题入手,逐步提出问题并给出解决方案,是一篇阐述编程之道的上乘之作,接下来请允许笔者再推荐一下《Quake3》当中的神级代码。

《Quake3》这款3D游戏当年可以在几十兆内存的环境下跑得飞起,和目前动辄要求几十G显存的所谓3A大作形成鲜明对比,而《Quake3》取得这种性价比奇迹的关键在于把代码写得像神创造的一样。

《Quake3》最大的贡献莫过于提出使用平方根倒数速算法,并引入了0x5f3759df这样一个魔法数,目前这段代码的开源地址在:https://github.com/raspberrypi/quake3/blob/8d89a2a3c1707bf0f75b2ea26645b872e97c0b95/code/qcommon/q_math.c

如下:

float Q_rsqrt( float number )
{
floatint_t t;
float x2, y;
const float threehalfs = 1.5F;x2 = number * 0.5F;
t.f  = number;
t.i  = 0x5f3759df - ( t.i >> 1 );               // what the fuck?
y  = t.f;
y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removedreturn y;
}

这个算法的输入是一个float类型的浮点数,首先将输入右移一次(除以2),并用十六进制“魔术数字”0x5f3759df减去右移之后的数字,这样即可得对输入的浮点数的平方根倒数的首次近似值;而后重新将其作为原来的浮点数,以牛顿迭代法迭代,目前来看迭代一次即可满足要求,这个算法避免了大量的浮点计算,比直接使用浮点数除法要快四倍,大幅提升了平方根倒数运算的效率。

写在最后

写完本文之后笔者真是思绪万千,国外的很多技术讨论要不是由浅入深的编程之道,要不是直接碾压的神级代码,而这些方面都是我们所需要学习与提升的方面,希望本文也能让大家多一些思考。

《新程序员003》正式上市,50余位技术专家共同创作,云原生和数字化的开发者们的一本技术精选图书。内容既有发展趋势及方法论结构,华为、阿里、字节跳动、网易、快手、微软、亚马逊、英特尔、西门子、施耐德等30多家知名公司云原生和数字化一手实战经验!

☞技术负责人要停止写代码吗?

☞Unix操作系统背后的女程序员Lorinda Cherry去世,享年78岁

☞清华姚班陈丹琦等27位华人学者获奖,斯隆奖2022年获奖名单颁布!

微软大神“玩”出新花样,求平均值代码还能这样写?相关推荐

  1. 看完微软大神写的求平均值代码,我意识到自己还是too young了

    点击上方"AI遇见机器学习",选择"星标"公众号 重磅干货,第一时间送达 博雯 发自 凹非寺 量子位 | 公众号 QbitAI 取整求个无符号整数的平均值,居然 ...

  2. 看完微软大神写的求平均值代码,我意识到自己还是 too young 了

    博雯 发自 凹非寺 量子位 | 公众号 QbitAI取整求个无符号整数的平均值,居然也能整出花儿来? 这不,微软大神Raymond Chen最近的一篇长文直接引爆外网技术平台,引发无数讨论: 无数人点 ...

  3. Facebook 重金挖不到,ASP.NET 之父,微软大神“红衣教主”传奇

    作者 | 伍杏玲 出品 | CSDN (ID:CSDNnews) 2018 年底,微软"王者归来",时隔 16 年市值重返全球第一.人们纷纷用"力挽狂澜"&qu ...

  4. java大神请出来_求java大神,请分析以下代码,写出执行结果,并解释每行结果输出的原因。...

    求java大神,请分析以下代码,写出执行结果,并解释每行结果输出的原因.classPlate{publicPlate(){System.out.println("inPlateconstru ...

  5. 刺激战场大神玩绝地求生端游为何秒变菜鸟?网友:这就是差距

    刺激战场作为还原度最高的一款吃鸡手游,玩家非常多,菜鸟玩家和大神玩家也不少.但是很多刺激战场战神级别大神玩家再去玩绝地求生端游为何也会菜到不行?为什么手游比较简单端游比较难呢?今天就给大家分析一下刺激 ...

  6. 【深度学习】YOLOv7速度精度超越其他变体,大神AB发推,网友:还得是你!|开源...

    转载自 | 量子位 作者 | Pine 前脚美团刚发布YOLOv6, YOLO官方团队又放出新版本. 曾参与YOLO项目维护的大神Alexey Bochkovskiy在推特上声称: 官方版YOLOv7 ...

  7. 扯白 || 从“抄袭狗”到网文大神,那些年我们看过的文都是怎么写出来的【转】

    今年春天在写作圈发生了几件不大不小的抄袭洗稿事件.一件是言情大神匪我思存指责<甄嬛传>的作者流潋紫抄袭,另一件就是闹得沸沸扬扬的周冲洗稿六神磊磊. 这几位的作品,我都没有看过,因此不会评论 ...

  8. 计算机变成游戏,你玩游戏变成渣,复旦大神在5年前在游戏“我的世界”里写学术论文展示,从0开始打造计算机有多难!...

    一块小小的CPU里有多少个晶体管?几十亿个. 单枪匹马造出一个CPU乃至完整的电脑需要多长时间?有位大牛在<我的世界>游戏里用实际行动回答了这个问题:可能要花费一年多. 这篇造计算机的教程 ...

  9. 收藏 | 来自微软大神的机器学习秘籍!

    在这个人人都可能是学霸的全民学习时代,为什么人与人的差距依然很大?像优达学城这样的学习网站可以为每一个人想要学习的人带去技能和知识的补充,但要成为一个优秀的人才,你还需要一点点-嗯-特性!在这篇文章中 ...

最新文章

  1. 如果优美的将pytorch的卷积为自己所用
  2. java中hashMap的排序
  3. 汇编之浮点数处理(CrackMe003前置知识)
  4. Cordova创建你的第一个App
  5. SAP Spartacus RouteEvent,如何从localhost跳转到其他路由路径的
  6. 洛谷 P2384 最短路题解
  7. 通过安装和配置AD域解决Windows Server 2016的IIS无法加载SMB文件卷文件的问题
  8. Excel中 插入 对号等特殊字符
  9. SparkSql 数据类型转换
  10. 如何运行自动 Mac 清理
  11. Instagram新推两款AI过滤工具,没错!背后功臣就是Deep Text
  12. 07_封装丶静态和工具类
  13. 数据总线,地址总线,控制总线
  14. C++ Boost库分类总结
  15. 华为hicar 鸿蒙,华为智能座舱的野心:HiCar上车,为鸿蒙OS铺路
  16. asp实训报告摘要_asp实训报告总结.doc
  17. wsl2下安装lammps
  18. [codeforces 1333A] Little Artem 读懂题+找规律+多举例
  19. 几个非常好用的CMD命令
  20. 开机后黑屏看不到桌面_开机后黑屏看不到桌面怎么解决

热门文章

  1. 上海市高校大学生计算机一级,上海市高校计算机等级考试(一级)..doc
  2. java printwriter format_Java的格式化输出
  3. 当领导,核心是“抓住2点、做好5条”!做到了,员工根本不用管
  4. SAP MM供应商主数据表
  5. SAP MM 如何看一个Inbound Delivery单据相关的IDoc?
  6. 百度2019年财报喜忧参半,决胜AI时代仍不好说
  7. 热潮下的冷思考,人工智能即将改变的三大领域
  8. AI如何影响经济周期?诺奖得主表态:保持关注,我很乐观
  9. NLP中的词向量及其应用
  10. 北欧小国的宏大AI实验:让1%的人口接受人工智能培训