整数の1ビットが連続した領域にあるかどうかをテストするためのエレガントで高速な方法はありますか?


84

ビット値1の位置(32ビット整数の場合は0から31)が連続領域を形成するかどうかをテストする必要があります。例えば:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

このテスト、つまりいくつかの関数has_contiguous_one_bits(int)を移植可能にしたいと思います。

明らかな方法の1つは、位置をループして最初のセットビットを見つけ、次に最初の非セットビットを見つけて、それ以上セットビットがないかどうかを確認することです。

もっと速い方法があるのだろうか?最高と最低のセットビットを見つけるための高速な方法がある場合(しかし、この質問から、ポータブルなものはないようです)、可能な実装は次のとおりです。

bool has_contiguous_one_bits(int val)
{
    auto h = highest_set_bit(val);
    auto l = lowest_set_bit(val);
    return val == (((1 << (h-l+1))-1)<<l);
}

楽しみのために、ここに連続したビットを持つ最初の100個の整数があります:

0 1 2 3 4 6 7 8 12 14 15 16 24 28 30 31 32 48 56 60 62 63 64 96 112 120 124 126 127 128 192 224 240 248 252 254 255 256 384 448 480 496 504 508 510 511 512 768 896 960 992 1008 1016 1020 1022 1023 1024 1536 1792 1920 1984 2016 2032 2040 2044 2046 2047 2048 3072 3584 3840 3968 4032 4064 4080 4088 4092 4094 4095 4096 6144 7168 7680 7936 8064 8128 8160 8176 8184 8188 8190 8191 8192 12288 14336 15360 15872 16128 16256 16320

それらは(もちろん)(1<<m)*(1<<n-1)非負mn。の形式です。


4
@aafuleiはい、0x0コンパクトです。反対の(コンパクトではない)定義は簡単です。2つのセットビットがある場合、それらの間に少なくとも1つの未セットビットがあります。
ウォルター

1
@KamilCuk h>=l(暗黙)の機能により、highest_set_bit()及びlowest_set_bit()
ウォルター


6
そのOEISリンクは、これらの数値が2進数の場合、数字が増加しないことを示しています。それらを参照する別の方法は、それらが隣接している(またはおそらく接続されている)と言うことです。この数学者にとって、「コンパクト」とは非常に異なるものを意味します。
Teepeemm

1
@Teepeemmこの質問がホットネットワークの質問に終わった理由の1つは、コンパクトという言葉の誤用によるものだと思います。それが確かに私がそれをクリックした理由です。私はあまり考えていなかったので、コンパクトを定義することがどのように意味があるのか​​疑問に思いました。そのように。明らかにそれは意味がありません。
誰も

回答:


146
static _Bool IsCompact(unsigned x)
{
    return (x & x + (x & -x)) == 0;
}

簡単に:

x & -x設定された最下位ビットを提供しますx(またはゼロの場合xはゼロ)。

x + (x & -x) 連続する1の最下位の文字列を単一の1に変換します(またはゼロにラップします)。

x & x + (x & -x) それらの1ビットをクリアします。

(x & x + (x & -x)) == 0 他の1ビットが残っているかどうかをテストします。

より長いです:

-xに等しい~x+1、2の補数を使用します。ビットがフリップインされた後、~x1キャリーを追加すると、下位1ビット~xと最初の0ビットがフリップバックされますが、その後停止します。したがって、-x最初の1までの下位ビットはの下位ビットと同じxですが、上位ビットはすべて反転されます。(例:~10011100を与え01100011、1を加えると、を与える01100100ので、ロー100は同じですが、ハイ10011はに反転し01100ます。)次にx & -x、両方で1である唯一のビット、つまり最も低い1ビット(00000100)を与えます。(xがゼロの場合、x & -xはゼロです。)

これをに追加するxと、連続するすべての1がキャリースルーされ、0に変更されます。次に高い0ビットに1を残します(またはハイエンドをキャリースルーし、ラップされた合計をゼロのままにします)(10100000。)

これをとANDするとx、1が0に変更された場所(およびキャリーが0から1に変更された場所)に0があります。したがって、さらに1ビット上にある場合にのみ、結果はゼロではありません。


23
少なくとも誰かがハッカーのたのしみという本を知っています。答えは2-1章をご覧ください。しかし、これはすでにここSOで何度か回答されています。とにかく:+1
ArminMontigny20年

33
このようなコードを本番
環境で作成

14
これは、x86 BMI1から、Intelでは1 uop、AMDZenでは2uopのx & -x単一のblsi命令で実行できるという利点があります。 godbolt.org/z/5zBx-A。しかし、BMI1がなければ、@ KevinZのバージョンはさらに効率的です。
PeterCordes20年

3
@TommyAndersen:_Bool標準キーワードがあり、C 2018 6.4.1 1.につき
エリックPostpischil

1
@ウォルター:うーん?このコードはを使用しunsignedます。2の補数の符号付きのテストを実行する場合int、最も簡単な方法は、この回答のルーチンに渡すだけで、をintに変換することunsignedです。それは望ましい結果を与えるでしょう。intオーバーフロー/キャリーの問題により、署名された直接に操作ショーを適用すると問題が発生する可能性があります。(1の補数または符号と大きさをテストしたい場合int、それは別の問題であり、主に最近の理論的関心のみです。)
EricPostpischil20年

29

実際には、組み込み関数を使用する必要はありません。

最初に最初の1の前にすべての0を反転します。次に、新しい値がメルセンヌ数であるかどうかをテストします。このアルゴリズムでは、ゼロは真にマップされます。

bool has_compact_bits( unsigned const x )
{
    // fill up the low order zeroes
    unsigned const y = x | ( x - 1 );
    // test if the 1's is one solid block
    return not ( y & ( y + 1 ) );
}

もちろん、組み込み関数を使用する場合は、popcountメソッドを次に示します。

bool has_compact_bits( unsigned const x )
{
    size_t const num_bits = CHAR_BIT * sizeof(unsigned);
    size_t const sum = __builtin_ctz(x) + __builtin_popcount(x) + __builtin_clz(z);
    return sum == num_bits;
}

2
最初のバージョン-mtbmblsfill/blcfill命令を利用してコンパイルされた場合、4命令のみに削減されます。これまでに提案された最短バージョンになります。残念ながら、その命令セット拡張をサポートするプロセッサほとんどありません
ジョバンニセレタニ

18

実際には、先行ゼロをカウントする必要はありません。コメントのpmgで示唆されているように、探している数値がシーケンスOEIS A023758の数値、つまり2 ^ i-2 ^ jの形式でi> = jの数値であるという事実を利用して、末尾のゼロを数えることができます(つまり、j --1)、元の値のビットを切り替えて(2 ^ j -1を追加するのと同じ)、その値が2 ^ i-1の形式であるかどうかを確認します。GCC / clang組み込み関数を使用すると、

bool has_compact_bits(int val) {
    if (val == 0) return true; // __builtin_ctz undefined if argument is zero
    int j = __builtin_ctz(val) + 1;
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

このバージョン、あなたのバージョンよりもわずかに高速で、KamilCukによって提案されたバージョンと、ポップカウントのみのYuriFeldmanによって提案されたバージョンです。

あなたがC ++ 20を使用している場合は、交換することにより、ポータブル機能を得ることができます__builtin_ctzstd::countr_zero

#include <bit>

bool has_compact_bits(int val) {
    int j = std::countr_zero(static_cast<unsigned>(val)) + 1; // ugly cast
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

キャストは醜いですが、ビットを操作するときは符号なしタイプを使用する方がよいことを警告しています。C ++ 20より前の代替手段はboost::multiprecision::lsbです。

編集:

取り消し線リンクのベンチマークは、YuriFeldmanバージョンのポップカウント命令が発行されなかったという事実によって制限されていました。を使用してPCでコンパイルしようとすると-march=westmere、次の10億回の反復で、次の同じシーケンスを使用して次の時間を測定しましたstd::mt19937

  • あなたのバージョン:5.7秒
  • KamilCukの2番目のバージョン:4.7秒
  • 私のバージョン:4.7秒
  • Eric Postpischilの最初のバージョン:4.3秒
  • ユリフェルドマンのバージョン(明示的に使用__builtin_popcount):4.1秒

したがって、少なくとも私のアーキテクチャでは、最速はポップカウントのあるもののようです。

編集2:

新しいEricPostpischilのバージョンでベンチマークを更新しました。コメントで要求されているように、私のテストのコードはここにあります。PRNGに必要な時間を見積もるために、no-opループを追加しました。KevinZによる2つのバージョンも追加しました。コードはclangでコンパイルされ-O3 -msse4 -mbmi、取得popcntblsi命令が行われます(PeterCordesに感謝)。

結果:少なくとも私のアーキテクチャでは、EricPostpischilのバージョンはYuriFeldmanのバージョンとまったく同じ速度であり、これまでに提案された他のバージョンよりも少なくとも2倍高速です。


操作を削除しました:return (x & x + (x & -x)) == 0;
EricPostpischil20年

3
これは、@ Ericのバージョンの古いバージョンのベンチマークですよね?現在のバージョンでは、わずかで命令にエリックのコンパイルgcc -O3 -march=nehalem(POPCNTを利用できるようにする)、またはBMI1があれば、より少ないblsiために利用可能であるx & -xgodbolt.org/z/zuyj_f。そして、popcnt3サイクルのレイテンシーを持つYuriのバージョンを除いて、命令はすべて単純なシングルuopです。(しかし、あなたはスループットをベンチングしていたと思います。)またand val、Yuriから削除したに違いないと思います。そうしないと遅くなります。
PeterCordes20年

2
また、どのハードウェアでベンチマークを行いましたか?ベンチマークコード全体をGodboltなどにリンクすることをお勧めします。そうすれば、将来の読者はC ++の実装を簡単にテストできます。
PeterCordes20年

2
@KevinZのバージョンもテストする必要があります。BMI1なしでさらに少ない命令にコンパイルされます(少なくともclangでは; gccの非インラインバージョンはamovを浪費し、利用できませんlea): godbolt.org/z/5jeQLQ。 BMI1、エリックのバージョンは、少なくともインテルに、x86-64の上でまだ良いです単一のuopであるが、それはAMDの2つのuopです。blsi
PeterCordes20年

15

高速かどうかはわかりませんval^(val>>1)が、最大2ビットがオンになっていることを確認することでワンライナーを実行できます。

これは、符号なしタイプでのみ機能し0ます。符号ビットのコピーをシフトする算術右シフトではなく、先頭のシフト(論理シフト)が必要です。

#include <bitset>
bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2;
}

拒否するには0(つまり、連続するビットグループが1つだけある入力のみを受け入れる)、論理積をvalゼロ以外にします。この質問に対する他の回答は0コンパクトとして受け入れます。

bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2 and val;
}

C ++はstd::bitset::count()、を介して、またはC ++ 20ではstd::popcountを介してpopcountを移植可能に公開ます。Cには、利用可能なターゲットでpopcntまたは同様の命令に確実にコンパイルする移植可能な方法がまだありません。


2
また、これまでのところ最速です。
ジョバンニセレタニ

2
符号ビットのコピーではなく、ゼロにシフトすることを確認するには、符号なし型を使用する必要があると思います。考えてみてください11011111。算術右シフトすると11101111、になり、XORは00110000。論理右シフト(上部のシフト010110000を使用すると、複数のビットグループを取得して正しく検出できます。それを修正するための編集。
PeterCordes20年

3
これは本当に賢いです。私がスタイルを嫌うのと同じくらい(IMOはただ使用します__builtin_popcount()、すべてのコンパイラは今日そのようなプリミティブを持っています)、これは(現代のCPUで)はるかに速いです。実際、POPCNTを単一の命令として持たないCPUでは、私の実装がこれを上回る可能性があるため、そのプレゼンテーションは非常に重要であると主張します。したがって、この実装を使用する場合は、組み込み関数を使用する必要があります。std::bitsetひどいインターフェースを持っています。
KevinZ

9

CPUには、そのための専用の命令が非常に高速にあります。PCではBSR / BSF(1985年に80386で導入)、ARMではCLZ / CTZです。

1を使用して最下位のセットビットのインデックスを見つけ、その量だけ整数を右にシフトします。別のものを使用して最上位のセットビットのインデックスを見つけ、整数を(1u <<(bsr + 1))-1と比較します。

残念ながら、ハードウェアに合わせてC ++言語を更新するには35年では不十分でした。C ++からこれらの命令を使用するには、組み込み関数が必要です。これらは移植性がなく、わずかに異なる形式で結果を返します。プリプロセッサ#ifdefなどを使用してコンパイラを検出し、適切な組み込み関数を使用します。彼らはMSVCでは_BitScanForward_BitScanForward64_BitScanReverse_BitScanReverse64。GCCとclangでは、それらは__builtin_clz__builtin_ctzです。


2
@ e2-e4 Visual Studioは、AMD64用にコンパイルする場合のインラインアセンブリをサポートしていません。そのため、組み込み関数をお勧めします。
20

5
C ++ 20以降std::countr_zero、とがありstd::countl_zeroます。Boostを使用している場合は、boost::multiprecision::lsbandと呼ばれるポータブルラッパーがありboost::multiprecision::msbます。
ジョバンニセレタニ

8
これは私の質問にまったく答えません-なぜそれが賛成票を得たのだろうか
Walter

3
@ウォルター「答えない」とはどういう意味ですか?私はあなたが何をすべきかを正確に答えました。プリプロセッサを使用し、次に組み込み関数を使用します。
20

2
どうやらC ++ 20はついに#include <bit> en.cppreference.com/w/cpp/header/bitをビットスキャン、ポップカウント、ローテーションで追加しているようです。ビットスキャンを移植可能に公開するのにこれほど長い時間がかかったことは哀れですが、今ではかつてないほど良くなっています。(ポータブルpopcntはから入手できますstd::bitset::count()。)C ++ 20には、Rustが提供するもの(doc.rust-lang.org/std/primitive.i32.html)がまだありません。たとえば、一部のCPUが効率的に提供するビットリバースやエンディアンなどです。すべてではありません。ユーザーは何が高速かを知る必要がありますが、CPUが持つ操作のためのポータブルビルトインはある程度意味があります。
PeterCordes20年

7

1ではなく0と比較すると、一部の操作が節約されます。

bool has_compact_bits2(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    // Clear bits to the left
    val = (unsigned)val << h;
    int l = __builtin_ctz(val);
    // Invert
    // >>l - Clear bits to the right
    return (~(unsigned)val)>>l == 0;
}

次の結果はgcc10 -O3、x86_64で上記よりも少ない命令になり、符号拡張で使用します。

bool has_compact_bits3(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    val <<= h;
    int l = __builtin_ctz(val);
    return ~(val>>l) == 0;
}

godboltでテスト済み。


残念ながら、これはポータブルではありません。私はいつも、これらのシフト演算子で演算子の優先順位が間違っているの~val<<h>>h>>l == 0ではないかと心配しています-あなたが思っていることを本当にやっていますか?
ウォルター

4
はい、とにかく中括弧を編集して追加したことは確かです。さて、あなたはポータブルソリューションに興味がありますか?私が見てthere exists a faster way?、何でも行くと思ったからです。
KamilCuk

5

要件を言い換えることができます。

  • Nを前のビットとは異なるビット数に設定します(ビットを反復処理することにより)
  • N = 2で、最初または最後のビットが0の場合、答えは「はい」です。
  • N = 1の場合、答えは「はい」です(すべての1が片側にあるため)
  • N = 0で、任意のビットが0の場合、1はありません。答えが「はい」または「いいえ」であると考える場合は、あなた次第です。
  • 他の何か:答えはノーです

すべてのビットを通過すると、次のようになります。

unsigned int count_bit_changes (uint32_t value) {
  unsigned int bit;
  unsigned int changes = 0;
  uint32_t last_bit = value & 1;
  for (bit = 1; bit < 32; bit++) {
    value = value >> 1;
    if (value & 1 != last_bit  {
      changes++;
      last_bit = value & 1;
    }
  }
  return changes;
}

ただし、これは確実に最適化できます(たとえばforvalue到達時にループを中止することにより0、値1の重要なビットが存在しなくなります)。


3

この一連の計算を実行できます(val入力として想定)。

uint32_t x = val;
x |= x >>  1;
x |= x >>  2;
x |= x >>  4;
x |= x >>  8;
x |= x >> 16;

最も重要な1ものより下のすべてのゼロが1で埋められた数値を取得します。

また、計算することができますy = val & -valの最下位1ビットを除くすべてを除去するためにval(例えば、7 & -7 == 1および12 & -12 == 4)。
警告:これはで失敗するためval == INT_MIN、このケースを個別に処理する必要がありますが、これはすぐに実行されます。

次にy、1つの位置だけ右シフトして、の実際のLSBを少し下回り、次のval場合と同じルーチンを実行しxます。

uint32_t y = (val & -val) >> 1;
y |= y >>  1;
y |= y >>  2;
y |= y >>  4;
y |= y >>  8;
y |= y >> 16;

次にx - yx & ~yまたはx ^ yまたはは、の全長にまたがる「コンパクト」ビットマスクを生成しvalます。それを比較してvalval「コンパクト」かどうかを確認してください。


2

私たちは、を利用することができますgccの組み込み関数命令かどうかを確認するために:

セットビットの数

int __builtin_popcount(unsigned int x)x
の1ビットの数を返します。

(a --b)に等しい:

a:最上位のセットビットのインデックス(32-CTZ)(符号なし整数の32ビットのため32)。

int __builtin_clz(unsigned int x)
最上位ビット位置から始まるxの先頭の0ビットの数を返します。xが0の場合、結果は未定義です。

b:最下位セットビット(CLZ)のインデックス:

int __builtin_clz(unsigned int x)
最上位ビット位置から始まるxの先頭の0ビットの数を返します。xが0の場合、結果は未定義です。

たとえば、n = 0b0001100110の場合。popcountで4を取得しますが、インデックスの差(a --b)は6を返します。

これは次のように書くこともできます:

私はそれが現在最も賛成されている答えよりもエレガントで効率的だとは思いません:

次のアセンブリで:

mov     eax, edi
neg     eax
and     eax, edi
add     eax, edi
test    eax, edi
sete    al

しかし、おそらく理解しやすいでしょう。


1

さて、これはビットをループするバージョンです

template<typename Integer>
inline constexpr bool has_compact_bits(Integer val) noexcept
{
    Integer test = 1;
    while(!(test & val) && test) test<<=1; // skip unset bits to find first set bit
    while( (test & val) && test) test<<=1; // skip set bits to find next unset bit
    while(!(test & val) && test) test<<=1; // skip unset bits to find an offending set bit
    return !test;
}

最初の2つのループは、最初のコンパクト領域を見つけました。最後のループは、その領域を超えて他のセットビットがあるかどうかをチェックします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.