0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何取整求個無符號整數(shù)的平均值

算法與數(shù)據(jù)結(jié)構(gòu) ? 來源:量子位 ? 作者:量子位 ? 2022-03-18 10:24 ? 次閱讀
取整求個無符號整數(shù)的平均值,居然也能整出花兒來?

這不,微軟大神Raymond Chen最近的一篇長文直接引爆外網(wǎng)技術(shù)平臺,引發(fā)無數(shù)討論:

01e83eb8-9fc3-11ec-952b-dac502259ad0.png

無數(shù)人點進(jìn)去時無比自信:不就是一個簡單的相加后除二的小學(xué)生編程題嗎?

unsignedaverage(unsigneda,unsignedb)
{
return(a+b)/2;
}

但跟著大神的一路深挖,卻逐漸目瞪狗呆……

沒那么簡單的求平均值

先從開頭提到的小學(xué)生都會的方法看起,這個簡單的方法有個致命的缺陷:

如果無符號整數(shù)的長度為32位,那么如果兩個相加的值都為最大長度的一半,那么僅在第一步相加時,就會發(fā)生內(nèi)存溢出。

也就是average(0x80000000U, 0x80000000U)=0。

不過解決方法也不少,大多數(shù)有經(jīng)驗的開發(fā)者首先能想到的,就是預(yù)先限制相加的數(shù)字長度,避免溢出。

具體有兩種方法:

1、當(dāng)知道相加的兩個無符號整數(shù)中的較大值時,減去較小值再除二,以提前減少長度

unsignedaverage(unsignedlow,unsignedhigh)
{
returnlow+(high-low)/2;
}

2、對兩個無符號整數(shù)預(yù)先進(jìn)行除法,同時通過按位與修正低位數(shù)字,保證在兩個整數(shù)都為奇數(shù)時,結(jié)果仍然正確。

(順帶一提,這是一個被申請了專利的方法,2016年過期)

unsignedaverage(unsigneda,unsignedb)
{
return(a/2)+(b/2)+(a&b&1);
}

這兩個都是較為常見的思路,不少網(wǎng)友也表示,自己最快想到的就是2016年專利方法。

同樣能被廣大網(wǎng)友快速想到的方法還有SWARSIMD within a register)

unsignedaverage(unsigneda,unsignedb)
{
return(a&b)+(a^b)/2;//變體(a^b)+(a&b)*2

以及C++ 20版本中的std: : midpoint函數(shù)。

接下來,作者提出了第二種思路

如果無符號整數(shù)是32位而本機寄存器大小是64位,或者編譯器支持多字運算,就可以將相加值強制轉(zhuǎn)化為長整型數(shù)據(jù)。

unsignedaverage(unsigneda,unsignedb)
{
//Suppose"unsigned"isa32-bittypeand
//"unsignedlonglong"isa64-bittype.
return((unsignedlonglong)a+b)/2;
}

不過,這里有一個需要特別注意的點:

必須要保證64位寄存器的前32位都為0,才不會影響剩余的32位值。

像是x86-64和aarch64這些架構(gòu)會自動將32位值零擴(kuò)展為64位值:

//x86-64:Assumeecx=a,edx=b,upper32bitsunknown
moveax,ecx;rax=ecxzero-extendedto64-bitvalue
movedx,edx;rdx=edxzero-extendedto64-bitvalue
addrax,rdx;64-bitaddition:rax=rax+rdx
shrrax,1;64-bitshift:rax=rax>>1
;resultiszero-extended
;Answerineax

//AArch64(ARM64-bit):Assumew0=a,w1=b,upper32bitsunknown
uxtwx0,w0;x0=w0zero-extendedto64-bitvalue
uxtwx1,w1;x1=w1zero-extendedto64-bitvalue
addx0,x1;64-bitaddition:x0=x0+x1
ubfxx0,x0,1,32;Extractbits1through32fromresult
;(shift+zero-extendinoneinstruction)
;Answerinx0

而Alpha AXP、mips64等架構(gòu)則會將32位值符號擴(kuò)展為64位值。

這種時候,就需要額外增加歸零的指令,比如通過向左進(jìn)位兩字的刪除指令rldicl

//AlphaAXP:Assumea0=a,a1=b,bothincanonicalform
inslla0,#0,a0;a0=a0zero-extendedto64-bitvalue
inslla1,#0,a1;a1=a1zero-extendedto64-bitvalue
addqa0,a1,v0;64-bitaddition:v0=a0+a1
srlv0,#1,v0;64-bitshift:v0=v0>>1
addlzero,v0,v0;Forcecanonicalform
;Answerinv0

//MIPS64:Assumea0=a,a1=b,sign-extended
dexta0,a0,0,32;Zero-extenda0to64-bitvalue
dexta1,a1,0,32;Zero-extenda1to64-bitvalue
dadduv0,a0,a1;64-bitaddition:v0=a0+a1
dsrlv0,v0,#1;64-bitshift:v0=v0>>1
sllv0,#0,v0;Sign-extendresult
;Answerinv0

//Power64:Assumer3=a,r4=b,zero-extended
addr3,r3,r4;64-bitaddition:r3=r3+r4
rldiclr3,r3,63,32;Extractbits63through32fromresult
;(shift+zero-extendinoneinstruction)
;resultinr3

或者直接訪問比本機寄存器更大的SIMD寄存器,當(dāng)然,從通用寄存器跨越到SIMD寄存器肯定也會增加內(nèi)存消耗。

如果電腦處理器支持進(jìn)位加法,那么還可以采用第三種思路

這時,如果寄存器大小為n位,那么兩個n位的無符號整數(shù)的和就可以理解為n+1位,通過RCR(帶進(jìn)位循環(huán)右移)指令,就可以得到正確的平均值,且不損失溢出的位。

020224d6-9fc3-11ec-952b-dac502259ad0.png

帶進(jìn)位循環(huán)右移
//x86-32
moveax,a
addeax,b;Add,overflowgoesintocarrybit
rcreax,1;Rotaterightoneplacethroughcarry

//x86-64
movrax,a
addrax,b;Add,overflowgoesintocarrybit
rcrrax,1;Rotaterightoneplacethroughcarry

//32-bitARM(A32)
movr0,a
addsr0,b;Add,overflowgoesintocarrybit
rrxr0;Rotaterightoneplacethroughcarry

//SH-3
clrt;ClearTflag
mova,r0
addcb,r0;r0=r0+b+T,overflowgoesintoTbit
rotcrr0;Rotaterightoneplacethroughcarry

那如果處理器不支持帶進(jìn)位循環(huán)右移操作呢?

也可以使用內(nèi)循環(huán)(rotation intrinsic)

unsignedaverage(unsigneda,unsignedb)
{
#ifdefined(_MSC_VER)
unsignedsum;
autocarry=_addcarry_u32(0,a,b,&sum);
sum=(sum&~1)|carry;
return_rotr(sum,1);
#elifdefined(__clang__)
unsignedcarry;
sum=(sum&~1)|carry;
autosum=__builtin_addc(a,b,0,&carry);
return__builtin_rotateright32(sum,1);
#else
#errorUnsupportedcompiler.
#endif
}

結(jié)果是,x86架構(gòu)下的代碼生成沒有發(fā)生什么變化,MSCver架構(gòu)下的代碼生成變得更糟,而arm-thumb2的clang 的代碼生成更好了。

//_MSC_VER
movecx,a
addecx,b;Add,overflowgoesintocarrybit
setcal;al=1ifcarryset
andecx,-2;Clearbottombit
movzxecx,al;Zero-extendbyteto32-bitvalue
oreax,ecx;Combine
rorear,1;Rotaterightoneposition
;Resultineax

//__clang__
movecx,a
addecx,b;Add,overflowgoesintocarrybit
setcal;al=1ifcarryset
shldeax,ecx,31;Shiftleft64-bitvalue

//__clang__withARM-Thumb2
movsr2,#0;Preparetoreceivecarry
addsr0,r0,r1;Calculatesumwithflags
adcsr2,r2;r2holdscarry
lsrsr0,r0,#1;Shiftsumrightoneposition
lslsr1,r2,#31;Movecarrytobit31
addsr0,r1,r0;Combine

微軟大神的思考們

Raymond Chen1992年加入微軟,迄今為止已任職25年,做UEX-Shell,也參與Windows開發(fā),Windows系統(tǒng)的很多最初UI架構(gòu)就是他搞起來的。

他在MSDN 上建立的blogThe Old New Thing也是業(yè)內(nèi)非常出名的純技術(shù)向產(chǎn)出網(wǎng)站。

這篇博客的評論區(qū)們也是微軟的各路大神出沒,繼續(xù)深入探討。

有人提出了新方法,在MIPS ASM共有36個循環(huán):

unsignedavg(unsigneda,unsignedb)
{
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 ) ) 作為進(jìn)位放入加法器中計算呢?

還有人在評論區(qū)推薦了TopSpeed編譯器,能夠通過指定合適的代碼字節(jié)和調(diào)用約定來定義一個內(nèi)聯(lián)函數(shù),以解決“乘除結(jié)果是16位,中間計算值卻不是”的情況。

只能說,學(xué)無止境啊。

原文標(biāo)題:看完微軟大神寫的求平均值代碼,我意識到自己還是too young了

文章出處:【微信公眾號:算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
審核編輯:湯梓紅


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 微軟
    +關(guān)注

    關(guān)注

    4

    文章

    6534

    瀏覽量

    103807
  • 寄存器
    +關(guān)注

    關(guān)注

    31

    文章

    5268

    瀏覽量

    119644
  • 編程
    +關(guān)注

    關(guān)注

    88

    文章

    3543

    瀏覽量

    93465

原文標(biāo)題:看完微軟大神寫的求平均值代碼,我意識到自己還是too young了

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    求解平均值

    整數(shù)型定義的數(shù)據(jù)op1:in integer range 0 to 4095;op2:in integer range 0 to 4095;用mid=(op1+op2)/2這樣計算能求得兩數(shù)的平均值么?
    發(fā)表于 06-11 21:29

    平均值,并顯示

    采集好的10萬數(shù)據(jù),每次一千然后計算這一千數(shù)的平均值,最后將這所有平均值在波形圖中顯示出
    發(fā)表于 05-24 12:13

    平均值并顯示

    采集好的10萬數(shù)據(jù),每次一千然后計算這一千數(shù)的平均值,最后將這所有平均值在波形圖中顯示出
    發(fā)表于 05-24 12:16

    連續(xù)采樣平均值比較較小值

    平均值小,繼續(xù)比較反復(fù)進(jìn)行,直到前100點的平均值比后100點的小為止,由于得到的值得用在該while循環(huán)里,故只能在循環(huán)里邊采集邊處
    發(fā)表于 01-13 19:21

    將1000~100的隨機數(shù)后構(gòu)成10*10的二位數(shù)組,計算該二維數(shù)組的最大值及平均值

    labviwe實現(xiàn)將1000~100的隨機數(shù)后構(gòu)成10*10的二位數(shù)組,計算該二維數(shù)組的最大值及平均值,大神解答。。。
    發(fā)表于 05-29 20:45

    ROM中表格中8符號數(shù)的算術(shù)平均值

    1、實驗內(nèi)容一 1.1、問題一: 設(shè)ROM中的表格TAB中存儲有8符號數(shù)(小于等于10),這8
    發(fā)表于 07-14 08:08

    符號數(shù)的平均數(shù)

    符號數(shù)的平均數(shù)文章目錄題目重述問題分析以及求解思路程序代碼題目重述試內(nèi)部RAM30H~37H單元中8
    發(fā)表于 12-01 08:01

    平均值采樣法的使用

    在上一篇文章單片機ADC采樣算法---平均值采樣法中分析了平均值采樣法的使用,上篇文章中的平均值采樣法是連續(xù)采樣100數(shù)據(jù),然后
    發(fā)表于 01-11 07:58

    ADC初始平均值的方法

    ADC初始平均值的方法
    發(fā)表于 02-09 06:49

    雙字節(jié)十六進(jìn)制符號數(shù)據(jù)塊的平均值

    雙字節(jié)十六進(jìn)制符號數(shù)據(jù)塊的平均值 入口條件:數(shù)據(jù)塊的首址在DPTR中,雙字節(jié)數(shù)據(jù)總個數(shù)在R7中。出口信息:平均值在R4、R5中。影
    發(fā)表于 01-19 23:03 ?1390次閱讀

    單字節(jié)十六進(jìn)制符號數(shù)據(jù)塊的平均值

    單字節(jié)十六進(jìn)制符號數(shù)據(jù)塊的平均值 入口條件:數(shù)據(jù)塊的首址在DPTR中,數(shù)據(jù)個數(shù)在R7中。出口信息:平均值在累加器A中。影響
    發(fā)表于 01-19 23:03 ?1464次閱讀

    什么是平均值? 平均值是什么意思?

    什么是平均值? 平均值是什么意思? 交變電流的平均值是指在某段時間內(nèi)流過電路的總電荷與該段時間的比值。正弦量
    發(fā)表于 04-17 10:31 ?1w次閱讀

    2路輸入平均值的運算電路

    2路輸入平均值的運算電路 電路功能 一提到平均值運算,人們往
    發(fā)表于 05-08 11:47 ?7694次閱讀
    <b class='flag-5'>求</b>2路輸入<b class='flag-5'>平均值</b>的運算電路

    排除最大最小值后平均值

    輸入數(shù)據(jù)中排除最大最小值后平均值的算法,測試通過。
    發(fā)表于 08-18 18:24 ?11次下載

    ADC初始平均值

    主要邏輯:8次平均值,然后存入數(shù)組,求和然后取平均值。void AdcAverageInit(void){ u16 ADCInit_SPVal = 0; u8 n; for(n=0; n&
    發(fā)表于 12-06 10:21 ?6次下載
    ADC<b class='flag-5'>取</b>初始<b class='flag-5'>平均值</b>