単䞀ビットのカりントに関する詳现

8〜64ビットのサむズの倉数の単䞀ビットをカりントするためのアルゎリズムぞのアプロヌチのかなり完党な説明をしようずする蚘事をHabrコミュニティに提瀺したいず思いたす。 これらのアルゎリズムは、いわゆる「ビットマゞック」たたは「ビットアルケミヌ」のセクションに属し、倚くのプログラマヌにその矎しさず非自明性を魅了したす。 この錬金術の基本に耇雑なものは䜕もないこずを瀺したいず思いたす。たた、そのようなアルゎリズムを構成する基本的なテクニックに慣れるこずで、単䞀ビットをカりントする独自の方法を開発するこずさえできたす。



始める前に、この蚘事はプログラミングの初心者向けではないこずをすぐに譊告したいず思いたす。 読者には、最も単玔なビット操䜜ビット単䜍の「and」、「or」、シフトの抂芁を説明し、16進数システムの十分な知識があり、想像力を非垞に自信を持っお䜿甚し、垞に短いビットシヌケンスではないこずを瀺す必芁がありたす。 可胜であれば、すべおに写真が添付されたすが、完党なプレれンテヌションを単玔化するだけで、眮き換えるものではありたせん。

説明されおいるすべおの手法はC蚀語で実装され、32ビットず64ビットの2぀のモヌドでテストされおいたす。 したがっお、この蚘事をよりよく理解するには、C蚀語を少なくずもほが理解しおいる方が良いでしょう。 テストは、64ビットWindows 7䞊のプロセッサCore 2 Duo E8400 @ 3GHzで行われたした。プログラムのネットランタむムの枬定は、 runexeナヌティリティを䜿甚しお実行されたした。 説明されたアルゎリズムのすべおの゜ヌスコヌドは、Yandexディスクのアヌカむブで利甚できたす。そのコンパむルはVisual C ++、Intel C ++、GCC、およびCLangコンパむラでチェックされたす。したがっお、原則ずしお、誰かが自宅で結果をダブルチェックしたい堎合、問題はないはずです。 Linuxナヌザヌは、自分のシステムでプログラムのランタむムをテストする方法を私よりよく知っおいるず思うので、アドバむスはしたせん。

読者の䞭には、ビデオで同じものを芋る方が簡単だず感じる人がいるかもしれたせん。 このようなビデオ58分を蚘録したしたが、プレれンテヌション圢匏では、テキストの䞋䜍にあるものずたったく同じものが蚭定されおいたすが、テキストを少し埩掻させようずしおいる間に、少しドラむで厳栌なスタむルである可胜性がありたす。 したがっお、誰にずっおもより䟿利な方法で資料を研究しおください。

ビデオを芋る



錬金術の方法の1぀たたは別のセットによっお生成されたアルゎリズムを順番に説明したす。各セクションには、異なるサむズの倉数の動䜜時間を比范する衚があり、最埌にすべおのアルゎリズムの芁玄衚がありたす。 すべおのアルゎリズムは、8〜64ビットの笊号なし数倀の゚むリアスを䜿甚したす。

typedef unsigned char u8; typedef unsigned short int u16; typedef unsigned int u32; typedef unsigned long long u64; 


玠朎なアプロヌチ


明らかに、ビット錬金術はむンタビュヌで茝くために䜿甚されるのではなく、プログラムを倧幅に高速化するために䜿甚されたす。 䜕に関連する加速 タスクをより詳现に掘り䞋げる時間がないずきに思い浮かぶかもしれない些现なトリックに関連しお。 このような手法は、ビットをカりントするための単玔なアプロヌチです。数からビットを1぀ず぀「バむト」し、それらを芁玄しお、数がれロになるたで手順を繰り返したす。

 u8 CountOnes0 (u8 n) { u8 res = 0; while (n) { res += n&1; n >>= 1; } return res; } 

私はこの些现なサむクルで䜕かにコメントする理由はないず思いたす。 nの最䞊䜍ビットが1の堎合、サむクルは、最䞊䜍に達する前に数倀のすべおのビットを通過する必芁があるこずは肉県で明らかです。

入力パラメヌタヌu8のタむプをu16、u32、およびu64に倉曎するず、4぀の異なる関数が埗られたす。 æ··oticずした方法で提瀺された2 32個の数字のストリヌムでそれらをそれぞれテストしたしょう。 u8には256の異なる入力デヌタがありたすが、䞀貫性を保぀ために、これらすべおの関数ず埌続のすべおの関数に察しお2 32個の乱数を実行し、垞に同じ順序で実行したす詳现に぀いおは、アヌカむブのカリキュラムコヌドを参照しおください 

次の衚の時間は秒単䜍です。 テストでは、プログラムが3回実行され、平均時間が遞択されたした。 ゚ラヌはほずんど0.1秒を超えたせん。 最初の列はコンパむラモヌド32ビット゜ヌスコヌドたたは64ビットを反映し、4぀の列が入力デヌタの4぀のバリアントを担圓したす。
モヌドu8u16u32u64
x8638.1872.00130.49384.76
x6437.7271.51131.47227.46

ご芧のずおり、操䜜の速床は入力パラメヌタヌのサむズずずもに非垞に自然に増加したす。 このオプションは、数倀のサむズが64ビットで、蚈算がx86モヌドの堎合、䞀般的な芏則性から少し倖れおいたす。 入力パラメヌタヌを2倍にするず、プロセッサヌは4倍の䜜業を匷いられるこずは明らかであり、3倍遅い速床でしか凊理できないこずも良いこずです。

このアプロヌチの最初の利点は、実装時にミスを犯しにくいこずです。そのため、この方法で䜜成されたプログラムは、より耇雑なアルゎリズムをテストするためのリファレンスになりたすこれはたさに私の堎合でした。 2番目の利点は、汎甚性ず、あらゆるサむズの番号ぞの比范的簡単な移怍性です。

最䞋䜍ビットの「バむト」トリック


この錬金術の手法は、最䞋䜍ビットをリセットするずいう考えに基づいおいたす。 数字nがあれば、数字nから最小の1぀を取り、呪文n = nn-1を唱えるこずができたす。 以䞋のn = 232の図は、このトリックに぀いお最初に孊んだ人々の状況を明確にしたす。


プログラムコヌドはあたり倉曎されおいたせん。
 u8 CountOnes1 (u8 n) { u8 res = 0; while (n) { res ++; n &= n-1; //   . } return res; } 

これで、サむクルは数nの単䜍ずたったく同じ回数実行されたす。 これは、数倀内のすべおのビットが単䞀である最悪の堎合を排陀したせんが、平均反埩回数を倧幅に削枛したす。 このアプロヌチは、プロセッサの苊痛を倧幅に軜枛したすか 実際にはそうではありたせんが、8ビットではさらに悪化したす。 結果の抂芁テヌブルは最埌になりたすが、ここでは各セクションに独自のテヌブルがあるこずに泚意しおください。
モヌドu8u16u32u64
x8644.7355.8872.02300.78
x6440.9669.1679.13126.72


事前蚈算


急いで「ハヌド」スペルに切り替えるのではなく、最も経隓の浅い魔術垫でさえも救うこずができる最埌の簡単なトリックを考えおみたしょう。 この問題の解決策はビット錬金術に盎接適甚されたせんが、完党を期すために、画像は必ず考慮する必芁がありたす。 256ず65536の倀を持぀2぀のテヌブルを開始したす。このテヌブルでは、考えられるすべおの1バむトず2バむトの倀に察する回答がそれぞれ事前に蚈算されたす。
  u8 BitsSetTableFF[256]; //       u8 BitsSetTableFFFF[65536]; //       

これで、1バむトのプログラムは次のようになりたす。
  u8 CountOnes2_FF (u8 n) { return BitsSetTableFF[n]; } 

より倧きな数のビット数を蚈算するには、バむトに分割する必芁がありたす。 たずえば、u32の堎合、次のようなコヌドがありたす。
  u8 CountOnes2_FF (u32 n) { u8 *p = (u8*)&n; n = BitsSetTableFF[p[0]] + BitsSetTableFF[p[1]] + BitsSetTableFF[p[2]] + BitsSetTableFF[p[3]]; return n; } 

たたは、2バむトの事前蚈算テヌブルを䜿甚する堎合
  u8 CountOnes2_FFFF (u32 n) { u16 *p = (u16*)&n; n = BitsSetTableFFFF[p[0]] + BitsSetTableFFFF[p[1]]; return n; } 

さお、あなたはそれを掚枬したした、各オプションの入力パラメヌタヌnのサむズ8ビットを陀くには、䜿甚する2぀のテヌブルのどちらかに応じお、事前蚈算のための2぀のオプションがありたす。 読者はBitsSetTableFFFFFFFFテヌブルを取埗および取埗できない理由を理解しおいるず思いたすが、これが正圓化される問題がある可胜性がありたす。

事前蚈算は高速に機胜したすか すべおサむズに䟝存したす。以䞋の衚を参照しおください。 最初はシングルバむトの事前蚈算甚で、2番目はダブルバむト甚です。
モヌドu8u16u32u64
x860.011.8321.0736.25
x640.011.4424.7926.84

興味深い点x64モヌドの堎合、u64の事前蚈算ははるかに高速です。おそらくこれらは最適化機胜ですが、これは2番目の堎合には珟れたせん。
モヌドu8u16u32u64
x86---0.057.9513.01
x64---0,078.4913.01

重芁な泚意事前蚈算を䜿甚するこのアルゎリズムは、次の2぀の条件が満たされおいる堎合にのみ有益です 1远加のメモリがある堎合、 2単䞀ビットの数をテヌブル自䜓のサむズよりもはるかに倚く蚈算する必芁がある堎合、぀たりいく぀かの単玔なアルゎリズムをテヌブルに事前入力するのに費やした時間を「取り戻す」。 おそらく、実際には垞に満たされおいる゚キゟチックな条件を念頭に眮くこずもできたす。 メモリ自䜓ぞのアクセスが迅速であり、他のシステム機胜の動䜜が遅くならないようにする必芁がありたす。 実際には、テヌブルにアクセスするず、元々そこにあったものがキャッシュからスロヌされ、他のコヌドの䞀郚が遅くなる可胜性がありたす。 暪棒をすぐに芋぀けるこずはたずありたせんが、通垞のプログラムを実装する堎合、実際にこのような巚倧な最適化を必芁ずする人はほずんどいたせん。

乗算ず陀算の剰䜙


最埌に、錬金術連隊からより匷力なポヌションを取りたしょう。 乗算ず、ナニットなしの2の环乗による陀算の残りの助けを借りお、非垞に興味深いこずができたす。 1バむトの呪文のキャストを始めたしょう。 䟿宜䞊、「a」から「h」たでのラテン文字で1バむトのすべおのビットを瀺したす。 番号nは次の圢匏になりたす。


乗算n '=n⋅0x010101たずえば、接頭蟞「0x」を䜿甚しお、16進数を衚したすは、3バむトで1バむトを「耇補」するだけです。

ここで、24ビットを粟神的に各3ビットの8ブロックに分割したす次の図、プレヌトの最初の行を参照。 次に、マスク0x249249プレヌトの2行目でビット単䜍の「and」を䜿甚しお、各ブロックの2぀の䞊䜍ビットをれロにしたす。


衚の3行目は、マスクの16進衚蚘法を説明しおいたす。 最埌の行は、達成した結果を瀺しおいたす。元のバむトのすべおのビットは、それぞれ独自の3ビットブロックに含たれおいたすが、順序は異なりたす順序は重芁ではありたせん。

ここで泚意これら8぀のブロックを远加する必芁がありたす-そしお、ビットの合蚈を取埗したす

ある数Aを2 k -1で陀算した䜙りが、同じく数2の-1を法ずする数Aのkビットブロックの合蚈を䞎えるこずがわかりたす。
蚌明
数倀Aバむナリをそれぞれのkビットのブロックに分割したす必芁に応じお、最埌の最も叀いれロのブロックを远加できたす。 A iで i番目のブロックを瀺したす。 次に、これらのブロックの合蚈に察応する2のべき乗を掛けた数Aの倀を曞き蟌みたす。
A = A 0⋅20⋅k + A 1⋅21⋅k + ... + A N- 1⋅2 N-1⋅k 、
ここで、Nはブロック数です。
次に、A mod2 k -1を蚈算したす。
A mod2 k -1=A 0⋅20⋅k + A 1⋅21⋅k + ... + A N- 1⋅2 N-1⋅k mod2 k -1= A 0 + A 1 + ... + A N-1 mod2 k -1。
これは、負でない敎数iに察しお2i⋅k = 1mod2 k -1であるためです。 ただし、ここでは、k> 1のずきにトリックが意味をなすこずが重芁です。そうでなければ、モゞュヌル1をどのように解釈するかが完党に明確ではありたせん。 したがっお、2 k -1を法ずするブロックの合蚈を埗たした。

぀たり、受け取った数から、2 3 -17で割った䜙りを取埗する必芁がありたす。8ブロックの合蚈は7を法ずしおいたす。この堎合、ビットの合蚈は7たたは8になる可胜性がありたす。アルゎリズムはそれぞれ0ず1を返したす。 しかし、芋おみたしょうどのような堎合に答え8を埗るこずができたすか n = 255の堎合のみ。 そしお、どの堎合に0を取埗できたすか n = 0の堎合のみ。 したがっお、7の剰䜙を取った埌のアルゎリズムが0を䞎える堎合、入力でn = 0を受け取ったか、たたは数に正確に7単䜍ビットがありたす。 この掚論を芁玄するず、次のコヌドが埗られたす。
 u8 CountOnes3 (u8 n) { if (n == 0) return 0; //  ,   0. if (n == 0xFF) return 8; //  ,   8. n = (0x010101*n & 0x249249) % 7; //      7. if (n == 0) return 7; //   7  . return n; // ,     1  6  . } 

nのサむズが16ビットの堎合、8ビットの2぀の郚分に分割できたす。 たずえば、次のように
 u8 CountOnes3 (u16 n) { return CountOnes3 (u8(n&0xFF)) + CountOnes3 (u8(n>>8)); 

32ビットず64ビットの堎合、そのようなパヌティションは理論的にも意味がありたせん。3回の分岐での乗算ず陀算の残りは、4回たたは8回連続で実行されるず非垞に高䟡になりたす。 しかし、私はあなたのために空の堎所を䞋の衚に残したので、もしあなたが私を信じないなら、自分でそれらを蚘入しおください。 同様のプロセッサを䜿甚しおいる堎合、CountBits1プロシヌゞャに匹敵する結果が埗られる可胜性が最も高くなりたすここでSSEを䜿甚した最適化の可胜性に぀いおは話しおいないが、これは別の問題です。
モヌドu8u16u32u64
x8612,4230.57------
x6413.8833.88------

もちろん、このトリックは分岐せずに実行できたすが、ブロックに数倀を分割するずき、0から8たでのすべおの数倀がブロックに含たれおいる必芁があり、これは4ビットブロックおよびそれ以䞊の堎合にのみ達成できたす。 4ビットブロックの合蚈を実行するには、数倀を正しく「耇補」する係数を遞択し、2 4 -1 = 15による陀算の残りを取埗しお、結果のブロックを远加する必芁がありたす。 経隓豊富な錬金術垫数孊を知っおいるは、0x08040201のような芁玠を簡単に芋぀けるこずができたす。 なぜ圌はこのように遞ばれたのですか


実際には、元の数のすべおのビットが4ビットブロックの正しい䜍眮を占める必芁があり䞊の図、8ず4は比范的玠数ではないため、通垞の8ビットの4回のコピヌでは正しい堎所が埗られたせんビット。 9は盞互に単玔な4であるため、バむトにれロを1぀远加する必芁がありたす。぀たり、9ビットを耇補するために、サむズは36ビットですが、元のバむトのすべおのビットが4ビットブロックの䞋䜍にある数倀を取埗したす。 各ブロックの3぀の最䞊䜍ビットをリセットするために、0x111111111䞊の図の2行目のビット単䜍の「and」を取るだけです。 次に、ブロックを折り畳む必芁がありたす。

このアプロヌチを䜿甚するず、バむト内の単䞀ビットを蚈算するプログラムは非垞に簡単になりたす。
 u8 CountOnes3_x64 (u8 n) { return ((u64)0x08040201*n & 0x111111111) % 15; } 

プログラムの欠点は明らかです。64ビットの算術挔算を行っお、その埌の結果をすべお行う必芁がありたす。 実際には、このプログラムは64のうち33ビットのみを䜿甚するこずに気付きたす最䞊䜍3ビットはリセットされたす。原則ずしお、これらの蚈算を32ビット算術に転送する方法を理解できたすが、そのような最適化に関する話はこのトピックには含たれおいたせんガむド。 ずりあえず、テクニックを勉匷しおみたしょう。特定のタスクのために既に自分でそれらを最適化する必芁がありたす。

このトリックが正しく機胜するように、倉数nのサむズの問題に答えたす。 15で陀算した残りの郚分を䜿甚するため、このような倉数は14ビットを超えるサむズを持぀こずはできたせん。そうしないず、以前ず同様に分岐を適甚する必芁がありたす。 ただし、14ビットの堎合、すべおのビットが所定の䜍眮に収たるように14ビットに1぀のれロを远加するず、受信が機胜したす。 ここで、党䜓ずしお、受信の本質を孊び、耇補の乗数ず䞍芁なビットを自分でリセットするためのマスクを簡単に遞択できるず仮定したす。 完成した結果をすぐに衚瀺したす。
 u8 CountOnes3_x64 (u14 n) { //    (   )! return (n*0x200040008001llu & 0x111111111111111llu) % 15; } 

䞊蚘のプログラムは、14ビットの倉数が笊号なしの堎合、コヌドがどのように芋えるかを瀺しおいたす。 同じコヌドが15ビットの倉数で機胜したすが、そのうちの14のみが1に等しいずいう条件の䞋で、たたはn = 0x7FFFの堎合を個別に調べたす。 タむプu16の倉数に正しいコヌドを曞き蟌むには、これらすべおを理解する必芁がありたす。 アむデアは、最初に最䞋䜍ビットを「噛んで」、残りの15ビット数のビットをカりントしおから、「ビットオフ」ビットを远加し盎すこずです。
 u8 CountOnes3_x64 (u16 n) { u8 leastBit = n&1; //   . n >>= 1; //   15   . if (n == 0) return leastBit; //   0,     . if (n == 0x7FFF) return leastBit + 15; //  ,   15+ . return leastBit + (n*0x200040008001llu & 0x111111111111111llu) % 15; //  ( 14+ ). } 

n 32ビットの堎合、より深刻な顔を既に思い浮かべる必芁がありたす...最初に、答えは6ビットのみに収たりたすが、n = 2 32 -1の別のケヌスを考慮し、5ビットのフィヌルドで冷静に蚈算を行うこずができたす。 これによりスペヌスが節玄され、nの32ビットフィヌルドをそれぞれ12ビットの3぀の郚分に分割できたすはい、最埌のフィヌルドは完党ではありたせん。 12⋅5= 60なので、1぀の12ビットフィヌルドを5回耇補しお係数を遞択し、5ビットブロックを远加しお、残りの31で陀算したす。これを各フィヌルドで3回行う必芁がありたす。 結果を芁玄するず、次のコヌドが埗られたす。
 u8 CountOnes3_x64 ( u32 n ) { if (n == 0) return 0; if (n + 1 == 0) return 32; u64 res = (n&0xFFF)*0x1001001001001llu & 0x84210842108421llu; res += ((n&0xFFF000)>>12)*0x1001001001001llu & 0x84210842108421llu; res += (n>>24)*0x1001001001001llu & 0x84210842108421llu; res %= 0x1F; if (res == 0) return 31; return res; } 

ここでも同様に、3぀のブランチの代わりに、陀算から3぀の残基を取るこずができたしたが、ブランチオプションを遞択したした。これは、私のプロセッサでより良く機胜したす。

64ビットのサむズであるnに぀いおは、乗算や加算がそれほど倚くない適切な呪文を思い付くこずができたせんでした。 6たたは7のいずれかが刀明し、これはこのようなタスクには倚すぎたす。 もう1぀のオプションは、128ビット挔算を䜿甚するこずですが、それがどのような「ロヌルバック」を行うのかわからないため、準備のできおいないメむゞを壁に投げるこずができたす:)

仕事の時間をよく芋おみたしょう。
モヌドu8u16u32u64
x8639.7860.48146.78---
x646.7812.2831.12---

この衚から明らかな結論は、䞀般的にアルゎリズムは悪くないものの、32ビット実行モヌドでは64ビット挔算はあたり認識されないずいうこずです。 ケヌスu3224.79 sのシングルバむトテヌブルのx64モヌドでの事前蚈算アルゎリズムの速床を思い出せば、このアルゎリズムはわずか25遅れおいるこずがわかり、これが次のセクションで具䜓化された競争の理由です。

剰䜙を乗算ずシフトに眮き換える


残りを取埗する操䜜の欠点は誰にずっおも明らかです。 これは郚門であり、郚門は長いです。 もちろん、珟代のコンパむラヌは錬金術を知っおおり、陀算を乗算ずシフトで眮き換える方法を知っおおり、剰䜙を埗るには、陀数から陀数を掛けた商を匕く必芁がありたす。 しかし、それはただ長い時間です コヌドスペルキャスタヌの叀代の巻物には、以前のアルゎリズムを最適化する興味深い方法が保存されおいたした。 kビットブロックを芁玄するには、陀算の残りの郚分を䜿甚するのではなく、ブロックを远加しおビットをリセットするマスクによる別の乗算を䜿甚したす。 サむズがn 1バむトの堎合、次のようになりたす。

たず、バむトを再床3回耇補し、䞊蚘で既に枡された匏0x010101⋅n0x249249を䜿甚しお、各3ビットブロックの最䞊䜍2ビットを削陀したす。


䟿宜䞊、各3ビットブロックに倧文字のラテン文字を付けたした。 次に、結果に同じマスク0x249249を掛けたす。 マスクは各3番目の䜍眮に1぀のビットを含むため、この乗算はそれ自䜓の数を8回加算するのず同じで、毎回3ビットのシフトがありたす。


䜕が芋えたすか ビット21から23は適切な量を提䟛したす さらに、右偎のブロックのオヌバヌフロヌは、どのブロックにも7を超える数がないため、䞍可胜です。唯䞀の問題は、合蚈が8の堎合、0が埗られるこずですが、ケヌスは個別に怜蚎できたす。
 u8 CountOnes3_x64_m (u8 n) { if (n == 0xFF) return 8; return ((u64(0x010101*n & 0x249249) * 0x249249) >> 21) & 0x7; } 

実際、前のセクションのコヌドを䜿甚し、乗算、シフト、およびビット単䜍の「And」による7での陀算の残りを最埌に䜿甚するこずで眮き換えたした。 ただし、3぀のブランチの代わりに、1぀だけが残りたす。

同様のプログラムを16ビットでコンパむルするには、前のセクションのコヌドを䜿甚する必芁がありたす。このセクションでは、15による陀算の残りを取埗しおこの手順を乗算に眮き換える方法を瀺したす。 同時に、どの条件をコヌドから削陀できるかを簡単に確認できたす。
 u8 CountOnes3_x64_m (u16 n) { u8 leastBit = n&1; //    n >>= 1; //   15 . return leastBit + (( (n*0x200040008001llu & 0x111111111111111llu)*0x111111111111111llu >> 56) & 0xF); //  ( 15 +  ). } 

32ビットに぀いおも同じこずを行いたす。前のセクションのコヌドを䜿甚し、玙に少し曞いお、剰䜙を乗算に眮き換えた堎合のシフトがどうなるかを理解したす。
 u8 CountOnes3_x64_m ( u32 n ) { if (n+1 == 0) return 32; u64 res = (n&0xFFF)*0x1001001001001llu & 0x84210842108421llu; res += ((n&0xFFF000)>>12)*0x1001001001001llu & 0x84210842108421llu; res += (n>>24)*0x1001001001001llu & 0x84210842108421llu; return (res*0x84210842108421llu >> 55) & 0x1F; } 

64ビットの堎合、プロセッサをストヌブずしお機胜させないようなものも思い぀きたせんでした。
モヌドu8u16u32u64
x8612.6642.3799.90---
x643,544,5118.35---

x64モヌドの結果に喜んで驚いた。 予想どおり、u32の堎合、1バむトのテヌブルを䜿甚しお事前蚈算をやり盎したした。 事前蚈算を远い越すこずさえ可胜ですか 良い質問:)

䞊列加算


おそらくこれが最も䞀般的なトリックであり、非垞に倚くの堎合、経隓の少ないスペルキャスタヌが次々に繰り返し、正確に機胜する方法を理解しおいたせん。

1バむトから始めたしょう。 バむトは2ビットの4぀のフィヌルドで構成され、最初にこれらのフィヌルドのビットを加算しお、次のように蚀いたす。
 n = (n>>1)&0x55 + (n&0x55); 

以䞋に、この操䜜の説明図を瀺したす前ず同様に、1バむトのビットを最初のラテン文字で瀺したす。

1぀のビット単䜍のANDは各2ビットブロックの䞋䜍ビットのみを残し、2番目は䞊䜍ビットを残したすが、それらを䞋䜍ビットに察応する䜍眮にシフトしたす。合蚈の結果ずしお、各2ビットブロック䞊の図の最埌の行の隣接ビットの合蚈を取埗したす。

次に、2ビットフィヌルドの数倀をペアにしお、結果を2぀の4ビットフィヌルドに入れたす。
 n = (n>>2)&0x33 + (n&0x33); 

次の図は結果を説明しおいたす。私はこれ以䞊苊劎せずに今持っおきたす

最埌に、4ビットフィヌルドに2぀の数倀を远加したす。
 n = (n>>4)&0x0F + (n&0x0F); 


同様に、2の环乗に等しい任意のビット数に受信を拡匵できたす。スペル行の数は、ビット数の2進察数です。アむデアを理解したら、以䞋に蚘述されおいる4぀の関数を簡単に芋お、理解が正しいこずを確認しおください。
 u8 CountOnes4 (u8 n) { n = ((n>>1) & 0x55) + (n & 0x55); n = ((n>>2) & 0x33) + (n & 0x33); n = ((n>>4) & 0x0F) + (n & 0x0F); return n; } 

 u8 CountOnes4 (u16 n) { n = ((n>>1) & 0x5555) + (n & 0x5555); n = ((n>>2) & 0x3333) + (n & 0x3333); n = ((n>>4) & 0x0F0F) + (n & 0x0F0F); n = ((n>>8) & 0x00FF) + (n & 0x00FF); return n; } 

 u8 CountOnes4 (u32 n) { n = ((n>>1) & 0x55555555) + (n & 0x55555555); n = ((n>>2) & 0x33333333) + (n & 0x33333333); n = ((n>>4) & 0x0F0F0F0F) + (n & 0x0F0F0F0F); n = ((n>>8) & 0x00FF00FF) + (n & 0x00FF00FF); n = ((n>>16) & 0x0000FFFF) + (n & 0x0000FFFF); return n; } 

 u8 CountOnes4 (u64 n) { n = ((n>>1) & 0x5555555555555555llu) + (n & 0x5555555555555555llu); n = ((n>>2) & 0x3333333333333333llu) + (n & 0x3333333333333333llu); n = ((n>>4) & 0x0F0F0F0F0F0F0F0Fllu) + (n & 0x0F0F0F0F0F0F0F0Fllu); n = ((n>>8) & 0x00FF00FF00FF00FFllu) + (n & 0x00FF00FF00FF00FFllu); n = ((n>>16) & 0x0000FFFF0000FFFFllu) + (n & 0x0000FFFF0000FFFFllu); n = ((n>>32) & 0x00000000FFFFFFFFllu) + (n & 0x00000000FFFFFFFFllu); return n; } 

䞊列加算はそこで終わりたせん。このアむデアは、各ビットで同じビットマスクが2回䜿甚されるこずを芳察するこずで開発できたす。可胜ですが、すぐにはできたせん。u32のコヌドを䟋にずるず、次のこずができたすコメントを参照。
 u8 CountOnes4 (u32 n) { n = ((n>>1) & 0x55555555) + (n & 0x55555555); //     n = ((n>>2) & 0x33333333) + (n & 0x33333333); //   n = ((n>>4) & 0x0F0F0F0F) + (n & 0x0F0F0F0F); //   &   n = ((n>>8) & 0x00FF00FF) + (n & 0x00FF00FF); //   &   n = ((n>>16) & 0x0000FFFF) + (n & 0x0000FFFF); //    & return n; //    8-  . } 

挔習ずしお、次のコヌドが前のコヌドを正確に反映する理由を自分で蚌明したいず思いたす。最初の行では、ヒントを瀺したすが、すぐには調べたせん。
ヒント
ab 2a+b, 
?

 u8 CountOnes4_opt (u32 n) { n -= (n>>1) & 0x55555555; n = ((n>>2) & 0x33333333 ) + (n & 0x33333333); n = ((n>>4) + n) & 0x0F0F0F0F; n = ((n>>8) + n) & 0x00FF00FF; n = ((n>>16) + n); return n; } 

他のデヌタ型でも同様の最適化オプションが可胜です。

2぀のテヌブルを以䞋にリストしたす。1぀は通垞の䞊列合蚈甚、もう1぀は最適化甚です。
モヌドu8u16u32u64
x867.5214.1021.1262.70
x648.0611.8921.3022.59

モヌドu8u16u32u64
x867.1811.8918.8665.00
x648.0910.2719,2019,20


䞀般に、最適化されたアルゎリズムはうたく機胜したすが、u64のx86モヌドでは通垞のアルゎリズムに負けおいたす。

組み合わせた方法


単䞀ビットをカりントするための最適なオプションは、䞊列法最適化ありず、ブロックの合蚈を蚈算する乗算を䌎う耇補法であるこずがわかりたす。䞡方の方法を組み合わせお、組み合わせたアルゎリズムを取埗できたす。

最初に行うこずは、䞊列アルゎリズムの最初の3行を完了するこずです。これにより、数倀の各バむトの正確なビット数がわかりたす。たずえば、u32の堎合、次の手順を実行したす。
  n -= (n>>1) & 0x55555555; n = ((n>>2) & 0x33333333) + (n & 0x33333333); n = (((n>>4) + n) & 0x0F0F0F0F ); 

これで、数倀nは4バむトで構成されたす。これは、4぀の数倀ず芋なされる必芁があり、その合蚈が探しおいたす。


数倀nに0x01010101を掛けるず、これらの4バむトの合蚈を芋぀けるこずができたす。あなたは今、そのような乗算が䜕を意味するのかをよく理解しおいたす、答えがどこにあるかを決定する䟿宜のために、私は絵を匕甚したす


答えは3バむトです0から数える堎合。したがっお、u32の組み合わせたトリックは次のようになりたす。
 u8 CountOnes5 ( u32 n ) { n -= (n>>1) & 0x55555555; n = ((n>>2) & 0x33333333 ) + (n & 0x33333333); n = ((((n>>4) + n) & 0x0F0F0F0F) * 0x01010101) >> 24; return n; //      8  . } 

圌はu16です。
 u8 CountOnes5 (u16 n) { n -= (n>>1) & 0x5555; n = ((n>>2) & 0x3333) + (n & 0x3333); n = ((((n>>4) + n) & 0x0F0F) * 0x0101) >> 8; return n; //      8  . } 

圌はu64です。
 u8 CountOnes5 ( u64 n ) { n -= (n>>1) & 0x5555555555555555llu; n = ((n>>2) & 0x3333333333333333llu ) + (n & 0x3333333333333333llu); n = ((((n>>4) + n) & 0x0F0F0F0F0F0F0F0Fllu) * 0x0101010101010101) >> 56; return n; //      8  . } 

このメ゜ッドの速床は、ファむナルテヌブルですぐに確認できたす。

最終比范


以䞋の2぀の衚を調べお、興味のある独自の結論を出すこずを読者に提案したす。それらの䞭で、プログラムを実装するメ゜ッドの名前を指定し、それぞれの特定のケヌスで最良ず思われるアプロヌチを長方圢の枠でマヌクしたした。事前蚈算が垞に勝ちだず思っおいた人は、x64モヌドでは少し驚きたす。

x86コンパむルモヌドの最終比范。


x64コンパむルモヌドの最終比范。


発蚀


いずれにしおも、ファむナルテヌブルを䜕らかのアプロヌチを支持する蚌拠ず芋なさないでください。プロセッサずコンパむラでは、このようなテヌブルのいく぀かの数倀は完党に異なるず信じおください。残念ながら、どのアルゎリズムがいずれの堎合でもより良くなるず確信するこずはできたせん。タスクごずに特定の方法を匷化する必芁があり、残念ながら、普遍的な高速アルゎリズムは存圚したせん。

私は自分自身に぀いお知っおいるアむデアの抂芁を説明したしたが、これらは異なる組み合わせでの特定の実装が非垞に異なる可胜性があるアむデアです。これらのアむデアをさたざたな方法で組み合わせるず、単䞀のビットをカりントするための膚倧な数のさたざたなアルゎリズムを取埗できたす。それぞれのアルゎリズムは、堎合によっおは適切であるこずがわかりたす。

ご枅聎ありがずうございたした。 じゃあね

UPDSSE4.2をサポヌトするプロセッサがないため、SSE4.2からのPOPCNT呜什はテストリストに含たれおいたせん。

Source: https://habr.com/ru/post/J276957/


All Articles