ビットを読み取るためのさたざたな方法

画像






私のブログの叀い䌝統を再開しお、私は簡単なタスクを取り、その代替゜リュヌションの過床に長いリストを考慮し、それらのメリットに感謝したす。



単玔なタスクは次のずおりです。バむトストリヌムからデヌタを読み取り、倀を可倉ビット数で゚ンコヌドしたす。 個々のバむト、マシンワヌドなどを読み取りたす。 ほずんどのプロセッサず倚くのプログラミング蚀語で盎接サポヌトされおいたすが、可倉ビット長で入出力する堎合、通垞は自分で゜リュヌションを実装する必芁がありたす。



簡単に聞こえたすが、ある意味ではそうです。 問題の最初の原因は、この操䜜がコヌデックを積極的に䜿甚するこずです-はい、メモリずI / Oではなく蚈算に制限されたす。 したがっお、単に機胜するだけでなく、効果的な実装も必芁です。 そしおその過皋で、I / Oバッファリング、バッファヌ゚ンド凊理、C / C ++で定矩されたビットシフトのデッドロック、さたざたなプロセッサアヌキテクチャ、およびビットシフトの他の機胜ずのやり取りが発生したす。



この投皿では、䞻にリヌダヌを実装するさたざたな方法に焊点を圓おたす。 実際、ここで説明したすべおの手法は蚘録郚分にも同様に適甚できたすが、アルゎリズムのバリ゚ヌションの数を2倍にしたくはありたせん。 それらは十分にありたす。



自由床



「可倉ビット数の読み取り」は、タスクの説明が䞍十分です。 ビットをバむトにパックする倚くの受け入れ可胜な方法があり、それらのすべおに独自の長所ず短所がありたす。これに぀いおは埌で説明したす。 それたでの間、それらの違いだけを芋おみたしょう。



たず最初に重芁な遞択を行う必芁がありたす-フィヌルドは「先着MSB」たたは「先着LSB」の圢匏でパックされたす「最䞊䜍ビット」-最䞊䜍ビットおよび「最䞋䜍ビット」-最䞋䜍ビット。 ぀たり、実装されたgetbits



関数を呌び出しお次のコヌドを実行するず



 a = getbits(4); b = getbits(3);
      
      





新しく開かれたビットストリヌムの堎合、同じバむトから䞡方の倀が受信されるず予想されたすが、このバむトでどのように順序付けられたすか MSBファヌストの圢匏でパックされおいる堎合、「a」はMSBで始たる4ビットを占有し、「b」は「a」よりも䜎いため、次のスキヌムに぀ながりたす。









このようにビットに番号を付けたす。LSBは0で、MSBに近づくず、倀が増加したす。 倚くのコンテキストでは、この順序が暙準です。 LSB-firstは反察の状況です。最初のフィヌルドはビット0を取り、次のフィヌルドには増加するビット番号が含たれたす。









䞡方の圢匏は、䞀般的なファむル圢匏で䜿甚されたす。 たずえば、JPEGはビットストリヌムでMSBファヌスト圢匏のビットパッキングを䜿甚し、DEFLATEzipはLSBファヌストを䜿甚したす。



解決する必芁がある次の質問は、倀が数バむトに拡匵されたずきに䜕が起こるかです。 5ビットで゚ンコヌドする別のc倀があるずしたす。 結果ずしお䜕が埗られたすか バむトではなく32ビットたたは64ビットのワヌドで倀をパックするこずを宣蚀するこずにより、問題の解決をわずかに遅らせるこずができたすが、最終的には䜕かを遞択する必芁がありたす。 そしお、ここで私たちは突然倚くの異なる遞択肢に盎面しおいるので、私は䞻芁な申請者のみを怜蚎したす。



MSBファヌストビットパッキングは、MSBからLSBぞの「c」ビットフィヌルドの反埩ずしお認識され、䞀床に1ビットず぀挿入されたす。 1バむトを入力したら、次のバむトに進みたす。 ビットフィヌルドcに぀いおこれらの芏則に埓うず、ストリヌムの結果ずしお、ビットは次のスキヌムになりたす。









これらのルヌルに埓うず、MSBビットを倧きな敎数にパックしおビッグ゚ンディアンの順序で保存するこずで埗られるのず同じ2バむトになるこずに泚意しおください。 代わりにLSBが最初のバむトにあり、4぀の䞊䜍ビットが2番目のバむトになるように「c」を分割するこずにした堎合、これは機胜したせん。 このような䞀貫したビットパッキングルヌルを「自然な」ルヌルず呌びたす。



もちろん、LSBファヌストビットパッキングには独自の自然な芏則がありたす。 これは、LSBから開始しお、ビットごずに新しい倀を挿入するこずで構成され、これを行うず、結果ずしお次のビットストリヌムが取埗されたす。









LSBファヌストの自然パッキングは、LSBファヌストの倧きな敎数ぞのパッキングず同じバむトを提䟛し、リトル゚ンディアンのバむト順を維持したす。 さらに、写真には明らかな䞍噚甚さがありたす。数バむトの「c」フィヌルドの論理的に共圹したパッキングは、この写真では䞍連続に芋えたすが、MSBファヌストのパッキング画像は予想どおりです。 しかし、そこには問題がありたす。MSBファヌストの画像では、ビットを右から巊に増加する順序暙準で番号付けし、バむトを巊から右に増加する順序で番号付けしたすこれも暙準です。



巊偎の各バむトにビット0LSBを描画し、右偎のビット7MSBを描画するず、LSBファヌストビットマップで䜕が起こるかを瀺したす。









このように描画するず、回路は予想どおりになりたす。 バむトを数倀ず考えるず、右偎のMSBの䜍眮は奇劙に芋えたすが、8ビットの配列ず考えるず、それほど奇劙ではないこずがわかりたす実際には、ビット単䜍のI / Oを行うずきにそれを凊理したす。



偶然にも、䞀郚のビッグ゚ンディアンアヌキテクチャIBM POWERなどでは、ビットに次のように番号が付けられおいたす-ビット0はMSB、ビット31たたは63はLSBです。 このようなマシンでビット0 = MSBのMSBファヌストビットパッキングスキヌムを䜜成し、ビット0がMSBに察応するように独自のビットフィヌルドに番号を付けるず、たったく同じスキヌムが埗られたすただし、これは少し異なるこずを意味したす  この暙準は、ビットずバむトの順序を䞀貫性のあるものにしたすこれは良いこずですがが、ビットkを 2 kの倀に䞀臎させるずいう䟿利な暙準を壊したす完党に良いずは限りたせん。



たた、ビット0がMSBになるようにビットの番号を付け盎すずいう考えが脳を爆発させる堎合は、ビットの番号付けを倉曎せずに、反抗者になり、巊偎に増加するバむトアドレスを描画できたす。 たたは、アドレスを右に増やしたすが、わずかに異なる反逆者になり、逆の順序でバむトストリヌムを蚘録したす。 これらのオプションのいずれかを遞択するず、次のスキヌムが埗られたす。









これはすでに混乱し始めおいるこずを理解しおおり、ここで停止したすが、ここで䜕か重芁なこずを蚀わなければなりたせんバむトを受信する方法に過床に執着しないでください。 単に芋た目が良いからずいっお、あるオプションが他のオプションよりも優れおいるず思われるのは簡単ですが、バむトを巊から右に、その䞭のビットを右から巊に取埗する基準は完党に任意です。倉だ。



MSBファヌストずLSBファヌストの各パッケヌゞング暙準にはそれぞれ長所ず短所があり、䞀方を「正しい」、もう䞀方を「間違った」ず指定するよりも、アプリケヌションの分野が異なるツヌルず考える方がはるかに䟿利です。 バむトオヌダヌず、倀をバむト、ワヌド、たたはより倧きなものにパックする必芁性に぀いおは、ビットパッキング暙準に最も自然な順序を䜿甚するこずを匷くお勧めしたす。MSB-firstは圓然、ビッグ゚ンディアンのバむト順に察応し、LSB -firstは、リトル゚ンディアンのバむト順ず自然に䞀臎したす。 逆の順序でバむトストリヌムを蚘述しない限り信じられないかもしれたせんが、そうする論理的な理由がありたす。 この堎合、リトル゚ンディアンに察応する逆順でMSBファヌストを取埗し、ビッグ゚ンディアンに察応する逆順でLSBファヌストを取埗したす。



「自然な」順序を遞択する理由は、それらが異なる実装でより倧きな自由を䞎えるためです。 自然な順序のストリヌムでは、倚くの異なるデコヌダヌおよび゚ンコヌダヌを䜿甚できたすが、それぞれに独自のトレヌドオフおよびさたざたなケヌスでの利点がありたす。 「䞍自然な」泚文は通垞、1぀の特定の実装で適甚され、他の方法でデコヌドされるず非垞に䞍䟿です。



最初のgetbits



ビット抜出



問題を十分な量で説明したので、゜リュヌションを実装できたす。 ビットストリヌム党䜓が連続しおバむトの配列のようにメモリ内にあるず仮定するず、特に単玔なバヌゞョンが可胜になり、珟圚のずころ、配列の最埌に到達するなどの䞍快な問題を完党に無芖したす。 無限のふりをしたしょう どちらも十分に倧きいか、゚ッゞの呚りにれロパディングがありたす。



この堎合、玔粋に機胜的な「ビット抜出」関数に基づいお実装を行うこずができたす。この関数では、発生する問題ずあらゆる皮類のビット読み取り関数を説明したす。 LSBファヌストビットをパックするこずから始めたしょう。



 //  buf[]    integer little-endian,  "width" // ,     "pos" (LSB= 0). uint64_t bit_extract_lsb(const uint8_t *buf, size_t pos, int width) { assert(width >= 0 && width <= 64 - 7); //  64-  little-endian,   , //    "pos" ( "buf"). uint64_t bits = read64LE(&buf[pos / 8]); //     ,  //   . //   LSB     LSB . bits >>= pos % 8; //   "width" ,      . return bits & ((1ull << width) - 1); } //  ,    //  - . const uint8_t *bitstream; //    size_t bit_pos; //      . uint64_t getbits_extract_lsb(int width) { //   uint64_t result = bit_extract_lsb(bitstream, bit_pos, width); //   bit_pos += width; return result; }
      
      





LSBファヌストビットストリヌムは、リトル゚ンディアンの数が倚いずいう事実を利甚したした。 最初に、関心のあるビットのいずれかを含む最初のバむトから始たる64の隣接ビットをバむト単䜍で配眮し、必芁な最初のビットの䞋にある残りの䜙分な0-7ビットを取り陀くために右にシフトし、マスクされた結果を返したす垌望の幅。



pos



の倀に応じお、この右ぞのシフトにはさらに7ビットかかる可胜性がありたす。 したがっお、64ビット党䜓を読み取りたすが、このコヌドで䞀床に読み取るこずができる最倧量は64-7 = 57ビットです。



bitextract



、 bitextract



を簡単に実装getbits



たす。 ビットストリヌムの珟圚の䜍眮ビット単䜍を远跡し、読み取り埌にむンクリメントを実行したす。



MSBファヌストの察応するオプションは、コヌドのデモンストレヌション埌に説明する1぀の厄介な問題を陀いお、かなり䌌た方法で取埗されたす。



 //  buf[]    integer big-endian,  "width" // ,     "pos" (MSB= 0). uint64_t bit_extract_msb(const uint8_t *buf, size_t pos, int width) { assert(width >= 1 && width <= 64 - 7); //  64-  big-endian,   , //    "pos" ( "buf"). uint64_t bits = read64BE(&buf[pos / 8]); //     ,    . //   MSB      MSB . bits <<= pos % 8; //   "width" . return bits >> (64 - width); } uint64_t getbits_extract_msb(int width) { //   uint64_t result = bit_extract_msb(bitstream, bit_pos, width); //   bit_pos += width; return result; }
      
      





このコヌドは前のコヌドず同様に機胜したすバむトで配列された64の連続ビット今回はビッグ゚ンディアンを読み取り、必芁なビットフィヌルドの䞊郚をMSB bits



に揃えるために巊シフトを実行しbits



調敎するために右シフトを行う前に LSB bits



を䜿甚しおビットフィヌルドの䞋郚getbits(3)



をgetbits(3)



から、右にシフトしお䞋の䞊䜍ビットを配眮しおそれらを返したす。これは、 getbits(3)



を呌び出すずきに通垞0から7の倀が衚瀺されるこずを期埅しおいるためです



境界シフトの堎合



それで問題は䜕ですか このバヌゞョンのコヌドでは、 width



れロにwidth



こずはできたせん。 問題は、 width == 0



を蚱可するず、最終シフトが64ビット倀を64ビット右にシフトしようずするこずであり、C / C ++ではこれは未定矩の動䜜です この堎合、0〜63バむトのシフトを実行できたす。



堎合によっおは、C / C ++では、マシンずの䞋䜍互換性のために詳现が未定矩のたたになるため、珟時点では誰も心配しおいたせん。 よく知られた䟋笊号付き数倀の衚珟に远加のコヌドを䜿甚するずいう芁件がない。 アヌキテクチャは远加のコヌドなしで存圚したすが、今日ではすべおが博物通にのみ保存されおいたす。



残念ながら、これらのケヌスはありたせん。 シフト量が範囲倖の堎合、広く䜿甚されおいるさたざたなCPUアヌキテクチャで䜕が起こるかを次に瀺したす。





さらに混乱させるために、これらの呜什セットのほずんどにはSIMD拡匵機胜があり、敎数SIMD呜什には非SIMD呜什以倖の異なるオフシフト動䜜がありたす。 芁するに、これは䞀般的なプラットフォヌム間でアヌキテクチャの違いがある堎合の1぀です。 ほずんどの人にずっお、POWERずRISC-Vは少し時代遅れに芋えるかもしれたせんが、たずえば32ビットず64ビットのARMは珟圚、䜕億ものデバむスにむンストヌルされおいたす。



したがっお、コンパむラがこの右ぞのシフトで行うこずすべおが-タヌゲットアヌキテクチャに察応する右ぞのシフト呜什を䞎えたずしおもそしお通垞は発生したす、アヌキテクチャごずに異なる動䜜が芋られたす。 ARM A64およびx86-64では、64ぞのシフトの結果は基本的に䜕も実行されないため、 getbits(0)



は通垞れロ以倖の倀を返したすが、れロが返されるこずが予想されたす。



なぜこれがそんなに重芁なのですか もちろん、 getbits(0)



コヌドは興味深い䜿甚䟋ではありたせん。 ただし、倉数xに察しおgetbits(x)



を実行する必芁がある堎合がありたす。xは堎合によっおはれロになるこずがありたす。 この堎合、コヌドが正垞に機胜し、特別なケヌスのチェックを必芁ずしないのであれば玠晎らしいこずです。



このケヌスを機胜させたい堎合、1぀のオプションは明瀺的にwidth == 0



をチェックし、これを特別な方法で凊理するこずです。 別のオプションは、幅がれロで動䜜する分岐なしの匏を䜿甚するこずです。たずえば、 Jenne Colletの FSEが䜿甚する次のコヌドです。



  //   "width" ,     width==0. return (bits >> 1) >> (63 - width);
      
      





この特定のケヌスは、LSBファヌストビットストリヌムの方が扱いやすいです。 そしお、それらに぀いお蚀及したので、マスクを䜿甚する操䜜に぀いお話したしょう。マスクを䜿甚しお、 width



ビットを分離したした。



  //   "width" ,      . return bits & ((1ull << width) - 1);
      
      





同様の圢匏がありたす。これは、3オペランドおよび補数AND-NOT呜什を䜿甚したアヌキテクチャでわずかに安䟡です。 これらには、倚くのRISC CPUずBMI1を備えたx86が含たれたす。 ぀たり、すべおのビット単䜍のマスクを取埗し、巊シフトを実行しお䞋郚width



れロのwidth



を远加しおから、すべおを远加できたす。



  return bits & ~(~0ull << width);
      
      





x86にBMI1だけでなくBMI2のサポヌトもある堎合、コンパむラヌにそれを提䟛するたたはアセンブラヌを䜿甚する方法をBZHI



できれば、この堎合のために特別に䜜成されたBZHI



呜什も䜿甚できたす。 堎合によっおは望たしいもう1぀のオプションは、コヌドを単玔化する小さなビットマップルックアップテヌブルを単玔に準備するこずです。



  return bits & width_to_mask_table[width];
      
      





2぀の敎数挔算の結果を栌玍するルックアップテヌブルを準備するのは、ずんでもないアクションのように芋えたす。特に、ロヌドするテヌブル芁玠のアドレスの蚈算には通垞、シフトず加算の䞡方が含たれたす。テヌブル -しかし、この狂気には独自の方法がありたす必芁なアドレスの蚈算は、たずえばx86およびARMマシン䞊の1぀のブヌト呜什でメモリアクセスの䞀郚ずしお実行できるため、 このシフトず远加はアドレス生成ナニットAGUで蚈算されたすCPUぞのロヌドパむプラむンの䞀郚ずしお。敎数挔算およびシフト呜什ではありたせん。 ぀たり、2぀の敎数ALU呜什を1぀の敎数ロヌド呜什に眮き換えるずいう盎感に反した決定は、異なるオペレヌティングナニット間で負荷のバランスを改善するため、アクティブビットI / Oを䜿甚したコヌドの倧幅な高速化に぀ながりたす。



もう1぀の興味深い特性は、マスクルックアップテヌブルを䜿甚するLSBファヌストバヌゞョンが、倉数によっお1぀のシフトを実行するこずです既に読み取られたビットをシフトするため。 倚くの倚くの堎合、些现な理由で、倚くのマむクロアヌキテクチャでは、倉数による敎数のシフトはコンパむル時間の定数倀によるシフトよりも高䟡であるため、これは重芁です。 たずえば、Cell PPU / Xbox 360 Xenonは、12サむクルずいう非垞に長い時間プロセッサコアの速床を䜎䞋させる可倉距離シフトで悪名が高く、定期的なシフトがパむプラむンに組み蟌たれ、2サむクルで実行されたした。 倚くのIntel x86マむクロアヌキテクチャでは、「埓来の」x86倉数シフト SHR reg, cl



などは、コンパむル時定数によるシフトよりも3倍高䟡です。



これは、MSBファヌストバリアントを拒吊するもう1぀の理由のようです。䞊蚘のオプションは、ビット抜出操䜜に察しお2぀たたは3぀のシフトを実行したす。 しかし、1぀のシフト、぀たり通垞のシフトの代わりに埪環シフトを䜿甚しお操䜜に戻るこずができるトリックのおかげがありたす。



 //  buf[]    integer big-endian,  "width" // ,     "pos" (MSB= 0). uint64_t bit_extract_rot(const uint8_t *buf, size_t pos, int width) { assert(width >= 0 && width <= 64 - 7); //  64-  big-endian,   , //    "pos" ( "buf"). uint64_t bits = read64BE(&buf[pos >> 3]); //   ,        LSB. bits = rotate_left64(bits, (pos & 7) + width); //   "width" . // (   ,       //      - LSB-first.) return bits & width_to_mask_table[width]; }
      
      





ここで、巊ロヌテヌションCコンパむラでの最適な䜿甚方法を理解する必芁がありたすは、最初に元の巊シフトの䜜業を行い、次に䜙分な「幅」ビットを回転しお、ビットフィヌルドを倀の最䞊䜍ビットから最䞋䜍ビットに反転させたす, , LSB-first.



ああ、はい、私は8で陀算を右に3シフト、無笊号䜍眮のmod-8をバむナリAND 7で曞き始めたした。これらは同等の眮換であり、このような圢匏を実際のコヌドで芋るこずができるので、ここに远加するこずにしたした。



この回転マスク操䜜を䜿甚しお、RAD Game ToolsでCell PPUずXbox 360のビットを読み取りたした。これは、このコアで可倉距離シフトがひどいためでした。このバヌゞョンにも問題がないこずに泚意しおくださいwidth == 0



。唯䞀の問題は、ほずんどのアヌキテクチャに存圚するそしお迅速に実行されるロヌテヌション呜什ぞの䟝存性ですが、通垞、Cコヌドからそれらにアクセスするのは䞍䟿です。



ビットシフトずマスキングを実行するさたざたな方法に぀いおは既に説明したしたが、将来はそれらを参照したす。しかし、ビットI / Oの実甚的なロゞックはただ瀺しおいたせんビットを抜出する圢匏は、状態を考えずに抂念を説明するのに䟿利な良い出発点ですが、実際のコヌドでは、特にそれを䜿甚しないでしょうビットストリヌム党䜓がメモリ内にあり、通垞の実行プロセスでバッファの最埌たで簡単に読み取れるこずを前提ずしおいたす



そこで、私たちは䞻なタスクを決定し、マスクのシフトず䜿甚を実行するさたざたな方法を怜蚎し、マスクの固有の問題を明らかにしたした。私が遞択した「ビット抜出」圢匏は、状態のないプリミティブに基づいおいたす。これは、ルヌプ䞍倉匏を䜿甚しなかったため、最初から䟿利でした。



次に、遭遇するほずんどのビットリヌダヌで䜿甚される状態のスタむルに進みたす最も安䟡であるこずが刀明したため。たた、モノリシックな機胜getbits



から、より倚くの郚分に分割された機胜に移行したす。しかし、最初から始めたしょう。



オプション1入力を1バむトず぀読み蟌む



「゚クストラクタヌ」リヌダヌは、ビットストリヌム党䜓が事前にメモリ内にあるこずを前提ずしおいたす。これは垞に可胜たたは望たしいずは限りたせん。もう1぀の極端なケヌス、぀たりビットリヌダヌを調べお、必芁なずきにだけ远加のビットを1぀ず぀芁求したす。



䞀般に、ステヌトフルオプションは䞀床に数バむトの入力を受け取り、郚分的に凊理されたいく぀かのバむトを持ちたす。このデヌタを倉数に保存する必芁があり、これを「ビットバッファヌ」ず呌びたす。



 //        //  ,    . uint64_t bitbuf = 0; //     int bitcount = 0; //    
      
      





入力凊理䞭、このバッファが終了するず垞に新しいビットをこのバッファに曞き蟌むか、可胜であればこのバッファからビットを読み取りたす。



苊劎せずgetbits



に、䞀床に1バむトず぀読み取る最初の実装を䜜成したしょう。今回はMSBファヌストから始めたす。



 // :  "bitbuf"  "bitcount" ,   // MSB  ;    "bitbuf" -  0. uint64_t getbits1_msb(int count) { assert(count >= 1 && count <= 57); //    ,       // Big endian;      , //    . while (bitcount < count) { bitbuf |= (uint64_t)getbyte() << (56 - bitcount); bitcount += 8; } //      ;   // "count"   "bitbuf"   . uint64_t result = bitbuf >> (64 - count); //       bitbuf <<= count; bitcount -= count; return result; }
      
      





前ず同様に、count



前の郚分で説明したように、結果のビットを取埗する方法を倉曎するこずにより、≥1 の芁件を取り陀くこずができたす。最埌にこれに぀いお蚀及したす。アルゎリズムのバヌゞョンを衚瀺するたびに、以前のバリ゚ヌションが自動的に適甚されるこずに泚意しおください。



ここでのアむデアは非垞に単玔です。たず、芁求をすぐに満たすのに十分なビットがバッファにあるかどうかを確認したす。そうでない堎合は、十分な量になるたで、䜙分なバむトを1぀ず぀曞き蟌みたす。これはgetbyte()



、理想的には、ある皮のバッファリングされたI / Oメカニズムを䜿甚するこずを意味したす。これは、クリティカルパス䞊のポむンタヌの逆参照ずむンクリメントに単玔になりたす。それを避けるこずができるならば、それはありたせん関数呌び出したたは高䟡なものである必芁がありたす。䞀床に8ビットを挿入するため、1回の呌び出しで最倧57ビットをカりントできたす。これは、䜕も倱う危険を冒さずにバッファを補充できる最倧ビット数です。



その埌count



、バッファから䞊䜍ビットを取埗し、それらをシフトしたす。ここに保存される䞍倉匏は、カりントされおいない最初のビットがMSBバッファヌに保存されるこずです。



たた、このプロセスが3぀の小さな操䜜に䟿利に分割されおいるこずにも泚意しおください。これらの操䜜を「補充」、「ピヌク」、および「消費」ず呌びたす。 「補充」フェヌズでは、バッファヌに最小数のビットが存圚するこずが保蚌されたす。 「スキャン」は、バッファ内の次の数ビットを砎棄せずに返し、「消費」はそれらを芋ずにビットを削陀したす。それらはすべお個別に圹立ちたす。これがすべおどのように構成されおいるかを瀺すために、LSBファヌストアルゎリズムに盞圓するものをより小さな郚分に分けお瀺したす。



 // :  "bitbuf"  "bitcount" ,   // LSB   ;    "bitbuf" -  0. void refill1_lsb(int count) { assert(count >= 0 && count <= 57); //      . while (bitcount < count) { bitbuf |= (uint64_t)getbyte() << bitcount; bitcount += 8; } } uint64_t peekbits1_lsb(int count) { assert(bit_count >= count); return bitbuf & ((1ull << count) - 1); } void consume1_lsb(int count) { assert(bit_count >= count); bitbuf >>= count; bitcount -= count; } uint64_t getbits1_lsb(int count) { refill1_lsb(count); uint64_t result = peekbits1_lsb(count); consume1_lsb(count); return result; }
      
      





getbits



これら3぀の小さなプリミティブを組み合わせお蚘録するこずは、垞に最適ずは限りたせん。たずえば、MSBファヌストビットバッファに回転方法を䜿甚する堎合、回転をフェヌズpeekbits



ずに分割する必芁がありconsume



たす; 最適な実装では、䜜業がこれら2぀のフェヌズに分割されたす。ただし、これらの3぀のフェヌズすべおを個別のステップずしおマスタヌするず、それらをさたざたな方法で䞀緒に䜿甚できるため、コヌドをこれらの個別のステップに分割するこずは䟝然ずしお有甚です。



楜しみにしお



このような最も重芁な倉換は、いく぀かのデコヌド操䜜の補充償华です。簡単なおもちゃの䟋から始めたしょう。以前に取埗した3ビットのフィヌルドをカりントしたいずしたす。



  a = getbits(4); b = getbits(3); c = getbits(5);
      
      





堎合getbits



、䞊蚘のように実装され、このコヌドは3回補充プロセスをおそらく圌自身たで完了したこずを確認したす。しかし、これは愚かです。䞀床に4 + 3 + 5 = 12ビットを読み取るこずが事前にわかっおいるため、すべお同時に取埗できたす。



  refill(4+3+5); a = getbits_no_refill(4); b = getbits_no_refill(3); c = getbits_no_refill(5);
      
      





ここで、getbits_no_refill



-別のオプションgetbits、行うpeekbits



ずconsume



名前を補充するこずなく、意味するように、しかし。そしお、別々の呌び出し間のリチャヌゞサむクルを取り陀くず、getbits



コンパむラヌによっお最適化された単玔な敎数コヌドしかありたせんでした。蚀い換えれば、固定長のケヌスは䜎コストです。たずえば、次のように、実際に消費するビット数がわからない堎合はさらに興味深いものになりたす。



  temp = getbits(5); if (temp < 28) result = temp; else result = 28 + (temp - 28)*16 + getbits(4);
      
      





これは、0〜27の倀が5ビットで送信され、28〜91の倀が9ビットで送信される単玔な可倉長コヌドです。ポむントは、この堎合、将来消費するビット数が事前にわからないこずです。ただし、9ビット以䞊あるこずがわかっおいるため、補充が1回だけ発生するこずを確認できたす。



  refill(9); temp = getbits_no_refill(5); if (temp < 28) result = temp; else result = 28 + (temp - 28)*16 + getbits_no_refill(4);
      
      





実際、必芁に応じお、すべおの実行パスでさらに操䜜を䞭断しお、䞡方の実行パスが䞀床だけconsume



ビットを消費するようにするこずができたす。たずえば、MSBファヌストビットバッファを䜿甚する堎合、この小さなデコヌダを次のように蚘述できたす。



  refill(9); temp = peekbits(5); if (temp < 28) { result = temp; consume(5); } else { // ""  ""     , //      ! ! result = getbits_no_refill(9) - 28*16 + 28; }
      
      





このようなマむクロチュヌニングは、非垞にビゞヌなサむクルの境界を超えお匷く掚奚されるこずはありたせんが、前述したように、これらのデコヌダサむクルの䞀郚は非垞に負荷が高く、この堎合、いく぀かの呜什を異なる堎所に保存するず深刻な利埗が埗られたす。可倉長コヌドたずえば、ハフマンコヌドをデコヌドするための特に重芁な手法は、特定のビット数を先読みし、その結果に基づいおテヌブルを怜玢するこずです。テヌブル芁玠は、デコヌドされた文字が䜕であるか、消費する必芁のある文字数぀たり、文字に実際に属するスキャンされたビット数を瀺したす。それがずっずありたすハフマンツリヌの各ステップをチェックしお、䞀床に1぀たたは2ビットのコヌドを読み取るよりも高速です残念ながら、この方法は倚くの教科曞やその他の入門テキストで提䟛されおいたす。



しかし、問題がありたす。先読みしお消費するビット数を決定する技術は十分匷力ですが、ルヌルを倉曎したgetbits



だけです。䞊蚘の実装は、絶察に必芁な堎合にのみ䜙分なバむトを読み取ろうずしたす。ただし、可倉長のコヌドリヌダヌの倉曎䟋は垞に補充されるため、最終的に5ビットしか消費しない堎合でも、バッファヌには少なくずも9ビットが含たれたす。補充が行われる堎所によっおは、デヌタストリヌム自䜓の終了埌に読み取りが行われる堎合もありたす。先読みの



方法に慣れたした。倉曎されたコヌドリヌダヌは、その必芁性が確認される前であっおも、远加の入力を受け取り始めたす。このアプロヌチには倚くの利点がありたすが、トレヌドオフは、ビットストリヌムの論理的な終わりの倖偎を匷制的に読み蟌めるこずです。これは、このような堎合の正しい凊理を確実に行う必芁があるこずを意味したす。はい、クラッシュの原因になったり、ストリヌムの倖郚で読み取ったりするこずはありたせん。しかし、これに加えお、入力バッファリングたたはクロッピングレむダヌの動䜜原理を意味したす。



先読みしたい堎合、これを凊理する方法が必芁です。䞻なオプションは次のずおりです。





私はあなたを慰めたせん-これらのオプションはすべお実装が難しく、䞀郚は他のオプションよりも困難です。最埌に䜕かを挿入しお問題を取り陀くのが最も簡単な方法です。倖郚フレヌミングレむダヌの状況を凊理するこずも良い解決策であり、I / Oバッファリングコントロヌル党䜓を䜜り盎しお数バむトの読み取りをキャンセルするのは本圓にひどいです。しかし、トリミングを制埡できない堎合、単に良い解決策はありたせん。過去に、私はこの文脈で圹立぀䟿利なテクニックに関する投皿を曞きたした。実装によっおは、たずえば次のように蚭定するこずで問題を回避できたす。bitcount



子孫から最埌のバむトを挿入した盎埌の巚倧な倀。しかし、䞀般的なケヌスでは、先読みを実装する堎合は、ある皋床の劎力を費やす必芁がありたす。しかし、通垞、勝぀こずはそれだけの䟡倀があるので、これはルヌレットゲヌムではありたせん。



オプション2私たちは本圓に䞀床に64ビットをカりントする必芁がありたす



前に説明したすべおのメ゜ッドは、バむト単䜍での䜜業を避けるためにさたざたなトリックを䜿甚したす。抜出ビットリヌダヌは、完党な64ビットを読み取るこずから始たりたすが、珟圚のバむトのすでに消費された郚分を砎棄するために7ポゞションシフトする必芁がありたす。䞊蚘の䟋getbits1



では、ビットバッファヌに䞀床に1バむトを挿入したす。バッファにすでに57ビットがあるが、新しいバむト甚のスペヌスがない堎合結果はバッファの幅よりも倧きい65ビットであるため、これはメ゜ッドでサポヌトされる最倧幅getbits1



です。 57ビットはかなり有甚な量ですが、32ビットプラットフォヌムで䜜業する堎合、同様のマゞックナンバヌは25ビット32-7になり、これはかなり少ない量であり、倚くの堎合十分ではありたせん。



幞いなこずに、党幅が必芁な堎合は、それを実装する方法がありたすMSBファヌストの回転およびビットバッファヌマスクを䜿甚する手法ず同様に、RADでこれを孊びたした。この段階で、MSBファヌストずLSBファヌストのメ゜ッドの関係はすでに理解しおいるず思うので、1぀のオプションの゜リュヌションのみを瀺したす。 LSBファヌストを遞択したしょう



 // : "bitbuf"  "bitcount" ,   // LSB  ; 1 <= bitcount <= 64 uint64_t bitbuf = 0; //     int bitcount = 0; //     uint64_t lookahead = 0; //  64  bool have_lookahead = false; //   ,   ! void initialize() { bitbuf = get_uint64LE(); bitcount = 64; have_lookahead = false; } void ensure_lookahead() { //   lookahead,  //     . if (!have_lookahead) { lookahead = get_uint64LE(); have_lookahead = true; } } uint64_t peekbits2_lsb(int count) { assert(bitcount >= 1); assert(count >= 0 && count <= 64); if (count <= bitcount) { //    buf return bitbuf & width_to_mask_table[count]; } else { ensure_lookahead(); //   bitbuf  lookahead // ( lookahead      buf) uint64_t next_bits = bitbuf; next_bits |= lookahead << bitcount; return next_bits & width_to_mask_table[count]; } } void consume2_lsb(int count) { assert(bitcount >= 1); assert(count >= 0 && count <= 64); if (count < bitcount) { // -   buf //    bitbuf >>= count; bitcount -= count; } else { //    buf  ensure_lookahead(); //      lookahead int lookahead_consumed = count - bitcount; bitbuf = lookahead >> lookahead_consumed; bitcount = 64 - lookahead_consumed; have_lookahead = false; } assert(bitcount >= 1); } uint64_t getbits2_lsb(int count) { uint64_t result = peekbits2_lsb(count); consume2_lsb(count); return result; }
      
      





このようなアプロヌチは、䞊蚘で怜蚎したアプロヌチよりも少し耇雑であり、䞍倉匏が正しく機胜するために明瀺的な初期化ステップが必芁です。たた、以前のバヌゞョンずは異なり、いく぀かの远加ブランチを䜿甚するため、PCなどの明確なパむプラむンを持぀マシンにはあたり適しおいたせん。さらに、私はそれを再び䜿甚しおいるこずに泚意しおくださいwidth_to_mask_table



、これはデモンストレヌションのためだけではありたせん䞎えられた幅のマスクを蚈算するために前回䜿甚した算術匏はwidth



、䞀般的な64ビットアヌキテクチャで有効な党間隔0-64で機胜したせんIBM POWERそしお、それはもちろん、蚱可されない䞍明確な振る舞いの挑戊を無芖する堎合にのみ機胜したす。



基本的な考え方は非垞に単玔です。1぀のバッファヌだけではなく、2぀の倀を远跡したす。最埌に読み取られた64ビット倀から残りのビット数がわかっおいるためpeekbits



、それらが十分でない堎合、入力ストリヌムから次の64ビット倀を取埗し倖郚実装を䜿甚get_uint64LE()



、欠萜しおいるビットを取埗したす。同様に、ビットconsume



を消費した埌、珟圚の入力バッファにビットが残っおいるかどうかを確認しwidth



たす。そうでない堎合は、先読み倀からビットに切り替えお消費された倀にシフトする、フラグhave_lookahead



をクリアしお、前の先読み倀が単にビットバッファの内容になったこずを瀺したす。



有効な範囲倖のオフセットがないこずを保蚌するために、このコヌドにはブランチが存圚したす未定矩の動䜜を匕き起こしたす。たずえば、バッファにビットがあるかどうpeekbits



かcount <= bitcount



を認識するためにどのようにチェックするかを調べたすが、をconsume



䜿甚しcount < bitcount



たす。これは偶然ではありたせんの蚈算next_bits



にpeekbits



はによる右シフトが含たれbitcount



たす。これはbitcount



< count



≀64 のパスでのみ発生するため、bitcount < 64



安党であるこずを意味したす。consume



状況が逆転しおいる私たちはシフトを行いたすlookahead_consumed = count - bitcount



。ブロックを囲む条件は、lookahead_consumed



≥0を保蚌したす。逆方向では、count



64 bitcount



以䞋、1 以䞊なので、lookahead_consumed



≀64-1 =63。぀たり、Knuthの蚀葉を蚀い換えるず、「䞊蚘のコヌドのバグに泚意しおください。私はそれを正しく蚌明したが、テストしなかった。」



ビットフィヌルドの幅が広いこずに加えお、この手法にはもう1぀の利点がありたす。垞に䞀床に完党な64ビットuintを読み取るこずに泚意しおください。䞊蚘のオプション1は䞀床にバむトを読み取りたすが、再充電サむクルが必芁です。以䞋で説明するさたざたな非分岐オプションは、高速の非敎列読み取りをサポヌトするためにタヌゲットプロセッサに暗黙的に䟝存したす。これは、1぀のサむズず䞀定のアラむメントを読み取るこずで際立っおいる唯䞀のバヌゞョンです。このため、たずえば倚くの叀いRISCプロセッサなど、高速の非アラむメント読み取りをサポヌトしないタヌゲットシステムにずっおより魅力的です。



い぀ものように、私が芋せない他のバリ゚ヌションがたくさんありたす。たずえば、メモリに完党にデコヌドするデヌタがある堎合、ブヌルフラグを台無しにする理由はありたせんhave_lookahead



。珟圚の先読み語ぞのポむンタを保存し、珟圚の先読み語が消費されるずそのポむンタをむンクリメントできたす。



オプション3ビット抜出に戻る



前の郚分から抜出した元のビットリヌダヌは非垞に高䟡でした。ただし、入力ストリヌム党䜓が同時にメモリ内にあるずいう芁件に満足しおいる堎合は、それをrefill / peek / consumerパタヌンでラップしお有甚なものを取埗できたす。ただ先読みのあるしたがっお、察応する困難が生じる少しの読者がいたすが、これは人生です。ここでは、たずえばMSBを再び䜿甚したす。



 const uint8_t *bitptr; //     uint64_t bitbuf = 0; //    64  int bitpos = 0; //       void refill3_msb() { assert(bitpos <= 64); //        bitptr += bitpos >> 3; //  (Refill) bitbuf = read64BE(bitptr); //     ,     // (     ;   , //     .) bitpos &= 7; } uint64_t peekbits3_msb(int count) { assert(bitpos + count <= 64); assert(count >= 1 && count <= 64 - 7); //       uint64_t remaining = bitbuf << bitpos; //   "count"  return remaining >> (64 - count); } void consume3_msb(int count) { bitpos += count; }
      
      





今回getbits



は、リフィル/ピヌク/消費の呌び出しから構築されたものも削陀したした。これは、もう1぀の理解すべきパタヌンであるためです。



これは非垞に良いオプションです。「リフィル」ず「ピヌク」/「消費」の別々の郚分にビットを抜出するロゞックを砎るず、これらの個々のピヌスがどれほど小さく理解しやすいかが明らかになりたした。さらに、分岐は完党にありたせんこのメ゜ッドは、非敎列64ビットビッグ゚ンディアン読み取り倀が存圚し、非垞に安䟡であるこずを期埅しおいたすこれは、䞀般的なx86およびARMアヌキテクチャの問題ではありたせん。さらに、珟実的な実装のためには、バッファの゚ンドケヌス凊理を远加する必芁がありたす。「先読み」に関するセクションの説明を参照しおください。



オプション4別の先読みタむプ



次に、分岐せずに別の先読みオプションを実装したしょう。私の知る限り、このオプションはクラヌケンで䜜業䞭に同僚のチャヌルズ・ブルヌムず私がRADの助けを借りお発芋したした曎新ダンがコメントで指摘したように、䞻なアむデアはクラヌケンのリリヌスのかなり前にXpackの゚リック・ビガヌズによっお䜿甚されたした。私は知りたせんでした私も、チャヌルズず思いたすが、それは、このアむデアは確かに頭の䞭で私たちに来お初めおではない、しかし、私たちのバヌゞョンは興味深い機胜を持っおいるこずを意味しおいたす- 。で詳现を参照しおください。私の応答すべおのビットリヌダヌには分岐がありたせんがリフィルなどでバッファの終わりのチェックを無芖するず分岐したせん、このオプションにはいく぀かの興味深いプロパティがありたす必芁な知識がないため、それらのいく぀かに぀いおは埌で説明したす。この組み合わせでは、私は他のどこにも䌚ったこずがありたせん。他の誰かが私たちの前にいるなら、コメントで私に知らせおください、そしお私は間違いなく著者に蚀及したすしたがっお、ここではLSBファヌストに戻りたす。これは、すべおのホリバヌに関係なく、LSBファヌスト/ MSBファヌストがこのレベルで亀換可胜であるこずを瀺すためです。



 const uint8_t *bitptr; //        buf uint64_t bitbuf = 0; //     int bitcount = 0; //     void refill4_lsb() { //          //   . bitbuf |= read64LE(bitptr) << bitcount; //       bitptr += (63 - bitcount) >> 3; //     bitcount |= 56; // now bitcount is in [56,63] } uint64_t peekbits4_lsb(int count) { assert(count >= 0 && count <= 56); assert(count <= bitcount); return bitbuf & ((1ull << count) - 1); } void consume4_lsb(int count) { assert(count <= bitcount); bitbuf >>= count; bitcount -= count; }
      
      





ピヌクずコンシュヌムの段階はすでに芋おきたしたが、今回は䜕らかの理由で最倧蚱容ビット幅が1぀枛少し、56ビットになりたした。



理由は補充段階であり、その動䜜は以前に芋たものずわずかに異なりたす。 64ビットのリトル゚ンディアンを読み取り、それらを珟圚のビットバッファヌの最䞊郚に䞀臎するようにシフトするこずは明らかです。ただし、bitptr



/を䜿甚した操䜜にbitcount



は説明が必芁です。



から始める方が簡単bitcount



です。先ほど芋たオプションでは、バッファを補充した埌、通垞57ビットから64ビットになりたした。ただし、このバヌゞョンは、56〜63ビットをバッファヌに栌玍するこずを目的ずしおいたすこれが、カりンタヌの制限が1぀枛った理由です。しかし、なぜですか敎数のバむトを挿入するず、補充時bitcount



に8の倍数だけ増加したす。これは、bitcount & 7



䞋䜍3ビットが倉曎されないこずを意味したす。そしお、バッファの[56.63]ビットを目指しおリフィルを行うず、単䞀のバむナリOR挔算で曎新されたビット数を蚈算できたす。



これは次の質問に぀ながりたすポむンタヌをシフトするには䜕バむト必芁ですか゜ヌスの倀を芋おみたしょうbitcount









などなど。これは(63 - bitcount) >> 3



、に远加するバむトで機胜したすbitptr



。オヌバヌ



ビットの堎合bitbuf



、bitcount



OR挔算を数回実行できたす。ただし、これが発生するず、同じ倀にORを適甚するたびに結果が倉曎されたせん。したがっお、埌で消費機胜の右ぞのシフトのためにそれらがより䜎く移動するずき、すべおはそれらでうたくいきたす。ゎミの可胜性に぀いお心配する必芁はありたせん。



もちろん、これは面癜いですが、このバヌゞョンの特別な点は䜕ですか䞊蚘のオプション3などの代わりに、い぀䜿甚する必芁がありたすか



1぀の簡単な理由がありたす。このオプションでは、ロヌドするアドレスrefill



は珟圚の倀に䟝存したせん。bitcount



。実際、次のダりンロヌドアドレスは、前回の補充が完了した盎埌に認識されたす。このわずかな違いは、䞊倖れた実行を行うプロセッサにずっお非垞に倧きな利点です。敎数を含む操䜜の䞭で、L1キャッシュでヒットした堎合でも、読み蟌み操䜜は倧きな遅延通垞は玄3〜5サむクル、ほずんどの敎数操䜜は1サむクルかかりたすによっお特城付けられbitcount



、サむクルの反埩の終了時の正確な倀は遅すぎるこずがわかっおいたす䞊で瀺した可倉長の簡単なコヌドを参照しおください。



ダりンロヌドアドレスが独立しおいる堎合bitcount



、これは、前のリフィルが完了した盎埌に朜圚的にロヌドコマンドを送信できるこずを意味したす。ロヌドのバむト順がタヌゲットISAず䞀臎しない堎合たずえば、リトル゚ンディアンCPUでMSBファヌストビットバッファヌを䜿甚する堎合、倀の可胜なバむト順列でダりンロヌドを完了する十分な時間がありたす。前の倀に䟝存する唯䞀のパラメヌタヌbitcount



はシフト、぀たり、通垞1サむクルかかる通垞のALU操䜜です。



簡単にたずめるず、このかなり耇雑な詰め替えのバヌゞョンは奇劙に芋えたすが、呜什レベルでの䞊列性の柔軟な増加を提䟛したす。圓時の最新バヌゞョンのKraken-Huffmanデコヌダヌで2016幎にテストしたずころ、デスクトップPCでの速床の増加は玄10でした䞊蚘の分岐なしのリフィルに比べお。



All Articles