整数が既知の値のセットを持つ2つの整数(両端を含む)の間にあるかどうかを判断する最速の方法


389

x >= start && x <= endCまたはC ++ よりも整数が2つの整数の間にあるかどうかをテストするより速い方法はありますか?

更新:私の特定のプラットフォームはiOSです。これは、ピクセルを特定の正方形の円に制限するボックスぼかし関数の一部です。

更新受け入れられた回答を試した後、通常のx >= start && x <= end方法よりも1行のコードで桁違いに高速化しました。

更新:XCodeからのアセンブラーを使用した前後のコードは次のとおりです。

新しい方法

// diff = (end - start) + 1
#define POINT_IN_RANGE_AND_INCREMENT(p, range) ((p++ - range.start) < range.diff)

Ltmp1313:
 ldr    r0, [sp, #176] @ 4-byte Reload
 ldr    r1, [sp, #164] @ 4-byte Reload
 ldr    r0, [r0]
 ldr    r1, [r1]
 sub.w  r0, r9, r0
 cmp    r0, r1
 blo    LBB44_30

古い方法

#define POINT_IN_RANGE_AND_INCREMENT(p, range) (p <= range.end && p++ >= range.start)

Ltmp1301:
 ldr    r1, [sp, #172] @ 4-byte Reload
 ldr    r1, [r1]
 cmp    r0, r1
 bls    LBB44_32
 mov    r6, r0
 b      LBB44_33
LBB44_32:
 ldr    r1, [sp, #188] @ 4-byte Reload
 adds   r6, r0, #1
Ltmp1302:
 ldr    r1, [r1]
 cmp    r0, r1
 bhs    LBB44_36

分岐を削減または排除することで、このような劇的なスピードアップを実現できることは驚くべきことです。


28
これが十分に速くないことをなぜ心配しているのですか?
マットボール

90
誰が理由を気にするのか、それは興味深い質問です。それは挑戦のための単なる挑戦です。
デビッドは、モニカ

46
@SLaksしたがって、このような質問をすべて盲目的に無視し、「オプティマイザに実行させますか?」
デビッドは

87
なぜ質問されるのかは関係ありません。答えが「いいえ」の
tay10r

41
これは私のアプリの1つにある機能のボトルネックです
jjxtra

回答:


527

1つの比較/ブランチだけでこれを行うには古いトリックがあります。速度が本当に向上するかどうかは疑問視される可能性があり、たとえそうであっても、おそらく気づいたり気にかけたりするには少なすぎますが、2つの比較から始めただけでは、大幅な改善の可能性はかなり低いものです。コードは次のようになります。

// use a < for an inclusive lower bound and exclusive upper bound
// use <= for an inclusive lower bound and inclusive upper bound
// alternatively, if the upper bound is inclusive and you can pre-calculate
//  upper-lower, simply add + 1 to upper-lower and use the < operator.
    if ((unsigned)(number-lower) <= (upper-lower))
        in_range(number);

典型的な現代のコンピューター(つまり、2の補数を使用するもの)では、unsignedへの変換は実際にはnopです。同じビットの表示方法が変更されるだけです。

典型的なケースではupper-lower、(推定)ループの外側で事前計算できるため、通常は大幅な時間の増加にはなりません。これにより、分岐命令の数が減るだけでなく、(一般的に)分岐予測も改善されます。この場合、数値が範囲の下端より上か上端かにかかわらず、同じ分岐が行われます。

これがどのように機能するかについては、基本的な考え方はかなり単純です。負の数は、符号なしの数として見た場合、正の数として始まったものよりも大きくなります。

実際には、この方法は、変換numberと原点とのチェックポイントに間隔があればnumber区間である[0, D]場合、D = upper - lowernumber下限を下回る場合:、上限を上回る場合:より大きいD


8
@TomásBadan:合理的なマシンではどちらも1サイクルになります。高価なのはブランチです。
オリバーチャールズワース

3
短絡により追加の分岐が行われますか?この場合、lower <= x & x <= upper(の代わりにlower <= x && x <= upper)パフォーマンスも向上しますか?
Markus Mayr 2013年

6
@ AK4749、jxh:このナゲットと同じくらいかっこいいですが、残念ながら、実際には(これが結果のアセンブラとプロファイリング情報の比較を行うまで)速くなることを示唆するものは何もないためです。知っている限りでは、OPのコンパイラはOPのコードを単一のブランチオペコードでレンダリングする可能性があります...
Oliver Charlesworth

152
ワオ!!!これにより、この特定のコード行について、アプリの桁が大幅に向上しました。上下を事前計算することで、プロファイリングはこの関数の25%の時間から2%未満になりました!ボトルネックは現在、加算と減算の演算ですが、今はそれで十分だと思います:)
jjxtra

28
ああ、@ PsychoDadが質問を更新しました。なぜこれが速いのかは明らかです。実際のコードは、コンパイラが離れて短絡を最適化することができなかった理由は比較に副作用を持っています。
Oliver Charlesworth 2013年

17

そのような小規模でコードを大幅に最適化できることはまれです。パフォーマンスを大幅に向上させるには、上位レベルからコードを監視および変更します。範囲テストを完全に不要にするか、O(n ^ 2)ではなくO(n)のみを実行できる場合があります。不等式の片側が常に暗示されるように、テストを並べ替えることができる場合があります。アルゴリズムが理想的であるとしても、このコードが範囲テストを1000万回実行する方法を見て、それらをバッチ処理し、SSEを使用して多くのテストを並行して実行する方法を見つけると、利益が得られる可能性が高くなります。


16
反対投票にもかかわらず、私は私の答えを支持します:生成されたアセンブリ(承認された回答へのコメントのペーストビンリンクを参照)は、ピクセル処理関数の内部ループにあるものにはかなりひどいものです。受け入れられた答えは巧妙なトリックですが、その劇的な効果は、反復ごとに分岐の一部を排除することを期待するのに妥当なものをはるかに超えています。いくつかの副次的影響が支配的であり、この1つのテストでプロセス全体を最適化しようとする試みは、ダストの賢い範囲比較の利益を残すことが期待されます。
ベンジャクソン

17

同じデータに対してテストを何回実行するかによって異なります。

テストを1回実行する場合、アルゴリズムを高速化する意味のある方法はおそらくありません。

非常に有限な値のセットに対してこれを行う場合は、ルックアップテーブルを作成できます。インデックス作成の実行はコストが高くなる可能性がありますが、テーブル全体をキャッシュに収めることができる場合は、コードからすべての分岐を削除できます。これにより、処理速度が向上します。

データの場合、ルックアップテーブルは128 ^ 3 = 2,097,152になります。3つの変数のいずれかを制御して、一度にすべてのインスタンスを考慮することができる場合start = N、ワーキングセットのサイズは128^2 = 16432バイトに下がり、ほとんどの最新のキャッシュにうまく収まります。

ブランチレスルックアップテーブルが明らかな比較よりも十分に速いかどうかを確認するには、実際のコードをベンチマークする必要があります。


したがって、値、開始、終了を指定して何らかのルックアップを保存し、その間にあるかどうかを通知するBOOLを含めますか?
jjxtra 2013年

正しい。これは3Dルックアップテーブルになりますbool between[start][end][x]。アクセスパターンがどのようになるかがわかっている場合(たとえば、xが単調に増加している場合)、テーブル全体がメモリに収まらない場合でも局所性を維持するようにテーブルを設計できます。
Andrew Prock 2013年

この方法を試してみて、どうなるか見てみます。ポイントが円内にある場合にビットが設定されるラインごとのビットベクトルでそれを行うことを計画しています。ビットマスキングと比較して、byteまたはint32よりも高速になると思いますか?
jjxtra 2013年

2

この回答は、受け入れられた回答で行われたテストについて報告することです。私はソートされたランダムな整数の大きなベクトルに対してクローズドレンジテストを実行しましたが、驚いたことに(low <= num && num <= high)の基本的な方法は実際には上記の受け入れられた回答よりも高速です!テストはHP Pavilion g6(AMD A6-3400APUと6GB RAM)で行われました。テストに使用されたコアコードは次のとおりです。

int num = rand();  // num to compare in consecutive ranges.
chrono::time_point<chrono::system_clock> start, end;
auto start = chrono::system_clock::now();

int inBetween1{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (randVec[i - 1] <= num && num <= randVec[i])
        ++inBetween1;
}
auto end = chrono::system_clock::now();
chrono::duration<double> elapsed_s1 = end - start;

上記で受け入れられた答えである以下と比較してください:

int inBetween2{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (static_cast<unsigned>(num - randVec[i - 1]) <= (randVec[i] - randVec[i - 1]))
        ++inBetween2;
}

randVecはソートされたベクトルであることに注意してください。MaxNumのサイズに関係なく、私のマシンでは最初の方法が2番目の方法よりも優れています!


1
私のデータはソートされておらず、テストはiPhoneアームCPUで行われています。異なるデータとCPUでの結果は異なる場合があります。
jjxtra 2017

私のテストでソートされたのは、上限が下限よりも小さくないことを確認することだけでした。
rezeli 2017

1
ソートされた数値は、分岐予測が非常に信頼できることを意味し、切り替えポイントでいくつかを除いてすべての分岐を正しく行います。ブランチレスコードの利点は、予測不可能なデータに対するこの種の予測ミスを取り除くことです。
Andreas Klebinger

0

変数の範囲をチェックする場合:

if (x >= minx && x <= maxx) ...

ビット操作を使用する方が高速です。

if ( ((x - minx) | (maxx - x)) >= 0) ...

これにより、2つのブランチが1つに削減されます。

タイプセーフに関心がある場合:

if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...

より多くの変数範囲チェックを組み合わせることができます。

if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...

これにより、4つのブランチが1つに削減されます。

これはgccの古いものより3.4倍高速です。

ここに画像の説明を入力してください


-4

整数に対してビット演算を実行することは不可能ですか?

0から128の間でなければならないため、8番目のビットが設定されている場合(2 ^ 7)、128以上になります。ただし、包括的な比較が必要なため、エッジケースは面倒です。


3
彼はx <= end、どこにあるのか知りたいのend <= 128です。ないx <= 128
Ben Voigt 2013年

1
このステートメントは、「0から128の間でなければならないため、8番目のビットが設定されている場合(2 ^ 7)、128以上である」とは間違っています。256考えてみましょう
ハッピーグリーンキッド昼寝

1
ええ、どうやら私はそれを十分に考えていませんでした。ごめんなさい。
icedwater 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.