サイズが256(できれば1024、ただし256が最小)の配列を反復処理し、値が配列の内容と一致するかどうかを確認する必要がある、タイムクリティカルなISRを備えた組み込みアプリケーションがあります。この場合、bool
は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;
}
}
これを行うための絶対的な最速の方法は何でしょうか?インラインアセンブリの使用は許可されています。他の「よりエレガントではない」トリックも許可されます。
パフォーマンスが最も重要な状況では、Cコンパイラは、手作業で調整されたアセンブリ言語で実行できるものと比較して、おそらく最速のコードを生成しません。私は抵抗が最も少ないパスを取る傾向があります。このような小さなルーチンの場合、asmコードを記述するだけで、実行に必要なサイクル数がわかります。 Cコードをいじってコンパイラに適切な出力を生成させることができるかもしれませんが、そのように出力を調整することに多くの時間を浪費することになります。コンパイラ(特にMicrosoftから)は、ここ数年で大きな進歩を遂げましたが、一般的なケースだけでなく特定の状況で作業しているため、耳の間のコンパイラほどスマートではありません。コンパイラは、これを高速化できる特定の命令(LDMなど)を使用しない場合があり、ループを展開するのに十分なほどスマートではありません。これを行う方法は、コメントで言及した3つのアイデアを組み込んでいます:ループの展開、キャッシュのプリフェッチ、複数ロード(ldm)命令の使用。命令サイクルカウントは、配列要素ごとに約3クロックになりますが、これはメモリ遅延を考慮していません。
動作理論: ARMのCPU設計は、ほとんどの命令を1クロックサイクルで実行しますが、命令はパイプラインで実行されます。 Cコンパイラは、間に他の命令をインターリーブすることにより、パイプラインの遅延を排除しようとします。元のCコードのようなタイトループが発生すると、コンパイラはメモリから読み取った値をすぐに比較する必要があるため、遅延を隠すのに苦労します。以下の私のコードは、メモリ自体とデータをフェッチするパイプラインの遅延を大幅に削減するために、2つの4つのレジスタセットを交互に使用しています。一般に、大規模なデータセットを操作し、コードが使用可能なレジスタのほとんどまたはすべてを使用しない場合、最大のパフォーマンスが得られません。
; 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はおそらく同様の結果をもたらします。
それを最適化するためのトリックがあります(私は一度、ジョブインタビューでこれを尋ねられました):
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;
}
コンパイラがまだ適用していない場合、この関数は確実に適用します。一方、ループを展開するのがオプティマイザーで難しくなる可能性があるため、生成されたアセンブリコードでそれを確認する必要があります...
アルゴリズムを最適化するための支援を求めているため、アセンブラーにプッシュされる可能性があります。ただし、アルゴリズム(線形検索)はそれほど賢くないため、アルゴリズムの変更を検討する必要があります。例えば。:
256の「有効な」値が静的であり、コンパイル時にわかっている場合は、 完全なハッシュ関数 を使用できます。入力値を0 ..nの範囲の値にマッピングするハッシュ関数を見つける必要があります。ここではcollisions気にするすべての有効な値について。つまり、2つの「有効な」値が同じ出力値にハッシュすることはありません。適切なハッシュ関数を検索するときは、次のことを目指します。
効率的なハッシュ関数については、nは2のべき乗であることが多く、これは低ビットのビット単位マスク(AND演算)と同等です。ハッシュ関数の例:
((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n
_(i
、j
、k
、...必要に応じて、左シフトまたは右シフトで選択)次に、nエントリの固定テーブルを作成します。ここで、ハッシュは入力値をインデックスiにマップします。有効な値の場合、テーブルエントリiには有効な値が含まれます。他のすべてのテーブルエントリについては、インデックスiの各エントリに、iにハッシュされない他の無効な値が含まれていることを確認してください。
次に、割り込みルーチンで、入力xを使用します。
これは、256または1024の値の線形検索よりもはるかに高速です。
いくつかのPythonコード と書いて、妥当なハッシュ関数を見つけました。
256個の「有効な」値の配列を並べ替えると、線形検索ではなく binary search を実行できます。つまり、8つのステップ(log2(256)
)で256エントリのテーブル、または10ステップで1024エントリのテーブルを検索できるはずです。繰り返しますが、これは256または1024の値の線形検索よりもはるかに高速です。
テーブルをソートされた順序に保ち、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]);
_
ポイントは、
==
_の場合のポイントテストはありません。**確率の観点から考えることに慣れていない場合、すべての決定ポイントにはエントロピーがあります。これは、実行することで学習する平均情報です。 _>=
_テストの場合、各ブランチの確率は約0.5で、-log2(0.5)は1であるため、1つのブランチを取得する場合は1ビットを学習し、他のブランチを取得する場合は1ビットを学習しますビット、および平均は、各分岐で学習したものにその分岐の確率を掛けたものの合計です。したがって、_1*0.5 + 1*0.5 = 1
_なので、_>=
_テストのエントロピーは1です。学習するビットが10ビットあるため、10分岐します。それが速い理由です!
一方、最初のテストがif (key == a[i+512)
の場合はどうなりますか?真である確率は1/1024、偽である確率は1023/1024です。それで本当なら、あなたはすべての10ビットを学びます!しかし、それが偽の場合、-log2(1023/1024)= .00141ビットを学習しますが、実際には何もありません!したがって、そのテストから学習する平均量は_10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112
_ビットです。 約100分の1ビットそのテストは重量を運んでいない!
テーブル内の定数のセットが事前にわかっている場合は、 完全ハッシュ を使用して、テーブルへのアクセスが1回のみであることを確認できます。完全なハッシュは、すべての興味深いキーを一意のスロットにマップするハッシュ関数を決定します(そのテーブルは常に密であるとは限りませんが、通常はより単純なハッシュ関数につながる、密でないテーブルで余裕があるかどうかを決定できます)。
通常、特定のキーセットの完全なハッシュ関数は比較的簡単に計算できます。長く複雑になるのは望ましくありません。複数のプローブの実行に費やしたほうがおそらく時間の節約になるからです。
完全ハッシュは「1プローブ最大」スキームです。ハッシュコードの計算の単純さと、k個のプローブを作成するのにかかる時間とを交換する必要があるという考えで、アイデアを一般化できます。結局のところ、目標は「ルックアップするための最小合計時間」であり、最も少ないプローブや最も単純なハッシュ関数ではありません。ただし、k-probes-maxハッシュアルゴリズムを作成する人は誰もいません。できると思うが、それはおそらく研究だ。
もう1つの考え:プロセッサが非常に高速である場合、完全なハッシュからメモリへの1つのプローブがおそらく実行時間を支配します。プロセッサが非常に高速でない場合は、k> 1プローブが実用的です。
ハッシュセットを使用します。 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);
}
この実装例では、ルックアップ時間は通常非常に短くなりますが、最悪の場合、格納されるエントリの数まで可能です。リアルタイムアプリケーションでは、より予測可能なルックアップ時間を持つバイナリツリーを使用した実装も検討できます。
この場合、 ブルームフィルター を調査する価値があります。値が存在しないことをすばやく確認できます。これは、2 ^ 32の値のほとんどがその1024要素の配列にないため、良いことです。ただし、追加のチェックが必要ないくつかの誤検知があります。
テーブルは明らかに静的であるため、Bloomフィルターに存在する誤検知を特定し、それらを完全なハッシュに入れることができます。
プロセッサがLPC4357の最大値と思われる204 MHzで動作し、また、タイミング結果が平均的なケース(アレイの半分が通過)を反映していると仮定すると、次のようになります。
したがって、検索ループは反復ごとに約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;
}
それは少なくともテストする価値があります。
他の人は、バイナリ検索を提供するために、テーブルを再編成するか、センチネル値を最後に追加するか、ソートすることを提案しています。
「ポインタ演算とforループも使用します。これは、アップではなくダウンカウントを行います(i != 0
がi < 256
をチェックするよりも速いかどうかをチェックします)。
私の最初のアドバイスは、ポインター演算とダウンカウントを取り除くことです。のようなもの
for (i=0; i<256; i++)
{
if (compareVal == the_array[i])
{
[...]
}
}
コンパイラにとってidiomaticになる傾向があります。ループは慣用的であり、ループ変数に対する配列のインデックス付けは慣用的です。ポインター算術とポインターのジャグリングはobfuscateコンパイラーのイディオムになり、コンパイラーライターが最高と判断したものではなくyoに関連するコードを生成する傾向があります。一般的なコースtask。
たとえば、上記のコードは、-256
または-255
からゼロまで実行され、&the_array[256]
からインデックスを作成するループにコンパイルされる場合があります。おそらく、有効なCで表現することさえできないが、生成するマシンのアーキテクチャに一致するもの。
しない microoptimize。最適化プログラムの動作にスパナを投げているだけです。賢くなりたいのであれば、データ構造とアルゴリズムに取り組みますが、それらの表現を微最適化しないでください。現在のコンパイラ/アーキテクチャではない場合は、次のバイトに戻ってきます。
特に、配列とインデックスの代わりにポインター演算を使用することは、コンパイラーがアライメント、ストレージの場所、エイリアスの考慮事項などを十分に認識しているため、マシンのアーキテクチャに最適な方法で強度の低減などの最適化を行うために有害です。
多くの場合memchrの実装にあるため、ここではベクトル化を使用できます。次のアルゴリズムを使用します。
OSのビットカウント(64ビット、32ビットなど)と同じ長さのクエリのマスクを繰り返し作成します。 64ビットシステムでは、32ビットクエリを2回繰り返します。
リストをより大きなデータ型のリストにキャストし、値を引き出すだけで、リストを一度に複数のデータのリストとして処理します。各チャンクについて、XORマスクを使用して、次にXOR 0b0111 ... 1を使用して、1を追加し、マスクを0b1000で使用します。 .0 repeat。結果が0の場合、間違いなく一致しますが、そうでない場合(通常は非常に高い確率で)一致する可能性があるため、通常の方法でチャンクを検索します。
使用可能なメモリの量を使用して値のドメインをアプリケーションに対応できる場合、最も速い解決策は、配列をビットの配列として表すことです。
bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];
[〜#〜] edit [〜#〜]
私は批評家の数に驚いています。このスレッドのタイトルは"C配列に値が存在するかどうかをすばやく見つけるにはどうすればよいですか?"です。これは、最も効率の良いハッシュ関数を持っていると主張できます(アドレス===値なので)。私はコメントを読みましたが、明らかな警告を認識しています。間違いなくこれらの警告は、これを使用して解決できる問題の範囲を制限しますが、解決する問題については、非常に効率的に解決します。
この回答を完全に拒否するのではなく、ハッシュ関数を使用して速度とパフォーマンスのバランスを改善することで進化できる最適な出発点と考えてください。
私の答えがすでに答えられている場合は申し訳ありません-私は怠け者の読者です。自由に投票してください)))
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のほとんどは、ARMなどのRISCベースです。私の知る限り、最新のIntel X86 CPUでも内部でRISCコアを使用しています(X86からオンザフライでコンパイルします)。 RISCの主要な最適化はパイプラインの最適化で(Intelやその他のCPUも同様)、コードのジャンプを最小限に抑えます。そのような最適化の1つのタイプ(おそらく主要なもの)は、「サイクルロールバック」です。それは信じられないほど愚かで効率的であり、Intelコンパイラでさえその限りではありません。次のようになります。
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がない場合)、可能な限り高速です(もちろん、ハッシュテーブル、ソートされた配列などのアルゴリズム最適化はカウントされません)。配列サイズに応じてより良い結果が得られる可能性のある他の回答で言及されています。サイクルロールバックアプローチもそこに適用できます。他の人には見られなかったと思うことをここに書いています)
この最適化の2番目の部分は、配列項目が直接アドレス(コンパイル段階で計算され、静的配列を使用していることを確認)によって取得され、配列のベースアドレスからポインターを計算するために追加のADD opを必要としないことです。 AFAIK ARMアーキテクチャには配列のアドレス指定を高速化する特別な機能があるため、この最適化は大きな効果はありません。しかし、とにかく、Cコードで直接最善を尽くしたことを常に知っている方が良いです?
サイクルロールバックはROMの無駄のために厄介に見えるかもしれません(そう、ボードがこの機能をサポートしている場合、RAMの高速部分に正しく配置しました)、しかし実際には速度に対してかなりの支払いです、 RISCコンセプトに基づいていますこれは、計算最適化の一般的なポイントにすぎません-要件に応じて、速度のためにスペースを犠牲にします。
1024要素の配列のロールバックがあなたの場合にはあまりにも大きな犠牲になっていると思う場合、「部分的なロールバック」を検討することができます。たとえば、配列を512アイテムの2つの部分、または4x256などに分割します。
3)最新のCPUは、SIMD opsをサポートすることがよくあります。たとえば、ARM NEON命令セット-同じopsを並行して実行できます。率直に言って、比較opsに適しているかどうかは覚えていませんが、グーグルは、最大速度を得るためにいくつかのトリックもあるかもしれないことを示しています https://stackoverflow.com/a/5734019/1028256
新しいアイデアが得られることを願っています。
私はハッシングの大ファンです。もちろん、問題は、高速で、最小限のメモリを使用する(特に組み込みプロセッサで)効率的なアルゴリズムを見つけることです。
発生する可能性のある値が事前にわかっている場合は、多数のアルゴリズムを実行するプログラムを作成して、最適なアルゴリズム、つまりデータに最適なパラメーターを見つけることができます。
this post で読むことができるプログラムを作成し、非常に高速な結果を達成しました。 16000エントリは、バイナリ検索を使用して値を見つけるために、おおよそ2 ^ 14または平均14回の比較に変換されます。私は明示的に非常に高速なルックアップを目指しました-平均で<= 1.5ルックアップで値を見つける-より大きなRAM要件。より保守的な平均値(たとえば<= 3)比較すると、256または1024エントリでのバイナリ検索の平均的なケースでは、それぞれ8と10の平均比較回数になります。
私の平均的なルックアップには、汎用アルゴリズム(変数による1除算を使用)で約60サイクル(Intel i5を搭載したラップトップで)および特殊化(おそらく乗算を使用)で40-45サイクルが必要でした。これは、実行するクロック周波数に応じて、MCUでのサブマイクロ秒のルックアップ時間に変換されるはずです。
エントリ配列がエントリがアクセスされた回数を追跡する場合、さらに実際の調整が可能です。インデックスが計算される前にエントリ配列がアクセスの多いものから順に並べられている場合、1回の比較で最も頻繁に発生する値が検出されます。
これは答えというよりは補遺のようなものです。
過去にsimilarのケースがありましたが、私の配列はかなりの数の検索にわたって一定でした。
それらの半分では、検索された値は配列に存在しませんでした。その後、検索を行う前に「フィルター」を適用できることに気付きました。
この「フィルター」は単純な整数であり、計算された[〜#〜] 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 ...
}
「より良い」ハッシュアルゴリズムを使用できますが、これは非常に高速で、特に大きな数の場合に使用できます。これにより、さらに多くのサイクルを節約できる可能性があります。