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

はじめに

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

アルゴリズム

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

手法 算術 乗算 比較/分岐 メモリ読み込み 特徴
A 24 0 0 0 単純な実装
B 17 0 0 0 乗算が遅い環境で優秀
C 12 1 0 0 乗算が速い環境で優秀
D 3n 0 1n 0 n1になっている数
E 9 0 0 4 16bitごとに読み取る

実験

実験環境ソースコード

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

  • OS: Ubuntu 20.04.1
  • Kernel: 5.4.0-58-generic
  • CPU: Intel i5-3360M (4) @ 3.5 GHz
  • gcc: version 9.3.0 (C11)

実験1単純な比較

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

手法 時間
N 4.447 s
A 0.541 s
B 0.467 s
C 0.386 s
D 3.025 s
E 0.393 s

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

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

手法 時間
N 1.966 s
A 0.534 s
B 0.458 s
C 0.381 s
D 0.735 s
E 0.364 s

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

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

手法 時間
N 4.257 s
A 0.380 s
B 0.235 s
C 0.181 s
D 0.091 s
E 0.212 s

実験4 _popcnt64__asm__

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

手法 時間(N) 時間(Y)
_popcnt64 0.234 s 0.071 s
__asm__ 0.234 s 0.071 s

まとめ

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

文献

  1. ソースコード(GitHub)
  2. Hamming weight (Wiki)
  3. x86_64popcnt/tzcnt/lzcntするビット演算テクニックAdvent Calendar 2016 5日目 (Qiita)
  4. GCC (GNU)
  5. bitを高速に数える 手法の比較(Qiita)