C配列に値が存在するかどうかをすばやく見つけるには?


124

サイズが256(できれば1024、ただし256が最小)の配列を反復処理し、値が配列の内容と一致するかどうかを確認する必要のある、タイムクリティカルなISRを備えた組み込みアプリケーションがあります。boolこの場合、A はtrueに設定されます。

マイクロコントローラーはNXP LPC4357、ARM Cortex M4コア、コンパイラーはGCCです。私はすでに最適化レベル2(3は遅い)を組み合わせ、フラッシュではなくRAMに関数を配置しています。また、ポインター演算とforループを使用します。これは、アップではなくダウンカウントを行います(かどうかのチェックi!=0は、かどうかのチェックよりも高速ですi<256)。結局のところ、私は12.5 µsの持続時間で終了していますが、これは実現可能にするために大幅に削減する必要があります。これは私が今使っている(疑似)コードです:

uint32_t i;
uint32_t *array_ptr = &theArray[0];
uint32_t compareVal = 0x1234ABCD;
bool validFlag = false;

for (i=256; i!=0; i--)
{
    if (compareVal == *array_ptr++)
    {
         validFlag = true;
         break;
     }
}

これを行う最も速い方法は何でしょうか?インラインアセンブリの使用が許可されています。他の「あまりエレガントでない」トリックも許可されています。


28
配列に異なる値を格納する方法はありますか?それらを並べ替えることができれば、バイナリ検索は確かに高速になります。格納され、検索対象のデータが一定の範囲内にある場合、それらは等、ビットマップと表現かもしれない
Remo.D

20
@BitBank:過去30年間でコンパイラーがどれだけ改善されたかに驚くでしょう。ARMは特にコンパイラに非常に適しています。そして、GCC上のARMが複数のロード命令を発行できることを知っています(少なくとも2009年以降)
MSalters

8
素晴らしい質問です。人々は、パフォーマンスが重要な実際のケースがあることを忘れています。このような質問に対する回答が「stlを使用する」で何度も返される
Kik

14
タイトル「...配列を反復処理する」は誤解を招く可能性があります。実際、単に指定された値を検索しているだけだからです。配列を反復処理することは、各エントリで何かを行うことを意味します。ソートは、コストが多くの検索で償却できる場合、言語実装の問題とは関係なく、確かに効率的なアプローチです。
hardmath

8
バイナリ検索やハッシュテーブルを単純に使用できないことを確信していますか?256項目のバイナリ検索== 8比較。ハッシュテーブル==平均で1ジャンプ(完全なハッシュがある場合は最大 1ジャンプ)。アセンブリの最適化に頼るのは、1)まともな検索アルゴリズム(O(1)またはとO(logN)比較してO(N))を使用し、2)それをボトルネックとしてプロファイルした後である。
Groo

回答:


105

パフォーマンスが最も重要である状況では、Cコンパイラーは、手動で調整されたアセンブリ言語で実行できるものと比較して、最速のコードを生成しない可能性があります。私は最も抵抗の少ない経路をとる傾向があります-このような小さなルーチンの場合、私はasmコードを書くだけで、実行に何サイクルかかるかを理解しています。Cコードをいじってコンパイラに適切な出力を生成させることはできるかもしれませんが、そのようにして出力を調整することに多くの時間を費やすことになるかもしれません。コンパイラ(特にMicrosoft製)はここ数年で大きな進歩を遂げましたが、一般的なケースだけでなく、特定の状況に取り組んでいるため、耳の間のコンパイラほどスマートではありません。コンパイラは、これをスピードアップできる特定の命令(LDMなど)を使用しない場合があります。ループを展開するのに十分スマートである可能性は低いです。これは、コメントで述べた3つのアイデアを組み込んだ方法です。ループのアンロール、キャッシュのプリフェッチ、および複数のロード(ldm)命令の使用。命令サイクルカウントは、配列要素ごとに約3クロックになりますが、これはメモリ遅延を考慮していません。

動作理論: ARMのCPUデザインは1クロックサイクルでほとんどの命令を実行しますが、命令はパイプラインで実行されます。Cコンパイラーは、間に他の命令をインターリーブすることにより、パイプライン遅延を排除しようとします。元のCコードのようなタイトなループが提示されると、メモリから読み取られた値をすぐに比較する必要があるため、コンパイラは遅延を隠すのに苦労します。以下のコードでは、4つのレジスタの2つのセットを交互に使用して、メモリ自体とデータをフェッチするパイプラインの遅延を大幅に削減しています。一般に、大きなデータセットを使用していて、コードが使用可能なレジスタのほとんどまたはすべてを使用していない場合、最大のパフォーマンスは得られません。

; r0 = count, r1 = source ptr, r2 = comparison value

   stmfd sp!,{r4-r11}   ; save non-volatile registers
   mov r3,r0,LSR #3     ; loop count = total count / 8
   pld [r1,#128]
   ldmia r1!,{r4-r7}    ; pre load first set
loop_top:
   pld [r1,#128]
   ldmia r1!,{r8-r11}   ; pre load second set
   cmp r4,r2            ; search for match
   cmpne r5,r2          ; use conditional execution to avoid extra branch instructions
   cmpne r6,r2
   cmpne r7,r2
   beq found_it
   ldmia r1!,{r4-r7}    ; use 2 sets of registers to hide load delays
   cmp r8,r2
   cmpne r9,r2
   cmpne r10,r2
   cmpne r11,r2
   beq found_it
   subs r3,r3,#1        ; decrement loop count
   bne loop_top
   mov r0,#0            ; return value = false (not found)
   ldmia sp!,{r4-r11}   ; restore non-volatile registers
   bx lr                ; return
found_it:
   mov r0,#1            ; return true
   ldmia sp!,{r4-r11}
   bx lr

更新: 私の経験は逸話的/価値がなく、証拠が必要であると考えるコメントには多くの懐疑論者がいます。GCC 4.8(Android NDK 9Cから)を使用して、最適化-O2(ループの展開を含むすべての最適化をオンにした)を使用して次の出力を生成しました。上記の質問で提示された元のCコードをコンパイルしました。GCCが生成したものは次のとおりです。

.L9: cmp r3, r0
     beq .L8
.L3: ldr r2, [r3, #4]!
     cmp r2, r1
     bne .L9
     mov r0, #1
.L2: add sp, sp, #1024
     bx  lr
.L8: mov r0, #0
     b .L2

GCCの出力はループを展開しないだけでなく、LDR後のストールでクロックを浪費します。配列要素ごとに少なくとも8クロックが必要です。ループを終了するタイミングを知るためにアドレスを使用することはうまくいきますが、コンパイラーが実行できるすべての不思議なことは、このコードにはありません。ターゲットプラットフォームでコードを実行していません(私はコードを所有していません)が、ARMコードパフォーマンスの経験者なら誰でも私のコードの方が速いことがわかります。

更新2: MicrosoftのVisual Studio 2013 SP2に、コードをより効果的に使用する機会を与えました。NEON命令を使用して配列の初期化をベクトル化できましたが、OPによって書き込まれた線形値の検索は、GCCが生成したものと同様になりました(読みやすくするためにラベルの名前を変更しました)。

loop_top:
   ldr  r3,[r1],#4  
   cmp  r3,r2  
   beq  true_exit
   subs r0,r0,#1 
   bne  loop_top
false_exit: xxx
   bx   lr
true_exit: xxx
   bx   lr

私が言ったように、私はOPの正確なハードウェアを所有していませんが、3つの異なるバージョンのnVidia Tegra 3およびTegra 4でパフォーマンスをテストし、すぐに結果をここに投稿します。

更新3: コードとMicrosoftのコンパイル済みARMコードをTegra 3およびTegra 4(Surface RT、Surface RT 2)で実行しました。ループの1000000回の反復を実行しましたが、一致が見つからないため、すべてがキャッシュにあり、測定が簡単です。

             My Code       MS Code
Surface RT    297ns         562ns
Surface RT 2  172ns         296ns  

どちらの場合でも、コードはほぼ2倍の速度で実行されます。最近のARM CPUのほとんどは、おそらく同様の結果をもたらすでしょう。


13
@LưuVĩnhPhúc-これは一般的には真実ですが、タイトなISRは、コンパイラよりも多くのことをよく知っているという点で、最大の例外の1つです。
sapi 2014

47
悪魔の支持者:このコードが高速であるという定量的な証拠はありますか?
オリバーチャールズワース2014

11
@BitBank:それは十分ではありません。あなたはあなたの主張を証拠でバックアップしなければなりません。
オービットのライトネスレース2014年

13
私は何年も前に私のレッスンを学びました。UパイプとVパイプを最適に使用して、Pentiumのグラフィックルーチン用に驚くほど最適化された内部ループを作成しました。ループごとに6クロックサイクル(計算および測定)になり、私は自分自身を非常に誇りに思いました。Cで書かれた同じものに対してそれをテストしたとき、Cはより高速でした。Intelアセンブラーの別の行を書いたことはありません。
Rocketmagnet 2014

14
「私の経験は逸話的/価値がなく、証拠が必要だと思うコメントの懐疑論者」彼らのコメントを過度に否定的にしないでください。証明を表示することは、あなたの素晴らしい答えをはるかに良くするだけです。
Cody Grey

87

それを最適化するためのトリックがあります(私は就職の面接でこれを一度尋ねられました):

  • 配列の最後のエントリが探している値を保持している場合は、trueを返します
  • 探している値を配列の最後のエントリに書き込みます
  • 探している値が見つかるまで配列を繰り返します
  • 配列の最後のエントリの前に遭遇した場合は、trueを返します
  • falseを返す

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    uint32_t x = theArray[SIZE-1];
    if (x == compareVal)
        return true;
    theArray[SIZE-1] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    theArray[SIZE-1] = x;
    return i != SIZE-1;
}

これにより、反復ごとに2つのブランチではなく、反復ごとに1つのブランチが生成されます。


更新:

配列を割り当てることが許可されている場合 SIZE+1、「最後のエントリの交換」の部分を取り除くことができます。

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    theArray[SIZE] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    return i != SIZE;
}

に埋め込まれている追加の演算を取り除くこともできます theArray[i]代わりに次のコマンドを使用して、にます。

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t *arrayPtr;
    theArray[SIZE] = compareVal;
    for (arrayPtr = theArray; *arrayPtr != compareVal; arrayPtr++);
    return arrayPtr != theArray+SIZE;
}

コンパイラがまだ適用していない場合、この関数は確実に適用します。一方、ループをアンロールするのがオプティマイザで難しくなる可能性があるため、生成されたアセンブリコードでそれを確認する必要があります...


2
@ratchetfreak:OPは、この配列がどのように、どこで、いつ割り当てられ、初期化されるかについての詳細を提供しないため、それに依存しない答えを出しました。
barak manos 14

3
配列はRAMにありますが、書き込みは許可されていません。
2014

1
いいですが、配列はもはやconstではないため、これはスレッドセーフではありません。支払うのに高額のようです。
EOF

2
@EOF:const質問のどこで言及されましたか?
barak manos 2014

4
@barakmanos:配列と値を渡して、その値が配列内にあるかどうかを尋ねた場合、通常は配列を変更するとは思いません。元の質問はconstスレッドもスレッドも言及していませんが、この警告に言及するのは公平だと思います。
EOF 2014

62

アルゴリズムを最適化するための支援を求めているので、アセンブラーにプッシュされる可能性があります。ただし、アルゴリズム(線形検索)はそれほど賢くないため、アルゴリズムの変更を検討する必要があります。例えば:

完璧なハッシュ関数

256の「有効な」値が静的で、コンパイル時にわかっている場合は、完全なハッシュ関数を使用できます。入力値を0からnの範囲の値にマップするハッシュ関数を見つける必要があります。この場合、気になるすべての有効な値の衝突はありません。つまり、2つの「有効な」値が同じ出力値にハッシュされることはありません。良いハッシュ関数を探すときは、次のことを目指します。

  • ハッシュ関数を適度に高速に保ちます。
  • nを最小化します。取得できる最小値は256(最小の完全ハッシュ関数)ですが、データによっては、実現するのが難しい場合があります。

効率的なハッシュ関数については、nはしばしば2の累乗であることに注意してください。これは下位ビットのビット単位のマスク(AND演算)と同等です。ハッシュ関数の例:

  • 入力バイトのCRC、モジュロn
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n(多くのよう摘みijk、...、必要に応じて、左または右シフト付き)

次に、nエントリの固定テーブルを作成します。ハッシュでは、入力値をテーブルのインデックスiにマッピングします。有効な値の場合、テーブルエントリiには有効な値が含まれます。他のすべてのテーブルエントリのために、インデックスの各エントリがあることを確認し、私はにハッシュしない他のいくつかの無効な値が含まれている私を

次に、割り込みルーチンで、入力xを使用します。

  1. xをインデックスiにハッシュします(範囲は0..nです)。
  2. テーブルのエントリiを検索し、値xが含まれているかどうかを確認します。

これは、256または1024値の線形検索よりもはるかに高速です。

適切なハッシュ関数を見つけるために、Pythonコードいくつか作成しました。

二分探索

256の「有効な」値の配列をソートすると、線形検索ではなく、バイナリ検索を実行できます。つまり、256エントリのテーブルを8ステップ(log2(256))で、または1024エントリのテーブルを10ステップで検索できるはずです。繰り返しますが、これは256または1024値の線形検索よりもはるかに高速です。


それをありがとう。バイナリ検索オプションは私が選択したものです。最初の投稿の以前のコメントも参照してください。これは、アセンブリを使用せずに非常にうまく機能します。
wlamers 2014

11
実際、コードを最適化する前に(アセンブリやその他のトリックを使用するなど)、アルゴリズムの複雑さを軽減できるかどうかを確認する必要があります。通常、アルゴリズムの複雑さを減らすことは、数サイクルを削減しようとするよりも同じアルゴリズムの複雑さを維持するよりも効率的です。
ysdx 2014

3
バイナリ検索の場合は+1。アルゴリズムによる再設計が最適化の最良の方法です。
Rocketmagnet 2014

よく使われる概念は、効率的なハッシュルーチンを見つけるのに手間がかかりすぎるため、「ベストプラクティス」はバイナリ検索であるというものです。ただし、「ベストプラクティス」では不十分な場合もあります。パケットのヘッダーが到着した瞬間に(そのペイロードではなく)ネットワークトラフィックをその場でルーティングしているとします。組み込み製品には通常、たとえばx86実行環境での「ベストプラクティス」とは、組み込みで「簡単な方法」を採用するという制約と要件があります。
Olof Forshell、2015年

60

テーブルをソートされた順序に保ち、Bentleyの展開されたバイナリ検索を使用します。

i = 0;
if (key >= a[i+512]) i += 512;
if (key >= a[i+256]) i += 256;
if (key >= a[i+128]) i += 128;
if (key >= a[i+ 64]) i +=  64;
if (key >= a[i+ 32]) i +=  32;
if (key >= a[i+ 16]) i +=  16;
if (key >= a[i+  8]) i +=   8;
if (key >= a[i+  4]) i +=   4;
if (key >= a[i+  2]) i +=   2;
if (key >= a[i+  1]) i +=   1;
return (key == a[i]);

ポイントは、

  • テーブルの大きさがわかっていれば、反復の数がわかるので、完全に展開できます。
  • 次に、==最後の反復を除いて、そのケースの確率が低すぎてケースの時間テストを正当化できないため、各イテレーションのケースのポイントテストはありません。**
  • 最後に、テーブルを2の累乗に拡張することにより、最大で1つの比較を追加し、最大で2倍のストレージを追加します。

**確率について考えることに慣れていない場合は、すべての決定点にエントロピーがあります。これは、エントロピーを実行することによって得られる平均的な情報です。のために>=のテスト、各分岐の確率は0.5程度であり、-log2(0.5)を使用すると、一方のブランチを取る場合の手段は、あなたが1ビットを学び、あなたが他のブランチを取る場合は、1つのビットを学ぶように、1であり、平均各ブランチで学んだこととそのブランチの確率の合計です。したがって1*0.5 + 1*0.5 = 1>=テストのエントロピーは1です。学習するビットは10であるため、10の分岐が必要です。それが速い理由です!

一方、最初のテストが次の場合はどうなりますif (key == a[i+512)か?真である確率は1/1024、偽である確率は1023/1024です。だからそれが本当なら10ビットすべてを学ぶ!しかし、それがfalseの場合、-log2(1023/1024)= .00141ビットを学習し、実際には何も起こりません。したがって、そのテストから学ぶ平均量は10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112ビットです。約100分の1です。 そのテストはその重みを担っていません!


4
私はこのソリューションが本当に好きです。値の場所が機密情報である場合は、タイミングベースのフォレンジックを回避するために、一定のサイクルで実行するように変更できます。
OregonTrail 2014

1
@OregonTrail:タイミングベースのフォレンジック?楽しい問題ですが、悲しいコメントです。
Mike Dunlavey 2014

16
タイミングライブラリのen.wikipedia.org/wiki/Timing_attackを防ぐために、暗号ライブラリにこのような展開されたループが表示されます。これが良い例ですgithub.com/jedisct1/libsodium/blob/…この場合、攻撃者が文字列の長さを推測できないようにしています。通常、攻撃者はタイミング攻撃を実行するために関数呼び出しの数百万サンプルを取得します。
OregonTrail 2014

3
+1すごい!素敵な展開された検索。今まで見たことがない。私はそれを使うかもしれません。
Rocketmagnet 2014

1
@OregonTrail:私はあなたのタイミングベースのコメントに続きます。情報をタイミングベースの攻撃に漏らさないようにするために、一定のサイクル数で実行する暗号化コードを作成する必要が何度かありました。
TonyK 2014年

16

テーブル内の定数のセットが事前にわかっている場合は、完全なハッシュを使用して、テーブルへのアクセスが1つだけであることを確認できます。完全なハッシュは、すべての興味深いキーを一意のスロットにマッピングするハッシュ関数を決定します(そのテーブルは常に密であるとは限りませんが、密度の低いテーブルは通常、より単純なハッシュ関数につながるため、余裕のあるテーブルの密度を決定できます)。

通常、特定のキーセットの完全なハッシュ関数は比較的簡単に計算できます。時間をかけて競合するので、それが長くて複雑になることを望まないでしょう。

完全なハッシュは「1プローブ最大」スキームです。ハッシュコードの計算の単純さと、k個のプローブを作成するのにかかる時間とを交換する必要があるという考えで、アイデアを一般化することができます。結局のところ、目標は「ルックアップまでの総時間を最小にする」ことであり、プローブ数を最小限にすることや、ハッシュ関数を単純にすることではありません。ただし、k-probes-maxハッシュアルゴリズムを構築する人を見たことがありません。私はそれができると思いますが、それはおそらく研究です。

もう1つの考え:プロセッサが非常に高速である場合、完全なハッシュからメモリへの1つのプローブがおそらく実行時間を支配します。プロセッサがあまり高速でない場合は、k> 1プローブが実用的です。


1
Cortex-Mは非常に高速ではありません。
MSalters 2014

2
実際、この場合、彼はハッシュテーブルをまったく必要としません。彼は特定のキーがセットに含まれているかどうかを知りたいだけで、値にマップしたくありません。したがって、完全なハッシュ関数が各32ビット値を0または1にマップすれば十分であり、「1」は「セット内にある」と定義できます。
David Ongaro 2014

1
良い点は、彼がそのようなマッピングを生成するための完璧なハッシュジェネレーターを手に入れることができればです。しかし、それは「非常に密度の高いセット」になります。私は彼がそれを行う完璧なハッシュジェネレータを見つけることができると思います。セットにある場合は定数Kを生成し、セットにない場合はK以外の値を生成する完全なハッシュを取得するほうがよいでしょう。後者でも完全なハッシュを取得するのは難しいと思います。
Ira Baxter

@DavidOngaro table[PerfectHash(value)] == valueは、値がセット内にある場合は1を生成し、そうでない場合は0を生成します。PerfectHash関数を生成する方法はよく知られています(たとえば、burtleburtle.net / bob / hash / perfect.htmlを参照)。セット内のすべての値を1に直接マップし、セット内にないすべての値を0にマッピングするハッシュ関数を見つけようとするのは、簡単な作業です。
ジムバルター、2014年

@DavidOngaro:完全なハッシュ関数には多くの「誤検知」があります。つまり、セットにない値は、セット内の値と同じハッシュを持つことになります。したがって、「セット内」の入力値を含む、ハッシュ値で索引付けされたテーブルが必要です。したがって、特定の入力値を検証するには、(a)それをハッシュします。(b)ハッシュ値を使用してテーブルを検索します。(c)テーブルのエントリが入力値と一致するかどうかを確認します。
Craig McQueen

14

ハッシュセットを使用します。O(1)ルックアップ時間を提供します。

次のコードは、値0を「空の」値として予約できる、つまり実際のデータでは発生しないと想定しています。これが当てはまらない状況では、ソリューションを拡張できます。

#define HASH(x) (((x >> 16) ^ x) & 1023)
#define HASH_LEN 1024
uint32_t my_hash[HASH_LEN];

int lookup(uint32_t value)
{
    int i = HASH(value);
    while (my_hash[i] != 0 && my_hash[i] != value) i = (i + 1) % HASH_LEN;
    return i;
}

void store(uint32_t value)
{
    int i = lookup(value);
    if (my_hash[i] == 0)
       my_hash[i] = value;
}

bool contains(uint32_t value)
{
    return (my_hash[lookup(value)] == value);
}

この実装例では、通常、ルックアップ時間は非常に短くなりますが、最悪の場合、格納されているエントリ数に達することがあります。リアルタイムアプリケーションの場合は、より予測可能なルックアップ時間を持つバイナリツリーを使用した実装も検討できます。


3
これが有効になるために、このルックアップを何回実行する必要があるかによって異なります。
maxywb 2014

1
ええと、ルックアップは配列の最後から実行できます。そして、この種の線形ハッシュは衝突率が高く、O(1)を取得する方法はありません。良いハッシュセットはこのように実装されていません。
ジムバルター、2014年

@JimBalter完全なコードではなく、真です。より一般的なアイデアに似ています。既存のハッシュセットコードを指定しただけかもしれません。しかし、これが割り込みサービスルーチンであることを考えると、ルックアップがそれほど複雑なコードではないことを示すことが役立つ場合があります。
jpa

私はそれを包み込むように修正するだけです。
ジムバルター、2014年

完全なハッシュ関数のポイントは、1つのプローブを実行することです。限目。
Ira Baxter

10

この場合、ブルームフィルターを調査する価値があります。それらは、値が存在しないことをすばやく確立することができます。これは、2 ^ 32の可能な値のほとんどが1024要素の配列にないため、これは良いことです。ただし、追加のチェックが必要になるいくつかの誤検知があります。

テーブルは明らかに静的であるため、ブルームフィルターに存在する誤検知を特定し、それらを完全なハッシュに入れることができます。


1
興味深いことに、私はこれまでブルームフィルターを見たことがありませんでした。
Rocketmagnet 2014

8

プロセッサーがLPC4357の最大値であると思われる204 MHzで実行し、タイミング結果が平均的なケース(トラバースされたアレイの半分)を反映していると想定すると、次のようになります。

  • CPU周波数:204 MHz
  • サイクル期間:4.9 ns
  • サイクル時間:12.5 µs / 4.9 ns = 2551サイクル
  • 反復あたりのサイクル:2551/128 = 19.9

したがって、検索ループは反復ごとに約20サイクルを費やします。それはひどく聞こえませんが、私はそれをより速くするためにあなたはアセンブリを見る必要があると思います。

インデックスを削除し、代わりにポインター比較を使用して、すべてのポインターを作成することをお勧めしますconst

bool arrayContains(const uint32_t *array, size_t length)
{
  const uint32_t * const end = array + length;
  while(array != end)
  {
    if(*array++ == 0x1234ABCD)
      return true;
  }
  return false;
}

それは少なくともテストする価値があります。


1
-1、ARMにはインデックス付きアドレスモードがあるため、これは無意味です。ポインタの作成に関してはconst、GCCは変更されていないことをすでに発見しています。constdoesnt'tは何のいずれかを追加します。
MSalters 2014

11
@MSaltersわかりました。生成されたコードで検証しませんでした。ポイントは、Cレベルでそれをより簡単にする何かを表現することでした。ポインターではなくポインターを管理するだけで、インデックスより簡単になると思います。私は単に「const何も追加しない」ことに同意しません。それは値が変わらないことを読者に非常にはっきりと伝えます。それは素晴らしい情報です。
アンワインド

9
これは深く埋め込まれたコードです。これまでの最適化には、フラッシュからRAMへのコードの移動が含まれています。それでもなお、より高速である必要があります。この時点では、読みやすさは目標ではありません
MSalters 2014

1
@MSalters "ARMにはインデックス付きアドレスモードがあるため、これは無意味です"-まあ、完全にポイントを逃した場合、OPは "ポインタ演算とforループも使用します"と書きました。unwindは、インデックス付けをポインターに置き換えませんでした。インデックス変数を削除したため、ループの繰り返しごとに余分な減算が行われました。しかし、OPは(多くの人が答えたりコメントしたりするのとは異なり)賢明であり、最終的にバイナリ検索を実行していました。
ジムバルター2014年

6

他の人々は、バイナリ検索を提供するために、テーブルを再編成するか、最後に番兵値を追加するか、ソートすることを提案しています。

「私はまた、ポインタ演算とforループを使用します。これは、アップではなくダウンカウントを行います(かどうかのi != 0チェックは、i < 256)。」

私の最初のアドバイスは、ポインタ演算とダウンカウントを取り除くことです。のようなもの

for (i=0; i<256; i++)
{
    if (compareVal == the_array[i])
    {
       [...]
    }
}

コンパイラにとって慣用的である傾向があります。ループは慣用的であり、ループ変数に対する配列のインデックスは慣用的です。ポインタ演算とポインタとジャグリングをする傾向がある難読化コンパイラにイディオムをし、それがどのように関連するコードを生成しますあなたは、コンパイラライターは、一般的なのために最善であると判断するものではなく、書いたタスク

たとえば、上記のコードは、ゼロから、-256または-255ゼロまで実行されるループにコンパイルされ、インデックスがオフになります。&the_array[256]。おそらく、有効なCでは表現できなくても、生成対象のマシンのアーキテクチャーに一致するものです。

したがってマイクロ最適化しないでください。オプティマイザの作業にスパナを投入するだけです。賢くなりたい場合は、データ構造とアルゴリズムに取り組みますが、それらの表現をマイクロ最適化しないでください。現在のコンパイラー/アーキテクチャー上にない場合は、次に戻ってきます。

特に、配列やインデックスの代わりにポインター演算を使用すると、コンパイラーがアラインメント、ストレージの場所、エイリアスの考慮事項などを完全に認識し、マシンアーキテクチャーに最も適した方法で強度削減などの最適化を行う場合に有害になります。


ポインター上のループはCでは慣用的であり、優れた最適化コンパイラーは、インデックス付けと同様にそれらを処理できます。しかし、OPがバイナリ検索を実行することになったため、このことはまったく意味がありません。
ジムバルター、2014年

3

ベクトル化はmemchrの実装でよく行われるため、ここで使用できます。次のアルゴリズムを使用します。

  1. OSのビット数(64ビット、32ビットなど)と同じ長さのクエリの繰り返しのマスクを作成します。64ビットシステムでは、32ビットクエリを2回繰り返します。

  2. リストをより大きなデータ型のリストにキャストし、値を取り出すだけで、リストを一度に複数のデータのリストとして処理します。各チャンクについて、マスクとXORし、次に0b0111 ... 1とXO​​Rし、1を追加してから、&を0b1000 ... 0のマスクで繰り返します。結果が0の場合、完全に一致しません。そうでない場合、(通常は非常に高い確率で)一致する可能性があるため、通常どおりチャンクを検索します。

実装例:https : //sourceware.org/cgi-bin/cvsweb.cgi/src/newlib/libc/string/memchr.c?rev=1.3&content-type=text / x-cvsweb-markup&cvsroot=src


3

アプリケーションで使用可能なメモリ量で値のドメインに対応できる場合、最も速い解決策は、配列をビットの配列として表すことです。

bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];

編集

私は批評家の数に驚いています。このスレッドのタイトルは、「C配列に値が存在するかどうかをどのようにしてすばやく見つけるのですか?」です。それが正確に答えるので、私は私の答えを待ちます。これが最も高速なハッシュ関数であると主張できます(アドレス===値のため)。私はコメントを読みましたが、明らかな警告を認識しています。間違いなく、これらの警告は、これを使用して解決できる問題の範囲を制限しますが、解決できる問題については、非常に効率的に解決します。

この回答を完全に拒否するのではなく、ハッシュ関数を使用して速度とパフォーマンスのバランスを改善することで進化させることができる最適な開始点と見なしてください。


8
これはどのようにして4つの賛成票を獲得しますか?質問はそれがCortex M4であると述べています。RAMの容量は262.144 KBではなく、136 KBです。
MSalters 2014

1
回答者が森のために森を逃したため、明らかに間違った回答に対して何票の賛成票が与えられたのかは驚くべきことです。OPの最大ケースの場合O(log n)<< O(n)。
msw 2014

3
利用可能なはるかに優れたソリューションがあるときに、ばかげた量のメモリを書き込むプログラマーには非常に不機嫌になります。5年ごとに私のPCのメモリが不足しているようです。5年前はその量は十分でした。
Craig McQueen

1
最近の@CraigMcQueenキッズ。無駄な記憶。とんでもない!私の時代には、1 MiBのメモリと16ビットのワードサイズがありました。/ s
コールジョンソン

2
厳しい批評家とは何ですか?OPは速度がコードのこの部分にとって絶対的に重要であると明確に述べており、StephenQuanはすでに「とんでもない量のメモリ」について述べました。
Bogdan Alexandru

1

命令(「疑似コード」)とデータ(「theArray」)が別々の(RAM)メモリにあることを確認して、CM4 Harvardアーキテクチャが最大限に活用されるようにします。ユーザーマニュアルから:

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

CPUパフォーマンスを最適化するために、ARM Cortex-M4には、命令(コード)(I)アクセス、データ(D)アクセス、およびシステム(S)アクセス用の3つのバスがあります。命令とデータが別々のメモリに保持されている場合、コードとデータのアクセスを1サイクルで並行して実行できます。コードとデータが同じメモリに保持されている場合、データをロードまたは保存する命令には2サイクルかかることがあります。


興味深いことに、Cortex-M7にはオプションの命令/データキャッシュがありますが、その前には間違いなくありません。 en.wikipedia.org/wiki/ARM_Cortex-M#Silicon_customization
Peter Cordes

0

私の回答が既に回答されている場合は申し訳ありません-私は怠惰な読者です。その後、自由に投票してください))

1)カウンタ 'i'をすべて削除することができます-ポインタを比較するだけ、つまり

for (ptr = &the_array[0]; ptr < the_array+1024; ptr++)
{
    if (compareVal == *ptr)
    {
       break;
    }
}
... compare ptr and the_array+1024 here - you do not need validFlag at all.

ただし、大幅な改善は行われませんが、そのような最適化はおそらくコンパイラ自体によって実現できます。

2)他の回答ですでに言及されているように、最近のCPUのほとんどすべてはRISCベースです(ARMなど)。最近のIntel X86 CPUでも、私が知る限り(内部でX86からコンパイル)、内部でRISCコアを使用しています。RISCの主な最適化は、パイプラインの最適化(およびIntelや他のCPUの場合も同様)であり、コードのジャンプを最小限に抑えます。そのような最適化の1つのタイプ(おそらく大きなもの)は、「サイクルロールバック」のものです。それは信じられないほど愚かで効率的です。IntelコンパイラでさえAFAIKを実行できます。それは次のようになります:

if (compareVal == the_array[0]) { validFlag = true; goto end_of_compare; }
if (compareVal == the_array[1]) { validFlag = true; goto end_of_compare; }
...and so on...
end_of_compare:

このように最適化では、最悪の場合(compareValが配列に存在しない場合)パイプラインが壊れないため、可能な限り高速になります(もちろん、ハッシュテーブルやソートされた配列などのアルゴリズム最適化はカウントされません)。配列のサイズによってはより良い結果が得られる可能性がある他の回答で言及されています。ちなみに、Cycles Rollbackアプローチもそこで適用できます。他では見られなかったと思うことについて、ここに書いています)

この最適化の2番目の部分は、その配列項目が直接アドレス(コンパイル段階で計算され、静的配列を使用していることを確認)によって取得され、配列のベースアドレスからポインターを計算するために追加のADD操作を必要としないことです。AFAIK ARMアーキテクチャには配列のアドレス指定を高速化する特別な機能があるため、この最適化は大きな効果をもたらさない場合があります。とにかく、Cコードだけで最善を尽くしたことを直接知っている方が常に良いでしょう。

サイクルのロールバックはROMの浪費のために不自然に見えるかもしれません(そうです、ボードがこの機能をサポートしている場合、RAMの高速部分に正しく配置しました)。これは、計算の最適化の一般的なポイントにすぎません。要件に応じて、速度を上げるためにスペースを犠牲にし、その逆も同様です。

1024要素の配列のロールバックがあなたの場合にはあまりにも大きな犠牲であると思う場合、「部分的なロールバック」を検討できます。たとえば、配列を512項目の2つの部分に分割する、つまり4x256などです。

3)最近のCPUは、多くの場合、SIMD演算をサポートしています。たとえば、ARM NEON命令セット-同じ演算を並列に実行できます。率直に言って、それが比較演算に適しているかどうかは覚えていませんが、適切だと思うので、確認する必要があります。グーグルは、最大速度を得るために、いくつかのトリックがあるかもしれないことを示しています、https://stackoverflow.com/a/5734019/1028256を参照してください

私はそれがあなたにいくつかの新しいアイデアを与えることを願っています。


OPは線形ループの最適化に焦点を当てたすべての愚かな答えをバイパスし、代わりに配列を事前に並べ替えて二分探索を行いました。
ジムバルター2014年

@ジム、そのような最適化を最初に行う必要があることは明らかです。たとえば、配列をソートする時間がない場合など、一部のユースケースでは「愚かな」答えはそれほどばかげて見えない場合があります。または、取得した速度で十分ではない場合
Mixaz 2014年

「その種の最適化が最初に行われるべきであることは明らかです」-線形ソリューションを開発するために多大な努力をした人々にとっては明らかではありません。「配列をソートする時間がありません」-それが何を意味するのか私にはわかりません。「もしも​​あなたが得る速度がとにかく十分でない場合」-ええと、二分探索の速度が「不十分」である場合、最適化された線形探索を行ってもそれは改善されません。これでこの問題は終わりです。
ジムバルター、2014年

@ JimBalter、OPのような問題が発生した場合は、バイナリ検索などのアルゴリズムを使用することを検討します。OPがそれをまだ考慮していないとは思えませんでした。「配列をソートする時間がありません」とは、配列のソートに時間がかかることを意味します。入力データセットごとに行う必要がある場合は、線形ループよりも時間がかかる場合があります。「または、もしあなたが得る速度がとにかく十分ではない」ということは、以下を意味します-上記の最適化のヒントは、バイナリ検索コードなどを高速化するために使用できます
Mixaz

0

私はハッシュの大ファンです。もちろん問題は、高速であり、最小量のメモリを使用する(特に組み込みプロセッサ上で)効率的なアルゴリズムを見つけることです。

発生する可能性のある値を事前に知っている場合は、多数のアルゴリズムを実行するプログラムを作成して、最適なアルゴリズム、つまりデータに最適なパラメータを見つけることができます。

私はあなたがこの投稿で読むことができるようなプログラムを作成し、いくつかの非常に速い結果を達成しました。16000エントリは、およそ2 ^ 14または平均14の比較に変換され、バイナリ検索を使用して値を見つけます。私は明示的に非常に高速なルックアップを目指しました-平均して1.5以下のルックアップで値を見つけることで、RAM要件が大きくなりました。より控えめな平均値(たとえば、<= 3)を使用すると、多くのメモリを節約できると思います。256または1024エントリのバイナリ検索の平均ケースを比較すると、比較の平均数はそれぞれ8と10になります。

私の平均ルックアップには、一般的なアルゴリズム(変数による1除算を使用)で約60サイクル(Intel i5を搭載したラップトップ上)と、特殊な(おそらく乗算を使用)で40〜45サイクル必要でした。これはもちろん、実行するクロック周波数に応じて、MCUでのサブマイクロ秒のルックアップ時間に変換されます。

エントリ配列がエントリへのアクセス回数を追跡している場合は、さらに調整することができます。インデックスが計算される前に、エントリ配列がアクセス頻度の高いものから最も低いものへとソートされている場合、単一の比較で最も一般的に発生する値を見つけます。


0

これは答えというよりは補遺のようなものです。

私が持っていた似た過去のケースを、私の配列は、検索の相当数にわたり一定でした。

それらの半分では、検索された値は配列に存在しませんでした。その後、検索を行う前に「フィルター」を適用できることに気付きました。

この「フィルター」は単純な整数で、ONCEで計算され、各検索で使用されます。

これはJavaですが、非常に簡単です。

binaryfilter = 0;
for (int i = 0; i < array.length; i++)
{
    // just apply "Binary OR Operator" over values.
    binaryfilter = binaryfilter | array[i];
}

したがって、バイナリ検索を行う前に、binaryfilterを確認します。

// Check binaryfilter vs value with a "Binary AND Operator"
if ((binaryfilter & valuetosearch) != valuetosearch)
{
    // valuetosearch is not in the array!
    return false;
}
else
{
    // valuetosearch MAYBE in the array, so let's check it out
    // ... do binary search stuff ...

}

「より良い」ハッシュアルゴリズムを使用できますが、これは非常に高速で、特に大きな数の場合に有効です。これにより、さらに多くのサイクルを節約できる可能性があります。

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