看完微软大神写的求平均值代码,我意识到自己还是 too young 了
這不,微軟大神Raymond Chen最近的一篇長文直接引爆外網技術平臺,引發無數討論:
無數人點進去時無比自信:不就是一個簡單的相加后除二的小學生編程題嗎?
unsigned?average(unsigned?a,?unsigned?b) {return?(a?+?b)?/?2; }但跟著大神的一路深挖,卻逐漸目瞪狗呆……
沒那么簡單的求平均值
先從開頭提到的小學生都會的方法看起,這個簡單的方法有個致命的缺陷:
如果無符號整數的長度為32位,那么如果兩個相加的值都為最大長度的一半,那么僅在第一步相加時,就會發生內存溢出。
也就是average(0x80000000U, 0x80000000U)=0。
不過解決方法也不少,大多數有經驗的開發者首先能想到的,就是預先限制相加的數字長度,避免溢出。
具體有兩種方法:
1、當知道相加的兩個無符號整數中的較大值時,減去較小值再除二,以提前減少長度:
unsigned?average(unsigned?low,?unsigned?high) {return?low?+?(high?-?low)?/?2; }2、對兩個無符號整數預先進行除法,同時通過按位與修正低位數字,保證在兩個整數都為奇數時,結果仍然正確。
(順帶一提,這是一個被申請了專利的方法,2016年過期)
unsigned?average(unsigned?a,?unsigned?b) {return?(a?/?2)?+?(b?/?2)?+?(a?&?b?&?1); }這兩個都是較為常見的思路,不少網友也表示,自己最快想到的就是2016年專利方法。
同樣能被廣大網友快速想到的方法還有SWAR(SIMD within a register):
unsigned?average(unsigned?a,?unsigned?b) {return?(a?&?b)?+?(a?^?b)?/?2;//?變體?(a?^?b)?+?(a?&?b)?*?2以及C++ 20版本中的std: : midpoint函數。
接下來,作者提出了第二種思路:
如果無符號整數是32位而本機寄存器大小是64位,或者編譯器支持多字運算,就可以將相加值強制轉化為長整型數據。
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; }不過,這里有一個需要特別注意的點:
必須要保證64位寄存器的前32位都為0,才不會影響剩余的32位值。
像是x86-64和aarch64這些架構會自動將32位值零擴展為64位值:
//?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而Alpha AXP、mips64等架構則會將32位值符號擴展為64位值。
這種時候,就需要額外增加歸零的指令,比如通過向左進位兩字的刪除指令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或者直接訪問比本機寄存器更大的SIMD寄存器,當然,從通用寄存器跨越到SIMD寄存器肯定也會增加內存消耗。
如果電腦的處理器支持進位加法,那么還可以采用第三種思路。
這時,如果寄存器大小為n位,那么兩個n位的無符號整數的和就可以理解為n+1位,通過RCR(帶進位循環右移)指令,就可以得到正確的平均值,且不損失溢出的位。
△帶進位循環右移
//?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那如果處理器不支持帶進位循環右移操作呢?
也可以使用內循環(rotation intrinsic):
unsigned?average(unsigned?a,?unsigned?b) { #if?defined(_MSC_VER)unsigned?sum;auto?carry?=?_addcarry_u32(0,?a,?b,?&sum);sum?=?(sum?&?~1)?|?carry;return?_rotr(sum,?1); #elif?defined(__clang__)unsigned?carry;sum?=?(sum?&?~1)?|?carry;auto?sum?=?__builtin_addc(a,?b,?0,?&carry);return?__builtin_rotateright32(sum,?1); #else #error?Unsupported?compiler. #endif }結果是,x86架構下的代碼生成沒有發生什么變化,MSCver架構下的代碼生成變得更糟,而arm-thumb2的clang 的代碼生成更好了。
//?_MSC_VERmov?????ecx,?aadd?????ecx,?b??????????;?Add,?overflow?goes?into?carry?bitsetc????al??????????????;?al?=?1?if?carry?setand?????ecx,?-2?????????;?Clear?bottom?bitmovzx???ecx,?al?????????;?Zero-extend?byte?to?32-bit?valueor??????eax,?ecx????????;?Combineror?????ear,?1??????????;?Rotate?right?one?position;?Result?in?eax//?__clang__mov?????ecx,?aadd?????ecx,?b??????????;?Add,?overflow?goes?into?carry?bitsetc????al??????????????;?al?=?1?if?carry?setshld????eax,?ecx,?31????;?Shift?left?64-bit?value//?__clang__?with?ARM-Thumb2movs????r2,?#0??????????;?Prepare?to?receive?carryadds????r0,?r0,?r1??????;?Calculate?sum?with?flagsadcs????r2,?r2??????????;?r2?holds?carrylsrs????r0,?r0,?#1??????;?Shift?sum?right?one?positionlsls????r1,?r2,?#31?????;?Move?carry?to?bit?31adds????r0,?r1,?r0??????;?Combine微軟大神的思考們
Raymond Chen1992年加入微軟,迄今為止已任職25年,做UEX-Shell,也參與Windows開發,Windows系統的很多最初UI架構就是他搞起來的。
他在MSDN 上建立的blogThe Old New Thing也是業內非常出名的純技術向產出網站。
這篇博客的評論區們也是微軟的各路大神出沒,繼續深入探討。
有人提出了新方法,在MIPS ASM共有36個循環:
unsigned?avg(unsigned?a,?unsigned?b) {return?(a?&?b)?+?(a?^?b)?/?2; }//?lw??????$3,8($fp)??#?5 //?lw??????$2,12($fp)?#?5 //?and?????$3,$3,$2???#?4 //?lw??????$4,8($fp)??#?5 //?lw??????$2,12($fp)?#?5 //?xor?????$2,$4,$2???#?4 //?srl?????$2,$2,1????#?4 //?addu????$2,$3,$2???#?4有人針對2016年專利法表示,與其用(a / 2) + (b / 2) + (a & b & 1)的方法,為啥不直接把 (a & 1) & ( b & 1 ) ) 作為進位放入加法器中計算呢?
還有人在評論區推薦了TopSpeed編譯器,能夠通過指定合適的代碼字節和調用約定來定義一個內聯函數,以解決“乘除結果是16位,中間計算值卻不是”的情況。
只能說,學無止境啊。
最后
在我們的代碼中,很容易看出每個人基本功和編程水平,這也是大廠面試,尤其是鵝廠,面試題都是非常基礎的,簡單不代表沒有深度,我們需要不斷修煉自己的基本功,提高看待問題的全面性,這樣才能盡最大可能寫出bugfree的代碼。
? ? 計算機基礎扎實,到底是說什么?
? ?昨晚來了一場直播活動:
主題是“有問必答”,大家心里都有一些疑問,各個方面的,任何問題都可以詢問,一起暢聊3個多小時,包括硬核技術問題(驅動開發轉型,網絡高級開發,區塊鏈,云計算,虛擬化,Linux,后臺開發,開發語言的選擇),校招,社招,騰訊HC,實習,項目,技術轉型,跳槽互聯網等等各種問題,都非常認真且有深度給人解答,希望可以幫助到大家。
后續會每個月定期舉行一次有問必答直播,對星球感興趣的:極客星球,公眾號回復“優惠卷”,或者掃描下面二維碼,可以加入。
原文:
https://devblogs.microsoft.com/oldnewthing/20220207-00/?p=106223
參考鏈接:
https://news.ycombinator.com/item?id=30252263
- END -
看完一鍵三連在看,轉發,點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
定個目標|建立自己的技術知識體系
大廠后臺開發基本功修煉路線和經典資料
計算機基礎扎實,到底是說什么?
你好,這里是極客重生,我是阿榮,大家都叫我榮哥,從華為->外企->到互聯網大廠,目前是大廠資深工程師,多次獲得五星員工,多年職場經驗,技術扎實,專業后端開發和后臺架構設計,熱愛底層技術,豐富的實戰經驗,分享技術的本質原理,希望幫助更多人蛻變重生,拿BAT大廠offer,培養高級工程師能力,成為技術專家,實現高薪夢想,期待你的關注!點擊藍字查看我的成長之路。
校招/社招/簡歷/面試技巧/大廠技術棧分析/后端開發進階/優秀開源項目/直播分享/技術視野/實戰高手等,?極客星球希望成為最有技術價值星球,盡最大努力為星球的同學提供面試,跳槽,技術成長幫助!詳情查看->極客星球
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 求點贊,在看,分享三連
總結
以上是生活随笔為你收集整理的看完微软大神写的求平均值代码,我意识到自己还是 too young 了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有问必答
- 下一篇: 面试指南|GO高性能编程精华PDF