設定されている最下位ビットの位置


120

整数に設定されている最下位ビットの位置を決定する効率的な方法を探しています。たとえば、0x0FF0の場合は4になります。

簡単な実装は次のとおりです。

unsigned GetLowestBitPos(unsigned value)
{
   assert(value != 0); // handled separately

   unsigned pos = 0;
   while (!(value & 1))
   {
      value >>= 1;
      ++pos;
   }
   return pos;
}

それからいくつかのサイクルを絞る方法はありますか?

(注:この質問は、そのようなことを楽しむ人のためのものであり、xyzoptimizationは悪だと言われるためのものではありません。)

[編集] アイデアをありがとう!他にもいくつかのことを学びました。涼しい!


while((値_N >>(++ pos))!= 0);
トーマス

回答:


170

Bit Twiddling Hacksは、パフォーマンス/最適化のディスカッションが添付された、ビットツイドルハックの優れたコレクションを提供します。(そのサイトからの)あなたの問題に対する私のお気に入りの解決策は、「乗算とルックアップ」です:

unsigned int v;  // find the number of trailing zeros in 32-bit v 
int r;           // result goes here
static const int MultiplyDeBruijnBitPosition[32] = 
{
  0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 
  31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];

参考になる参考資料:


18
なぜ反対票か。これは、乗算の速度に応じて、おそらく最速の実装です。確かにコードはコンパクトで、(v&-v)トリックは誰もが覚えて覚えておくべきものです。
アダムデイビス

2
+1は非常に優れていますが、乗算演算はif(X&Y)演算と比較するとどれほど高価ですか。
ブライアンR.ボンディ

4
誰かがこれのパフォーマンスがと比較してどのように知っています__builtin_ffslffsl
Steven Lu

2
@ジム・バルター、しかしモジュロは現代のハードウェアでの乗算と比較して非常に遅いです。だから私はそれをより良い解決策とは言いません。
2014年

2
値0x01と0x00の両方が配列から値0になるように思えます。どうやらこのトリックは、0が渡されると最下位ビットが設定されることを示します!
abelenky 2016

80

組み込みのffsを使用しないのはなぜですか?(私はLinuxからmanページを入手しましたが、それよりも広く入手可能です。)

ffs(3)-Linuxのマニュアルページ

名前

ffs-単語の最初のビットセットを見つける

あらすじ

#include <strings.h>
int ffs(int i);
#define _GNU_SOURCE
#include <string.h>
int ffsl(long int i);
int ffsll(long long int i);

説明

ffs()関数は、ワードiの最初の(最下位)ビットセットの位置を返します。最下位ビットは位置1で、最上位位置は32または64です。関数ffsll()とffsl()は同じことを行いますが、サイズが異なる可能性のある引数を取ります。

戻り値

これらの関数は、最初のビットセットの位置を返すか、iにビットが設定されていない場合は0を返します。

に準拠

4.3BSD、POSIX.1-2001。

ノート

BSDシステムには、プロトタイプがあり<string.h>ます。


6
参考までに、これは利用可能な場合、対応するアセンブリコマンドにコンパイルされます。
ジェレミー

46

bsfそれを行うx86アセンブリ命令()があります。:)

より最適化されていますか?

サイドノート:

このレベルでの最適化は、本質的にアーキテクチャに依存しています。今日のプロセッサは(分岐予測、キャッシュミス、パイプライン処理に関して)複雑すぎるため、どのアーキテクチャでどのコードがより高速に実行されるかを予測することは非常に困難です。操作を32から9に減らすなどの操作を行うと、一部のアーキテクチャではパフォーマンスが低下する場合があります。単一のアーキテクチャで最適化されたコードは、他のアーキテクチャでより悪いコードになる可能性があります。これを特定のCPU向けに最適化するか、そのままにして、コンパイラーがより良いと思うものをコンパイラーに選択させると思います。


20
@dwc:私は理解していますが、この節を考えています:「それからいくつかのサイクルを絞る方法はありますか?」そのような答えは完全に受け入れられます!
Mehrdad Afshari、

5
+1エンディアンのため、彼の答えは必ず彼のアーキテクチャに依存しているため、アセンブリ命令にドロップダウンすることは完全に有効な答えです。
Chris Lutz、

3
+1賢い答え、はい、CでもC ++でもありませんが、それは仕事に適したツールです。
Andrew Hare、

1
待って、気にしないで。整数の実際の値はここでは関係ありません。ごめんなさい。
Chris Lutz、

2
@Bastian:オペランドがゼロの場合、ZF = 1を設定します。
Mehrdad Afshari、

43

最新のアーキテクチャのほとんどは、最下位セットビットまたは最上位セットビットの位置を検出したり、先行ゼロの数を数えたりするための命令を持っています。

このクラスのいずれかの命令がある場合は、他の命令を安価にエミュレートできます。

少し時間を取って紙の上で作業し、アーキテクチャ、ワード長などに関係なくx & (x-1)、xの最下位セットビットがクリアされ、最下位セットビット( x & ~(x-1) )のみが返されることを理解します。これを知っていると、ハードウェアカウントリーディングを使用するのは簡単です。 -zeroes / highest-set-bitを指定する明示的な命令がない場合は、最下位セットビットを検索します。

関連するハードウェアサポートがまったくない場合は、ここに示されている count-leading- zerosの乗法とルックアップの実装、またはビット小刻みハックページの1つを簡単に変換して、上記のIDとブランチレスであるという利点があります。


18

ほら、ソリューションの負荷であり、ベンチマークではありません。人々はあなた自身を恥じるべきです;-)

私のマシンはIntel i530(2.9 GHz)で、Windows 7 64ビットを実行しています。MinGWの32ビットバージョンでコンパイルしました。

$ gcc --version
gcc.exe (GCC) 4.7.2

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2
$ bench
Naive loop.         Time = 2.91  (Original questioner)
De Bruijn multiply. Time = 1.16  (Tykhyy)
Lookup table.       Time = 0.36  (Andrew Grant)
FFS instruction.    Time = 0.90  (ephemient)
Branch free mask.   Time = 3.48  (Dan / Jim Balter)
Double hack.        Time = 3.41  (DocMax)

$ gcc bench.c -o bench.exe -std=c99 -Wall -O2 -march=native
$ bench
Naive loop.         Time = 2.92
De Bruijn multiply. Time = 0.47
Lookup table.       Time = 0.35
FFS instruction.    Time = 0.68
Branch free mask.   Time = 3.49
Double hack.        Time = 0.92

私のコード:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>


#define ARRAY_SIZE 65536
#define NUM_ITERS 5000  // Number of times to process array


int find_first_bits_naive_loop(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            if (value == 0)
                continue;
            unsigned pos = 0;
            while (!(value & 1))
            {
                value >>= 1;
                ++pos;
            }
            total += pos + 1;
        }
    }

    return total;
}


int find_first_bits_de_bruijn(unsigned nums[ARRAY_SIZE])
{
    static const int MultiplyDeBruijnBitPosition[32] = 
    {
       1, 2, 29, 3, 30, 15, 25, 4, 31, 23, 21, 16, 26, 18, 5, 9, 
       32, 28, 14, 24, 22, 20, 17, 8, 27, 13, 19, 7, 12, 6, 11, 10
    };

    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int c = nums[i];
            total += MultiplyDeBruijnBitPosition[((unsigned)((c & -c) * 0x077CB531U)) >> 27];
        }
    }

    return total;
}


unsigned char lowestBitTable[256];
int get_lowest_set_bit(unsigned num) {
    unsigned mask = 1;
    for (int cnt = 1; cnt <= 32; cnt++, mask <<= 1) {
        if (num & mask) {
            return cnt;
        }
    }

    return 0;
}
int find_first_bits_lookup_table(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned int value = nums[i];
            // note that order to check indices will depend whether you are on a big 
            // or little endian machine. This is for little-endian
            unsigned char *bytes = (unsigned char *)&value;
            if (bytes[0])
                total += lowestBitTable[bytes[0]];
            else if (bytes[1])
              total += lowestBitTable[bytes[1]] + 8;
            else if (bytes[2])
              total += lowestBitTable[bytes[2]] + 16;
            else
              total += lowestBitTable[bytes[3]] + 24;
        }
    }

    return total;
}


int find_first_bits_ffs_instruction(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            total +=  __builtin_ffs(nums[i]);
        }
    }

    return total;
}


int find_first_bits_branch_free_mask(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            int i16 = !(value & 0xffff) << 4;
            value >>= i16;

            int i8 = !(value & 0xff) << 3;
            value >>= i8;

            int i4 = !(value & 0xf) << 2;
            value >>= i4;

            int i2 = !(value & 0x3) << 1;
            value >>= i2;

            int i1 = !(value & 0x1);

            int i0 = (value >> i1) & 1? 0 : -32;

            total += i16 + i8 + i4 + i2 + i1 + i0 + 1;
        }
    }

    return total;
}


int find_first_bits_double_hack(unsigned nums[ARRAY_SIZE])
{
    int total = 0; // Prevent compiler from optimizing out the code
    for (int j = 0; j < NUM_ITERS; j++) {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            unsigned value = nums[i];
            double d = value ^ (value - !!value); 
            total += (((int*)&d)[1]>>20)-1022; 
        }
    }

    return total;
}


int main() {
    unsigned nums[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++) {
        nums[i] = rand() + (rand() << 15);
    }

    for (int i = 0; i < 256; i++) {
        lowestBitTable[i] = get_lowest_set_bit(i);
    }


    clock_t start_time, end_time;
    int result;

    start_time = clock();
    result = find_first_bits_naive_loop(nums);
    end_time = clock();
    printf("Naive loop.         Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_de_bruijn(nums);
    end_time = clock();
    printf("De Bruijn multiply. Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_lookup_table(nums);
    end_time = clock();
    printf("Lookup table.       Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_ffs_instruction(nums);
    end_time = clock();
    printf("FFS instruction.    Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_branch_free_mask(nums);
    end_time = clock();
    printf("Branch free mask.   Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);

    start_time = clock();
    result = find_first_bits_double_hack(nums);
    end_time = clock();
    printf("Double hack.        Time = %.2f, result = %d\n", 
        (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
}

8
de Bruijnとルックアップの両方のベンチマークは誤解を招く可能性があります-そのようなタイトなループに座って、最初の操作の後、各タイプのルックアップテーブルは、最後のループの後までL1キャッシュに固定されます。これは、実際の使用法と一致しない可能性があります。
MattW

1
下位バイトにゼロがある入力の場合、ポインターキャストのため、シフトではなく格納/再読み込みによって上位バイトを取得します。(完全に不要なBTWであり、エンディアンに依存させるため、シフトはそうではありません)。とにかく、ホットキャッシュが原因でマイクロベンチマークが非現実的になるだけでなく、分岐予測子が準備され、入力をテストして非常によく予測し、LUTの機能を低下させます。実際のユースケースの多くは、入力ではなく結果の分布がより均一です。
Peter Cordes

2
あなたのFFSループは、残念ながらあなたの無愛想な古いコンパイラが避けていません(BSF命令で偽の依存関係によって減速されますが、より新しいgccは、すべきPOPCNT / lzcnt / tzcntに同じBSF実際の動作するので(その出力に偽の依存関係を持っていますinput = 0の場合、出力は変更されません。gccは、残念ながら、ループの反復間でレジスタをクリアしないことにより、これをループ運搬の依存関係に変えます。したがって、ループは5サイクルごとに1つ実行され、BSF(3)+ CMOVでボトルネックになります。 (2)待ち時間
Peter Cordes

1
ベンチマークでは、LUTがFFSメソッドのスループットのほぼ正確に2倍であり、静的分析予測と非常によく一致していることがわかりました:)。ループ内の唯一のシリアル依存関係が合計に合計されるため、遅延ではなくスループットを測定していることに注意してください。 誤った依存関係がなければ、ffs()1クロックあたり1つのスループットが必要でした(3 uops、BSFに1つ、CMOVに2つ、そして異なるポートで実行できます)。同じループオーバーヘッドで、クロックごとに3つ(CPUで)実行できるのは7 ALU uopsです。オーバーヘッドが支配します! 出典:agner.org/optimize
Peter Cordes

1
はい、順序どおりに実行bsf ecx, [ebx+edx*4]しないecxと、待機する必要のある入力として扱わなかった場合、ループの複数の反復がオーバーラップする可能性があります。(ECXは、前のイテラトンのCMOVによって最後に作成されました)。しかし、CPUはそのように動作し、「ソースがゼロの場合は変更されないdestを残​​す」動作を実装します(したがって、TZCNTの場合のように真にfalse depではありません。想定に分岐+投機的実行がないため、データ依存関係が必要です。入力が非ゼロであること)。のxor ecx,ecx前にを追加bsfしてECXへの依存を解除することで、これを克服できます。
Peter Cordes

17

これに対する最速の(非組み込み/非アセンブラー)ソリューションは、最下位バイトを見つけて、そのバイトを256エントリーのルックアップテーブルで使用することです。これにより、4つの条件付き命令のワーストケースパフォーマンスと1のベストケースが得られます。これにより、命令の量が最小になるだけでなく、最新のハードウェアで非常に重要な分岐の量も最小になります。

テーブル(256個の8ビットエントリ)には、0〜255の範囲の各数値のLSBのインデックスが含まれている必要があります。値の各バイトをチェックし、ゼロ以外の最下位バイトを見つけ、この値を使用して実際のインデックスを検索します。

これには256バイトのメモリが必要ですが、この機能の速度が非常に重要な場合は、256バイトで十分です。

例えば

byte lowestBitTable[256] = {
.... // left as an exercise for the reader to generate
};

unsigned GetLowestBitPos(unsigned value)
{
  // note that order to check indices will depend whether you are on a big 
  // or little endian machine. This is for little-endian
  byte* bytes = (byte*)value;
  if (bytes[0])
    return lowestBitTable[bytes[0]];
  else if (bytes[1])
      return lowestBitTable[bytes[1]] + 8;
  else if (bytes[2])
      return lowestBitTable[bytes[2]] + 16;
  else
      return lowestBitTable[bytes[3]] + 24;  
}

1
それは実際には3つの条件文の最悪のケースです:)しかし、はい、これが最速のアプローチです(そして通常、このようなインタビューの質問で人々が探しているものです)。
ブライアン

4
どこかに+ 8、+ 16、+ 24が必要ですか?
マークランサム

7
ルックアップテーブルを使用すると、キャッシュミスの可能性が高まり、メモリアクセスのコストが発生する可能性があります。これは、命令の実行より数桁も高くなる可能性があります。
Mehrdad Afshari、

1
私はビットシフトも使用します(毎回8ずつシフトします)。レジスタを使用して完全に行うことができます。ポインタを使用すると、メモリにアクセスする必要があります。
Johannes Schaub-litb 2009

1
合理的な解決策ですが、ルックアップテーブルがキャッシュにない可能性(指摘されているように解決できます)と分岐の数(潜在的な分岐の予測ミス)の間では、乗算とルックアップの解決策(分岐なし、より小さなルックアップテーブル)。もちろん、組み込み関数またはインラインアセンブリを使用できる場合は、おそらくそれらがより良い選択です。それでも、このソリューションは悪くありません。

13

OMGはこれを急上昇させています。

これらの例のほとんどに欠けているのは、すべてのハードウェアがどのように機能するかについて少し理解していることです。

ブランチがあるときはいつでも、CPUはどのブランチが使用されるかを推測する必要があります。命令パイプには、推測されたパスを導く命令が読み込まれます。CPUが間違っていると推測した場合は、命令パイプがフラッシュされ、他のブランチをロードする必要があります。

上部の単純なwhileループについて考えてみます。推測はループ内に留まることになります。それがループを去るとき、それは少なくとも一度は間違っているでしょう。これにより、命令パイプがフラッシュされます。この動作は、ループを終了すると推測するよりもわずかに優れています。この場合、ループが繰り返されるたびに命令パイプがフラッシュされます。

失われるCPUサイクルの量は、プロセッサの種類によって大きく異なります。しかし、20〜150のCPUサイクルの損失が予想されます。

次に悪いグループは、値を小さな部分に分割し、さらにいくつかのブランチを追加することにより、数回の反復を節約することを考えているグループです。これらの各分岐は、命令パイプをフラッシュする追加の機会を追加し、さらに20〜150クロックサイクルのコストがかかります。

テーブルの値を検索するとどうなるかを考えてみましょう。おそらく、値が現在キャッシュ内にない可能性があります。少なくとも、関数が初めて呼び出されたときはそうではありません。これは、値がキャッシュからロードされている間、CPUがストールすることを意味します。繰り返しますが、これはマシンごとに異なります。新しいIntelチップは実際にこれを使用して、現在のスレッドがキャッシュのロードが完了するのを待っている間にスレッドを交換します。これは、命令パイプのフラッシュよりも簡単にコストがかかる可能性がありますが、この操作を何度も実行している場合は、1回しか発生しない可能性があります。

明らかに、最速の一定時間の解は、決定論的な計算を伴うものです。純粋でエレガントなソリューション。

これがすでにカバーされていた場合、私の謝罪。

XCODE AFAIKを除き、私が使用するすべてのコンパイラには、フォワードビットスキャンとリバースビットスキャンの両方に対応するコンパイラ組み込み関数があります。これらは、ほとんどのハードウェアで単一のアセンブリ命令にコンパイルされ、キャッシュミス、分岐ミス予測、および他のプログラマが生成する障害ブロックはありません。

Microsoftコンパイラの場合、_BitScanForwardおよび_BitScanReverseを使用します。
GCCの場合、__ builtin_ffs、__ builtin_clz、__ builtin_ctzを使用します。

また、議論されている主題について十分な知識がない場合は、回答を投稿したり、誤解を招く可能性のある新規参入者を控えたりしてください。

申し訳ありませんが、解決策を提供するのを完全に忘れていました。これは、IPADで使用するコードで、タスクのアセンブリレベルの指示はありません。

unsigned BitScanLow_BranchFree(unsigned value)
{
    bool bwl = (value & 0x0000ffff) == 0;
    unsigned I1 = (bwl * 15);
    value = (value >> I1) & 0x0000ffff;

    bool bbl = (value & 0x00ff00ff) == 0;
    unsigned I2 = (bbl * 7);
    value = (value >> I2) & 0x00ff00ff;

    bool bnl = (value & 0x0f0f0f0f) == 0;
    unsigned I3 = (bnl * 3);
    value = (value >> I3) & 0x0f0f0f0f;

    bool bsl = (value & 0x33333333) == 0;
    unsigned I4 = (bsl * 1);
    value = (value >> I4) & 0x33333333;

    unsigned result = value + I1 + I2 + I3 + I4 - 1;

    return result;
}

ここで理解しておくべきことは、高価なのは比較ではなく、比較後に発生する分岐であることです。この場合の比較は、.. == 0を使用して0または1の値に強制され、その結果を使用して、分岐のいずれかの側で発生したであろう数学を結合します。

編集:

上記のコードは完全に壊れています。このコードは機能し、ブランチなしです(最適化されている場合)。

int BitScanLow_BranchFree(ui value)
{
    int i16 = !(value & 0xffff) << 4;
    value >>= i16;

    int i8 = !(value & 0xff) << 3;
    value >>= i8;

    int i4 = !(value & 0xf) << 2;
    value >>= i4;

    int i2 = !(value & 0x3) << 1;
    value >>= i2;

    int i1 = !(value & 0x1);

    int i0 = (value >> i1) & 1? 0 : -32;

    return i16 + i8 + i4 + i2 + i1 + i0;
}

これは、0が指定された場合は-1を返します。0を気にしない場合、または0で31を取得することに満足している場合は、i0計算を削除して時間のチャンクを節約します。


3
修正しました。投稿内容を必ずテストしてください。
ジムバルター2013年

5
そこに三項演算子が含まれている場合、それをどのように「ブランチフリー」と呼ぶことができますか?
BoltBait 2016

2
その条件付きの動き。両方の可能な値をパラメーターとして取り、条件の評価に基づいてmov操作を実行する単一のアセンブリ言語命令。したがって、「ブランチフリー」です。別の不明な、またはおそらく正しくないアドレスへのジャンプはありません。
Dan


7

セットビットの検索を含むこの同様の投稿に触発され、私は以下を提供します:

unsigned GetLowestBitPos(unsigned value)
{
   double d = value ^ (value - !!value); 
   return (((int*)&d)[1]>>20)-1023; 
}

長所:

  • ループなし
  • 分岐なし
  • 一定の時間で実行されます
  • 範囲外の結果を返すことにより、value = 0を処理します
  • たった2行のコード

短所:

  • コード化されたリトルエンディアンを前提としています(定数を変更することで修正できます)
  • doubleが実数* 8のIEEE浮動小数点数(IEEE 754)であると想定します

更新: コメントで指摘されているように、ユニオンは(少なくともCでは)よりクリーンな実装であり、次のようになります。

unsigned GetLowestBitPos(unsigned value)
{
    union {
        int i[2];
        double d;
    } temp = { .d = value ^ (value - !!value) };
    return (temp.i[1] >> 20) - 1023;
}

これは、すべてに対してリトルエンディアンストレージを備えた32ビットの整数を想定しています(x86プロセッサを考えてみてください)。


1
おもしろい-ビット演算にdoubleを使うのはまだ怖いですが、覚えておきます
peterchen 2009

frexp()を使用すると、移植性が少し向上する可能性があります
aka.nice

1
ポインターキャストによる型パンニングは、CまたはC ++では安全ではありません。C ++でmemcpyを使用するか、Cで共用体を使用します(コンパイラが安全を保証する場合はC ++で共用体を使用します。たとえば、C ++のGNU拡張機能(多くのコンパイラでサポートされています)は、共用体型パニングが安全であることを保証しています。)
Peter Cordes

1
古いgccは、ポインタキャストの代わりにユニオンを使用してより優れたコードも作成します。これは、格納/再ロードする代わりに、FP reg(xmm0)からrax(movqを使用)に直接移動します。新しいgccとclangは両方の方法でmovqを使用します。共用体バージョンについては、godbolt.org / g / x7JBiLを参照してください。算術シフトを20ずつ行うことは意図的ですか?また、仮定にintint32_t、と、符号付き右シフトが算術シフトである(C ++では実装定義)
Peter Cordes

1
また、Visual Studio(少なくとも2013年)もtest / setcc / subアプローチを使用しています。私はcmp / adcの方が好きです。
DocMax 2017

5

32未満の操作というワーストケースで実行できます。

原則: 2ビット以上のチェックは、1ビットのチェックと同じくらい効率的です。

したがって、たとえば、最初にどのグループにグループ化されているかを確認してから、そのグループの各ビットを最小から最大にチェックすることを妨げるものは何もありません。

したがって...
一度に2ビットをチェックする場合、最悪の場合(Nbits / 2)+ 1は合計をチェックします。
一度に3ビットをチェックする場合、最悪の場合(Nbits / 3)+ 2チェックの合計になります。
...

最適なのは、4つのグループでチェックインすることです。最悪の場合、32ではなく11の操作が必要になります。

最良のケースは、アルゴリズムの1つのチェックから、このグループ化のアイデアを使用する場合の2つのチェックです。しかし、ベストケースでの追加の1チェックは、最悪の場合の節約には価値があります。

注:ループの方が効率的であるため、ループを使用する代わりに完全に書き出します。

int getLowestBitPos(unsigned int value)
{
    //Group 1: Bits 0-3
    if(value&0xf)
    {
        if(value&0x1)
            return 0;
        else if(value&0x2)
            return 1;
        else if(value&0x4)
            return 2;
        else
            return 3;
    }

    //Group 2: Bits 4-7
    if(value&0xf0)
    {
        if(value&0x10)
            return 4;
        else if(value&0x20)
            return 5;
        else if(value&0x40)
            return 6;
        else
            return 7;
    }

    //Group 3: Bits 8-11
    if(value&0xf00)
    {
        if(value&0x100)
            return 8;
        else if(value&0x200)
            return 9;
        else if(value&0x400)
            return 10;
        else
            return 11;
    }

    //Group 4: Bits 12-15
    if(value&0xf000)
    {
        if(value&0x1000)
            return 12;
        else if(value&0x2000)
            return 13;
        else if(value&0x4000)
            return 14;
        else
            return 15;
    }

    //Group 5: Bits 16-19
    if(value&0xf0000)
    {
        if(value&0x10000)
            return 16;
        else if(value&0x20000)
            return 17;
        else if(value&0x40000)
            return 18;
        else
            return 19;
    }

    //Group 6: Bits 20-23
    if(value&0xf00000)
    {
        if(value&0x100000)
            return 20;
        else if(value&0x200000)
            return 21;
        else if(value&0x400000)
            return 22;
        else
            return 23;
    }

    //Group 7: Bits 24-27
    if(value&0xf000000)
    {
        if(value&0x1000000)
            return 24;
        else if(value&0x2000000)
            return 25;
        else if(value&0x4000000)
            return 26;
        else
            return 27;
    }

    //Group 8: Bits 28-31
    if(value&0xf0000000)
    {
        if(value&0x10000000)
            return 28;
        else if(value&0x20000000)
            return 29;
        else if(value&0x40000000)
            return 30;
        else
            return 31;
    }

    return -1;
}

私からの+1。それは最速ではありませんが、ポイントだったオリジナルよりも高速です
アンドリューグラント

@ onebyone.livejournal.com:コードにバグがあったとしても、グループ化の概念は、私が理解しようとしたポイントです。実際のコードサンプルはそれほど重要ではなく、よりコンパクトになる可能性がありますが、効率は低下します。
ブライアンR.ボンディ

私の答えに本当に悪い部分があるのか​​、それとも人々がそれを好まなかったのか、私はそれを完全に書きましたか?
ブライアンR.ボンディ

@ onebyone.livejournal.com:2つのアルゴリズムを比較するときは、最適化フェーズによって魔法のように変換されるとは想定せずに、アルゴリズムをそのまま比較する必要があります。私のアルゴリズムも「高速」であると主張したことはありません。それだけが少ない操作です。
ブライアンR.ボンディ

@ onebyone.livejournal.com:...上記のコードをプロファイルして、操作が少ないことを知る必要はありません。はっきり見えます。私はプロファイリングを必要とするいかなる主張もしませんでした。
ブライアンR.ボンディ

4

バイナリ検索を使用しないのはなぜですか?これは常に5つの操作の後に完了します(intサイズが4バイトであると想定)。

if (0x0000FFFF & value) {
    if (0x000000FF & value) {
        if (0x0000000F & value) {
            if (0x00000003 & value) {
                if (0x00000001 & value) {
                    return 1;
                } else {
                    return 2;
                }
            } else {
                if (0x0000004 & value) {
                    return 3;
                } else {
                    return 4;
                }
            }
        } else { ...
    } else { ...
} else { ...

+1これは私の答えと非常によく似ています。最良の場合の実行時間は私の提案よりも悪いですが、最悪の場合の実行時間の方が優れています。
ブライアンR.ボンディ

2

別の方法(係数の除算とルックアップ)は、@ anton-tykhyyによって提供された同じリンクから、ここで特別な言及に値します。この方法は、パフォーマンスがDeBruijn乗算およびルックアップ方法と非常に似ていますが、わずかながら重要な違いがあります。

係数の除算とルックアップ

 unsigned int v;  // find the number of trailing zeros in v
    int r;           // put the result in r
    static const int Mod37BitPosition[] = // map a bit value mod 37 to its position
    {
      32, 0, 1, 26, 2, 23, 27, 0, 3, 16, 24, 30, 28, 11, 0, 13, 4,
      7, 17, 0, 25, 22, 31, 15, 29, 10, 12, 6, 0, 21, 14, 9, 5,
      20, 8, 19, 18
    };
    r = Mod37BitPosition[(-v & v) % 37];

モジュラス除算とルックアップメソッドは、v = 0x00000000とv = FFFFFFFFに対して異なる値を返しますが、DeBruijn乗算とルックアップメソッドは両方の入力でゼロを返します。

テスト:-

unsigned int n1=0x00000000, n2=0xFFFFFFFF;

MultiplyDeBruijnBitPosition[((unsigned int )((n1 & -n1) * 0x077CB531U)) >> 27]); /* returns 0 */
MultiplyDeBruijnBitPosition[((unsigned int )((n2 & -n2) * 0x077CB531U)) >> 27]); /* returns 0 */
Mod37BitPosition[(((-(n1) & (n1))) % 37)]); /* returns 32 */
Mod37BitPosition[(((-(n2) & (n2))) % 37)]); /* returns 0 */

1
mod遅い。代わりに、元の乗法とルックアップ法を使用して!vから減算しrて、エッジケースを処理できます。
Eitan T

3
@EitanTオプティマイザは、そのmodをハッカーの喜びのように高速な乗算に変換する可能性があります
phuclv

2

Chess Programming BitScanページと私の測定によると、減算とxorは、否定とマスクよりも高速です。

(で末尾のゼロを数える場合は0、私が持っているメソッドが返されます63が、否定とマスクは返されます0。)

64ビットの減算とxorは次のとおりです。

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61,
  54, 58, 35, 52, 50, 42, 21, 44, 38, 32, 29, 23, 17, 11, 4, 62,
  46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, 31, 22, 10, 45,
  25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v ^ (v-1)) * 0x03F79D71B4CB0A89U)) >> 58];

参考までに、64ビットバージョンのnegate and maskメソッドを次に示します。

unsigned long v;  // find the number of trailing zeros in 64-bit v 
int r;            // result goes here
static const int MultiplyDeBruijnBitPosition[64] = 
{
  0, 1, 48, 2, 57, 49, 28, 3, 61, 58, 50, 42, 38, 29, 17, 4,
  62, 55, 59, 36, 53, 51, 43, 22, 45, 39, 33, 30, 24, 18, 12, 5,
  63, 47, 56, 27, 60, 41, 37, 16, 54, 35, 52, 21, 44, 32, 23, 11,
  46, 26, 40, 15, 34, 20, 31, 10, 25, 14, 19, 9, 13, 8, 7, 6
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x03F79D71B4CB0A89U)) >> 58];

この(v ^ (v-1))作品は提供されましたv != 0。以下の場合、v == 0一方で、それが0xFF .... FFを返す(v & -v)ゼロ与えます(ちなみに間違っても、bufが、少なくともそれが合理的な結果につながります)。
CiaPan 2014年

@CiaPan:それは良い点です。63番目のインデックスに0を置くことによってこれを解決する別のDe Bruijn数があると思います。
jnm2 14年

ああ、それは問題ではありません。0と0x8000000000000000はどちらもの後に0xFFFFFFFFFFFFFFFF v ^ (v-1)となるため、区別する必要はありません。私のシナリオでは、ゼロは入力されません。
jnm2 14年

1

下位ビットが設定されているかどうかを確認できます。もしそうなら、残りのビットの下位を見てください。例えば、:

32ビット整数-最初の16のいずれかが設定されているかどうかを確認します。その場合は、最初の8つが設定されているかどうかを確認します。もしそうなら、...

そうでない場合は、上位16のいずれかが設定されているかどうかを確認します。

基本的には、バイナリ検索です。


1

私の答えを参照してくださいここで見つけることがことを除いて、単一のx86命令でそれを行う方法については、少なくとも重要なセットが欲しいよビットBSF(「スキャン前進ビット」)命令の代わりに、BSRそこに記載さを。


1

さらに別の解決策は、おそらく最速ではありませんが、かなり良いようです。
少なくとも枝はありません。;)

uint32 x = ...;  // 0x00000001  0x0405a0c0  0x00602000
x |= x <<  1;    // 0x00000003  0x0c0fe1c0  0x00e06000
x |= x <<  2;    // 0x0000000f  0x3c3fe7c0  0x03e1e000
x |= x <<  4;    // 0x000000ff  0xffffffc0  0x3fffe000
x |= x <<  8;    // 0x0000ffff  0xffffffc0  0xffffe000
x |= x << 16;    // 0xffffffff  0xffffffc0  0xffffe000

// now x is filled with '1' from the least significant '1' to bit 31

x = ~x;          // 0x00000000  0x0000003f  0x00001fff

// now we have 1's below the original least significant 1
// let's count them

x = x & 0x55555555 + (x >>  1) & 0x55555555;
                 // 0x00000000  0x0000002a  0x00001aaa

x = x & 0x33333333 + (x >>  2) & 0x33333333;
                 // 0x00000000  0x00000024  0x00001444

x = x & 0x0f0f0f0f + (x >>  4) & 0x0f0f0f0f;
                 // 0x00000000  0x00000006  0x00000508

x = x & 0x00ff00ff + (x >>  8) & 0x00ff00ff;
                 // 0x00000000  0x00000006  0x0000000d

x = x & 0x0000ffff + (x >> 16) & 0x0000ffff;
                 // 0x00000000  0x00000006  0x0000000d
// least sign.bit pos. was:  0           6          13

1最下位1からLSBまでのすべてのs を取得するには、((x & -x) - 1) << 1代わりに使用します
phuclv 14年

さらに高速な方法:x ^ (x-1)
phuclv 2014年

1
unsigned GetLowestBitPos(unsigned value)
{
    if (value & 1) return 1;
    if (value & 2) return 2;
    if (value & 4) return 3;
    if (value & 8) return 4;
    if (value & 16) return 5;
    if (value & 32) return 6;
    if (value & 64) return 7;
    if (value & 128) return 8;
    if (value & 256) return 9;
    if (value & 512) return 10;
    if (value & 1024) return 11;
    if (value & 2048) return 12;
    if (value & 4096) return 13;
    if (value & 8192) return 14;
    if (value & 16384) return 15;
    if (value & 32768) return 16;
    if (value & 65536) return 17;
    if (value & 131072) return 18;
    if (value & 262144) return 19;
    if (value & 524288) return 20;
    if (value & 1048576) return 21;
    if (value & 2097152) return 22;
    if (value & 4194304) return 23;
    if (value & 8388608) return 24;
    if (value & 16777216) return 25;
    if (value & 33554432) return 26;
    if (value & 67108864) return 27;
    if (value & 134217728) return 28;
    if (value & 268435456) return 29;
    if (value & 536870912) return 30;
    return 31;
}

すべての数値の50%がコードの最初の行に返されます。

すべての数値の75%がコードの最初の2行で返されます。

すべての数値の87%がコードの最初の3行で返されます。

すべての数値の94%がコードの最初の4行で返されます。

すべての数値の97%がコードの最初の5行で返されます。

このコードの最悪の場合のシナリオがいかに非効率的であるかについて不平を言っている人々は、その状態がどれほどまれに起こるかを理解していないと思います。


3
そして、32ブランチの予測ミスの最悪のケース:)

1
これは、少なくともスイッチにすることができませんでした...?
Steven Lu

「これを少なくともスイッチにすることはできません...?」それが可能であることを示唆する前に、それを試みましたか?スイッチの場合に正しく計算できるのはいつですか?これは、クラスではなくルックアップテーブルです。
j riv 2018

1

「マジックマスク」を使用してこの巧妙なトリックを見つけた「プログラミングの芸術、パート4」は、nビット数のO(log(n))時間でそれを行います。[log(n)の追加スペースあり]。セットビットをチェックする一般的なソリューションは、O(n)であるか、ルックアップテーブルにO(n)の追加スペースが必要であるため、これは適切な妥協案です。

魔法のマスク:

m0 = (...............01010101)  
m1 = (...............00110011)
m2 = (...............00001111)  
m3 = (.......0000000011111111)
....

重要なアイデア: x = 1 * [(x&m0)= 0] + 2 * [(x&m1)= 0] + 4 * [(x&m2)= 0] + ...

int lastSetBitPos(const uint64_t x) {
    if (x == 0)  return -1;

    //For 64 bit number, log2(64)-1, ie; 5 masks needed
    int steps = log2(sizeof(x) * 8); assert(steps == 6);
    //magic masks
    uint64_t m[] = { 0x5555555555555555, //     .... 010101
                     0x3333333333333333, //     .....110011
                     0x0f0f0f0f0f0f0f0f, //     ...00001111
                     0x00ff00ff00ff00ff, //0000000011111111 
                     0x0000ffff0000ffff, 
                     0x00000000ffffffff };

    //Firstly extract only the last set bit
    uint64_t y = x & -x;

    int trailZeros = 0, i = 0 , factor = 0;
    while (i < steps) {
        factor = ((y & m[i]) == 0 ) ? 1 : 0;
        trailZeros += factor * pow(2,i);
        ++i;
    }
    return (trailZeros+1);
}

1

C ++ 11が利用できる場合、コンパイラーがタスクを実行できることがあります:)

constexpr std::uint64_t lssb(const std::uint64_t value)
{
    return !value ? 0 : (value % 2 ? 1 : lssb(value >> 1) + 1);
}

結果は1ベースのインデックスです。


1
賢いですが、入力がコンパイル時の定数でない場合、それは致命的に悪いアセンブリにコンパイルされます。 godbolt.org/g/7ajMyT。(gccを使用したビットのダムループ、またはclangを使用した実際の再帰関数呼び出し。)gcc / clangはffs()コンパイル時に評価できるため、定数伝播が機能するためにこれを使用する必要はありません。(もちろん、inline-asmを避ける必要があります。)C ++ 11として機能するものが本当に必要な場合constexprでも、GNU Cを使用できます__builtin_ffs
Peter Cordes 2017

0

これは@Anton Tykhyyの回答に関するものです

ここに私のC ++ 11 constexpr実装がキャストを取り除き、64ビットの結果を32ビットに切り捨てることによりVC ++ 17の警告を削除します:

constexpr uint32_t DeBruijnSequence[32] =
{
    0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
    31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
constexpr uint32_t ffs ( uint32_t value )
{
    return  DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

0x1と0x0の両方が0を返す問題を回避するには、次のようにします。

constexpr uint32_t ffs ( uint32_t value )
{
    return (!value) ? 32 : DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

しかし、コンパイラーが呼び出しを前処理できない場合、または行わない場合は、計算に数サイクルが追加されます。

最後に、興味がある場合は、コードが意図したとおりに機能することを確認する静的アサートのリストを次に示します。

static_assert (ffs(0x1) == 0, "Find First Bit Set Failure.");
static_assert (ffs(0x2) == 1, "Find First Bit Set Failure.");
static_assert (ffs(0x4) == 2, "Find First Bit Set Failure.");
static_assert (ffs(0x8) == 3, "Find First Bit Set Failure.");
static_assert (ffs(0x10) == 4, "Find First Bit Set Failure.");
static_assert (ffs(0x20) == 5, "Find First Bit Set Failure.");
static_assert (ffs(0x40) == 6, "Find First Bit Set Failure.");
static_assert (ffs(0x80) == 7, "Find First Bit Set Failure.");
static_assert (ffs(0x100) == 8, "Find First Bit Set Failure.");
static_assert (ffs(0x200) == 9, "Find First Bit Set Failure.");
static_assert (ffs(0x400) == 10, "Find First Bit Set Failure.");
static_assert (ffs(0x800) == 11, "Find First Bit Set Failure.");
static_assert (ffs(0x1000) == 12, "Find First Bit Set Failure.");
static_assert (ffs(0x2000) == 13, "Find First Bit Set Failure.");
static_assert (ffs(0x4000) == 14, "Find First Bit Set Failure.");
static_assert (ffs(0x8000) == 15, "Find First Bit Set Failure.");
static_assert (ffs(0x10000) == 16, "Find First Bit Set Failure.");
static_assert (ffs(0x20000) == 17, "Find First Bit Set Failure.");
static_assert (ffs(0x40000) == 18, "Find First Bit Set Failure.");
static_assert (ffs(0x80000) == 19, "Find First Bit Set Failure.");
static_assert (ffs(0x100000) == 20, "Find First Bit Set Failure.");
static_assert (ffs(0x200000) == 21, "Find First Bit Set Failure.");
static_assert (ffs(0x400000) == 22, "Find First Bit Set Failure.");
static_assert (ffs(0x800000) == 23, "Find First Bit Set Failure.");
static_assert (ffs(0x1000000) == 24, "Find First Bit Set Failure.");
static_assert (ffs(0x2000000) == 25, "Find First Bit Set Failure.");
static_assert (ffs(0x4000000) == 26, "Find First Bit Set Failure.");
static_assert (ffs(0x8000000) == 27, "Find First Bit Set Failure.");
static_assert (ffs(0x10000000) == 28, "Find First Bit Set Failure.");
static_assert (ffs(0x20000000) == 29, "Find First Bit Set Failure.");
static_assert (ffs(0x40000000) == 30, "Find First Bit Set Failure.");
static_assert (ffs(0x80000000) == 31, "Find First Bit Set Failure.");

0

ログを見つけることは少しコストがかかりますが、ここに簡単な代替手段があります。

if(n == 0)
  return 0;
return log2(n & -n)+1;   //Assuming the bit index starts from 1

-3

最近、シンガポールのプレミアが彼がFacebookに書いたプログラムを投稿したことがわかりました。それについて言及する1行があります。

ロジックは単に「値と-値」です。0x0FF0があり、次に0FF0&(F00F + 1)が0x0010に等しいと想定します。これは、最下位の1が4番目のビットにあることを意味します。


1
これは最下位ビットを分離しますが、この質問が求めている位置を提供しません。
橋本

これが最後のビットを見つけるのに役立つとは思いません。
yyny

value&
〜value

おっと、目が悪くなってる。マイナスをチルドと間違えました。私のコメントを無視してください
khw

-8

場合は、リソースを持っている、あなたは速度を向上するために、メモリを犠牲にすることができます:

static const unsigned bitPositions[MAX_INT] = { 0, 0, 1, 0, 2, /* ... */ };

unsigned GetLowestBitPos(unsigned value)
{
    assert(value != 0); // handled separately
    return bitPositions[value];
}

注:このテーブルは少なくとも4 GBを消費します(戻り値の型をのままにした場合は16 GB unsigned)。これは、ある限られたリソース(RAM)を別のリソース(実行速度)と交換する例です。

関数の移植性を維持し、コストをかけずに可能な限り高速に実行する必要がある場合は、この方法が適しています。ほとんどの実際のアプリケーションでは、4GBのテーブルは非現実的です。


1
入力の範囲は既にパラメータータイプによって指定されています-'unsigned'は32ビット値なので、いいえ、あなたは元気ではありません。
ブライアン

3
うーん...あなたの神秘的なシステムとOSはページメモリの概念を持っていますか?その費用はどれくらいかかりますか?
Mikeage 2009

14
これは答えではありません。ソリューションはすべての現実世界のアプリケーションでは完全に非現実的であり、それを「トレードオフ」と呼ぶことは不誠実です。16GBのRAMで単一の機能に専念する神秘的なシステムは存在しません。あなたも「量子コンピュータを使う」と答えていたでしょう。
ブライアン、

3
速度のためにメモリを犠牲にしますか?4GB以上のルックアップテーブルが現在存在するマシンのキャッシュに収まることは決してないので、ここにある他のほとんどすべての回答よりもおそらく遅いでしょう。

1
ああ。この恐ろしい答えは私に:)@Dan を悩ませ続けます:あなたはメモリキャッシングについて正しいです。上記のMikeageのコメントを参照してください。
e.James、2014年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.