Daiji256

bitを高速に数える(手法の比較)

公開
更新

はじめに

本記事ではbitの1の数を数える手法(アルゴリズム)の比較を行います。アルゴリズムの詳しい内容等は紹介せずあくまで処理速度の比較だけ行います。アルゴリズムの詳しい内容は下の参考文献に飛んでください。

アルゴリズム

Hamming weightで紹介されている5つの手法の演算回数について簡単にまとめます。Aは有名なアルゴリズムをそのまま実装した状態です。BとCはAの高速化である派生です。Dはbitが立っている(1になっている)数に比例して演算量が増えます。Eは力技で、16 bitすべての数に対して立ってるbitの数をメモしておき、64 bitを4分割してその合計として求めています。メモリを多く使用し、64 bitのようにbit数が多い場合はあまり高速ではありません。

手法算術乗算比較/分岐メモリ読み込み特徴
A24000単純な実装
B17000乗算が遅い環境で優秀
C12100乗算が速い環境で優秀
D3n01n0nは1になっている数
E900416 bitごとに読み取る

実験

実験環境・ソースコード

実験で使用したソースコードはGitHubにあげておきます。実験環境は下のとおりです。最適化についてはGitHubにあげているMakefileを見てください。

実験1単純な比較

まずは全bitに対して処理を行う安直な手法Nと5つの手法A—EをC言語に実装してタイムを計測します。1億個の乱数に対してビットカウントを行いその処理時間を計測します。計測結果を表にまとめます。このときの立っているbitの平均は32です。(64 bitの乱数でやりました。)この環境ではCがBよりも高速でした。Dはこの中ではもっとも遅いですがそれでもNに比べて十分高速です。EもCと同等の速度ですが、メモリのことを踏まえると微妙に感じます。

手法時間
N4.447 s
A0.541 s
B0.467 s
C0.386 s
D3.025 s
E0.393 s

実験2特殊環境下での比較

次に立っているbitの数が平均0.5の場合で測定します。Dは4倍程度高速になったがもっと高速化されてもいいのかなと思いました。しかし、Cなどと比較してもまだ遅いことから、この環境下では使用する必要性はないと思います。また、Nは下位bitから参照し0になったとき終了するようにしているため、高速化されたがそもそもNは使わない。

手法時間
N1.966 s
A0.534 s
B0.458 s
C0.381 s
D0.735 s
E0.364 s

実験3 -march=nativeによる最適化

-march=nativeは特定のCPUを指定して高速化を行います。コンパイル後のバイナリデータを他の人と共有する場合は不適切ですが、自分の環境だけでの使用の場合はオススメします。実験結果を示します。D以外は少し高速化されました。注目できるのはDで、30倍程度高速化されこの中で最速となっています。

手法時間
N4.257 s
A0.380 s
B0.235 s
C0.181 s
D0.091 s
E0.212 s

実験4 _popcnt64と__asm__

最後に_popcnt64と__asm__を利用して計測する。-march=native無しの結果をN、有りの結果をYに示しています。結果はどの自作関数より高速になりました。これらが使える環境では使用したほうが良いと思います。

手法時間 (N)時間 (Y)
_popcnt640.234 s0.071 s
__asm__0.234 s0.071 s

まとめ

bitを高速に数える手法の比較を行いました。安直に数える場合に比べて10倍程度高速に数えるアルゴリズムであると確認できました。_popcnt64と__asm__はこれらに比べてより高速であることも確認できました。

文献

  1. ソースコード
  2. Hamming weight
  3. x86_64でpopcnt / tzcnt / lzcntする【ビット演算テクニック Advent Calendar 2016 5日目】
  4. GCC
  5. bitを高速に数える 手法の比較