Cプログラミングで32ビットの符号なし整数の先行ゼロの数を数える効率的なアルゴリズムは何ですか?
この説明は、コンパイラが操作をサポートしていないか、十分なアセンブリを生成しないことを前提としています。最近では、これらの両方が発生する可能性は低いため、コンパイラでgccまたは同等のものに ___builtin_clz
_ を使用することをお勧めします。
どちらが「最良の」clzアルゴであるかを判断できるのはあなただけであることに注意してください。最新のプロセッサは複雑な獣であり、これらのアルゴリズムのパフォーマンスは、実行するプラットフォーム、スローするデータ、およびそれを使用するコードに大きく依存します。確実にする唯一の方法は、もう少し測定し、測定し、測定することです。違いがわからない場合は、おそらくボトルネックを見ていません。他の場所で時間を費やしたほうがよいでしょう。
退屈な免責事項が邪魔にならないので、問題について ハッカーのたのしみ が何を言っているのか見てみましょう。簡単な調査によると、すべてのアルゴリズムは何らかの記述の二分探索に依存しています。簡単な例を次に示します。
_int n = 32;
unsigned y;
y = x >>16; if (y != 0) { n = n -16; x = y; }
y = x >> 8; if (y != 0) { n = n - 8; x = y; }
y = x >> 4; if (y != 0) { n = n - 4; x = y; }
y = x >> 2; if (y != 0) { n = n - 2; x = y; }
y = x >> 1; if (y != 0) return n - 2;
return n - x;
_
これは32intで機能し、必要に応じて反復バージョンに変換することもできることに注意してください。残念ながら、そのソリューションには命令レベルの並列性があまりなく、ブランチがかなりあるため、アルゴを少しいじるのはあまりうまくいきません。上記のコードのブランチフリーバージョンが存在することに注意してくださいが、それははるかに冗長なので、ここでは再現しません。
それでは、pop命令(ビット数をカウント)を使用してソリューションを改善しましょう。
_x = x | (x >> 1);
x = x | (x >> 2);
x = x | (x >> 4);
x = x | (x >> 8);
x = x | (x >>16);
return pop(~x);
_
では、これはどのように機能しますか?重要なのは、最後にあるpop(~x)
命令で、x
のゼロの数をカウントします。ゼロの数を意味のあるものにするには、最初に、先頭にないすべての0を取り除く必要があります。これを行うには、バイナリアルゴリズムを使用して1を正しく伝播します。まだ命令レベルの並列性はあまりありませんが、すべてのブランチを削除し、以前のソリューションよりも少ないサイクルを使用します。ずっといい。
それで、そのポップ命令はどうですか、それはごまかしではありませんか?ほとんどのアーキテクチャには、コンパイラビルトイン(gccの___builtin_pop
_など)を介してアクセスできる1サイクルのポップ命令があります。それ以外の場合はテーブルベースのソリューションが存在しますが、テーブルが完全にL1キャッシュに保持されている場合でも、キャッシュアクセスのサイクルをトレードオフする場合は注意が必要です。
最後に、ハッカーの喜びにいつものように、私たちは奇妙な領域をさまよい始めます。浮動小数点数を使用して、いくつかの先行ゼロを数えましょう。
_union {
unsigned asInt[2];
double asDouble;
};
asDouble = (double)k + 0.5;
return 1054 - (asInt[LE] >> 20);
_
まず、ちょっとした警告:このアルゴリズムは使用しないでください。これは、標準に関する限り、未定義の動作をトリガーします。これは、実際の使用よりも楽しい要素のために再現されました。あなた自身の危険で使用してください。
免責事項が邪魔にならないので、どのように機能しますか?最初にintをdoubleに変換し、次にdoubleの指数成分を抽出します。きちんとしたもの。 LE定数は、リトルエンディアンマシンで実行する場合は1、ビッグエンディアンマシンで実行する場合は0にする必要があります。
これにより、この問題のさまざまなビット調整アルゴリズムについて簡単に調査できます。この本にはさまざまなトレードオフをもたらすこれらのバリエーションがいくつかあることに注意してください。ただし、それらを自分で発見できるようにします。
これはおそらく純粋なCでそれを行うための最適な方法です:
int clz(uint32_t x)
{
static const char debruijn32[32] = {
0, 31, 9, 30, 3, 8, 13, 29, 2, 5, 7, 21, 12, 24, 28, 19,
1, 10, 4, 14, 6, 22, 25, 20, 11, 15, 23, 26, 16, 27, 17, 18
};
x |= x>>1;
x |= x>>2;
x |= x>>4;
x |= x>>8;
x |= x>>16;
x++;
return debruijn32[x*0x076be629>>27];
}
1つの制限:記述されているように、ゼロの入力をサポートしていません(結果は32になるはずです)。すべての入力が0x80000000
未満の場合は、テーブルの最初の値を32に変更することで、追加コストなしでゼロをサポートできます。それ以外の場合は、最初に行を追加します。
if (!x) return 32;