これはよく知られた選択アルゴリズムです。 http://en.wikipedia.org/wiki/Selection_algorithm を参照してください。
3x3x3ボクセル値のセットの中央値を見つけるために必要です。ボリュームは10億ボクセルで構成されており、アルゴリズムは再帰的であるため、少し高速である方が良いでしょう。一般に、値は比較的近いと予想できます。
これまでに試した中で最も高速な既知のアルゴリズムは、クイックソートパーティション関数を使用しています。もっと速いものがあったら教えてください。
2つのヒープを使用して20%高速なものを「発明」しましたが、ハッシュを使用してさらに高速なものを期待していました。これを実装する前に、非常に高速なソリューションがすでに存在しているかどうかを知りたいと思います。
フロートを使用しているという事実は重要ではありません。フロートは符号ビットを反転した後、符号なし整数と見なすことができるからです。順序は保持されます。
編集:Davy Landmanによって提案されたように、ベンチマークとソースコードは別の回答に移動しました。 chmikeによる回答については、以下を参照してください。
[〜#〜] edit [〜#〜]:これまでで最も効率的なアルゴリズムは、Boojumによって 高速中央値およびバイラテラルフィルタリング この質問に対する答えとなった論文。この方法の最初の賢いアイデアは、基数ソートを使用することです。2つ目は、多くのピクセルを共有する隣接ピクセルの中央値検索を組み合わせることです。
大量のボリュームデータに対して中央値フィルターを実行しているように聞こえるので、SIGGRAPH 2006の 高速中央値とバイラテラルフィルタリング の論文をご覧ください。この論文は2Dを扱っています。画像処理ですが、アルゴリズムを3Dボリュームに適合させることができる場合があります。他に何もない場合は、少し後戻りして、少し異なる視点から問題を検討する方法についていくつかのアイデアが得られるかもしれません。
選択アルゴリズムは線形時間(O(n))です。複雑な方法では、すべてのデータを読み取るのに線形時間がかかるため、線形時間よりも優れた方法はありません。したがって、複雑さの点でより高速なものを作成することはできませんでした。おそらく、特定の入力で定数要素がより速い何かがあるでしょうか?それが大きな違いを生むとは思えません。
C++にはすでに線形時間選択アルゴリズムが含まれています。なぜそれを使用しないのですか?
_std::vector<YourType>::iterator first = yourContainer.begin();
std::vector<YourType>::iterator last = yourContainer.end();
std::vector<YourType>::iterator middle = first + (last - first) / 2;
std::nth_element(first, middle, last); // can specify comparator as optional 4th arg
YourType median = *middle;
_
編集:技術的には、これは奇数の長さのコンテナーの中央値のみです。偶数の長さの場合は、「上限」の中央値になります。中央値の従来の定義を偶数長さにしたい場合は、first + (last - first) / 2
とfirst + (last - first) / 2 - 1
の2つの「中間」のそれぞれに対して1回ずつ、2回実行してから、それらを平均するか、または何か。
編集:謝る必要があります。以下のコードは間違っていました。修正されたコードがありますが、やり直すにはiccコンパイラを見つける必要があります測定値
これまでに検討されたアルゴリズムのベンチマーク結果
プロトコルとアルゴリズムの簡単な説明については、以下を参照してください。最初の値は200の異なるシーケンスの平均時間(秒)で、2番目の値はstdDevです。
HeapSort : 2.287 0.2097
QuickSort : 2.297 0.2713
QuickMedian1 : 0.967 0.3487
HeapMedian1 : 0.858 0.0908
NthElement : 0.616 0.1866
QuickMedian2 : 1.178 0.4067
HeapMedian2 : 0.597 0.1050
HeapMedian3 : 0.015 0.0049 <-- best
プロトコル:Rand()から取得したランダムビットを使用して27個のランダムフロートを生成します。各アルゴリズムを500万回続けて適用し(以前の配列コピーを含む)、200のランダムシーケンスの平均とstdDevを計算します。 icc -S -O3でコンパイルされ、8GB DDR3のIntel E8400で実行されるC++コード。
アルゴリズム:
HeapSort:ヒープソートを使用してシーケンスの完全なソートを行い、中間値を選択します。添え字アクセスを使用した単純な実装。
QuickSort:クイックソートを使用したシーケンスのフルインプレイスソートと中間値の選択。添え字アクセスを使用した単純な実装。
QuickMedian1:スワップ付きのクイック選択アルゴリズム。添え字アクセスを使用した単純な実装。
HeapMedian1:事前にスワッピングを使用してバランスの取れたヒープメソッドを配置します。添え字アクセスを使用した単純な実装。
NthElement:nth_element STLアルゴリズムを使用します。データはmemcpy(vct.data()、rndVal、...);を使用してベクターにコピーされます。
QuickMedian2:ポインターを使用したクイック選択アルゴリズムを使用し、2つのバッファーにコピーしてスワップを回避します。 MSaltersの提案に基づいています。
HeapMedian2:共有ヘッドを持つデュアルヒープを使用した、私の発明したアルゴリズムのバリアント。左ヒープはヘッドとして最大の値を持ち、右ヒープはヘッドとして最小の値を持ちます。最初の値を共通の頭として初期化し、最初の中央値を推測します。ヘッドの1つがいっぱいになるまで、ヘッドよりも小さい場合は後続の値を左ヒープに追加し、そうでない場合は右ヒープに追加します。 14個の値が含まれている場合は満杯です。次に、完全なヒープのみを検討します。正しいヒープの場合、すべての値がヘッドよりも大きい場合は、ヘッドをポップして値を挿入します。他のすべての値を無視します。左側のヒープの場合、すべての値がヘッドよりも小さい場合は、ヘッドをポップしてヒープに挿入します。他のすべての値を無視します。すべての値が処理されると、共通の頭は中央値です。配列への整数インデックスを使用します。ポインタ(64ビット)を使用するバージョンは、ほぼ2倍遅い(〜1秒)ようでした。
HeapMedian3:HeapMedian2と同じアルゴリズムですが、最適化されています。 unsigned charインデックスを使用し、値のスワッピングやその他のさまざまな小さなことを回避します。平均値とstdDev値は、1000個のランダムシーケンスに対して計算されます。 nth_elementでは、同じ1000個のランダムシーケンスで0.508sと0.159537のstdDevを測定しました。したがって、HeapMedian3はnth_element stl関数より33倍高速です。返される各中央値は、heapSortによって返される中央値と照合され、すべて一致します。ハッシュを使用する方法が大幅に高速になる可能性があるとは思いません。
編集1:このアルゴリズムはさらに最適化できます。比較結果に基づいて要素が左または右のヒープにディスパッチされる最初のフェーズでは、ヒープは必要ありません。 2つの順序付けられていないシーケンスに要素を追加するだけで十分です。フェーズ1は、1つのシーケンスがいっぱいになるとすぐに停止します。つまり、フェーズ1には14個の要素(中央値を含む)が含まれます。 2番目のフェーズは、シーケンス全体をヒープ化することから始まり、次にHeapMedian3アルゴリズムで説明されているように進みます。新しいコードとベンチマークをできるだけ早く提供します。
編集2:最適化されたアルゴリズムを実装してベンチマークしました。しかし、heapMedian3と比較してパフォーマンスに大きな違いはありません。平均して少し遅いです。表示された結果が確認されます。より大きなセットがあるかもしれません。また、最初の中央値として最初の値を選択するだけであることにも注意してください。示唆されたように、「重複する」値セットの中央値を検索するという事実から利益を得ることができます。中央値の中央値アルゴリズムを使用すると、はるかに優れた初期中央値の推測を選択するのに役立ちます。
HeapMedian3のソースコード
// return the median value in a vector of 27 floats pointed to by a
float heapMedian3( float *a )
{
float left[14], right[14], median, *p;
unsigned char nLeft, nRight;
// pick first value as median candidate
p = a;
median = *p++;
nLeft = nRight = 1;
for(;;)
{
// get next value
float val = *p++;
// if value is smaller than median, append to left heap
if( val < median )
{
// move biggest value to the heap top
unsigned char child = nLeft++, parent = (child - 1) / 2;
while( parent && val > left[parent] )
{
left[child] = left[parent];
child = parent;
parent = (parent - 1) / 2;
}
left[child] = val;
// if left heap is full
if( nLeft == 14 )
{
// for each remaining value
for( unsigned char nVal = 27 - (p - a); nVal; --nVal )
{
// get next value
val = *p++;
// if value is to be inserted in the left heap
if( val < median )
{
child = left[2] > left[1] ? 2 : 1;
if( val >= left[child] )
median = val;
else
{
median = left[child];
parent = child;
child = parent*2 + 1;
while( child < 14 )
{
if( child < 13 && left[child+1] > left[child] )
++child;
if( val >= left[child] )
break;
left[parent] = left[child];
parent = child;
child = parent*2 + 1;
}
left[parent] = val;
}
}
}
return median;
}
}
// else append to right heap
else
{
// move smallest value to the heap top
unsigned char child = nRight++, parent = (child - 1) / 2;
while( parent && val < right[parent] )
{
right[child] = right[parent];
child = parent;
parent = (parent - 1) / 2;
}
right[child] = val;
// if right heap is full
if( nRight == 14 )
{
// for each remaining value
for( unsigned char nVal = 27 - (p - a); nVal; --nVal )
{
// get next value
val = *p++;
// if value is to be inserted in the right heap
if( val > median )
{
child = right[2] < right[1] ? 2 : 1;
if( val <= right[child] )
median = val;
else
{
median = right[child];
parent = child;
child = parent*2 + 1;
while( child < 14 )
{
if( child < 13 && right[child+1] < right[child] )
++child;
if( val <= right[child] )
break;
right[parent] = right[child];
parent = child;
child = parent*2 + 1;
}
right[parent] = val;
}
}
}
return median;
}
}
}
}
確かに知っているように、あるアルゴリズムの別のアルゴリズムに対するパフォーマンスは、アルゴリズム自体に依存するだけでなく、コンパイラー/プロセッサー/データ構造の組み合わせにも依存するという単純な理由で、この質問に簡単に答えることはできません
したがって、それらのいくつかを試すアプローチは十分に思えます。そして、はい、クイックソートはかなり高速でなければなりません。まだ行っていない場合は、小さなデータセットでよくパフォーマンスするInsertionsortを試してみてください。これは、十分に速く仕事をする分類アルゴリズムで解決する、と述べました。通常、「正しい」アルゴリズムを選択するだけでは、10倍速くはなりません。
大幅な高速化を実現するには、より多くの構造を使用することが頻繁に推奨されます。過去に大規模な問題で私のために働いたいくつかのアイデア:
ボクセルの作成中に効率的に事前計算し、27フロートではなく28フロートを格納できますか?
近似解で十分ですか?その場合は、「9つの値の中央値」を見てください。これは、「一般に、値が比較的近いことが期待できるため」です。または、値が比較的近い限り、平均に置き換えることができます。
本当に何十億ものボクセルの中央値が必要ですか?おそらく、中央値が必要かどうかを簡単にテストして、関連するサブセットについてのみ計算できます。
他に何も役に立たない場合:コンパイラーが生成するasmコードを見てください。大幅に高速なasmコードを作成できる場合があります(たとえば、レジスタを使用してすべての計算を行うことにより)。
編集:価値があるので、以下のコメントに記載されている(部分的に)InsertionSortコードを添付しました(完全にテストされていません)。 _numbers[]
_がサイズN
の配列であり、最小のP
浮動小数点数を配列の先頭でソートする場合は、partial_insertionsort<N, P, float>(numbers);
を呼び出します。したがって、partial_insertionsort<27, 13, float>(numbers);
を呼び出すと、_numbers[13]
_には中央値が含まれます。さらに速度を上げるには、whileループも展開する必要があります。上記で説明したように、本当に速くするには、データに関する知識を使用する必要があります(たとえば、データは既に部分的にソートされていますか?データの分布の特性を知っていますか?おそらく、ドリフトが発生します)。
_template <long i> class Tag{};
template<long i, long N, long P, typename T>
inline void partial_insertionsort_for(T a[], Tag<N>, Tag<i>)
{ long j = i <= P+1 ? i : P+1; // partial sort
T temp = a[i];
a[i] = a[j]; // compiler should optimize this away where possible
while(temp < a[j - 1] && j > 0)
{ a[j] = a[j - 1];
j--;}
a[j] = temp;
partial_insertionsort_for<i+1,N,P,T>(a,Tag<N>(),Tag<i+1>());}
template<long i, long N, long P, typename T>
inline void partial_insertionsort_for(T a[], Tag<N>, Tag<N>){}
template <long N, long P, typename T>
inline void partial_insertionsort(T a[])
{partial_insertionsort_for<0,N,P,T>(a, Tag<N>(), Tag<0>());}
_
最初の試行で使用する可能性が最も高いアルゴリズムは、単にnth_elementです。それはほとんどあなたがあなたが直接欲しいものをあなたに与えます。 14番目の要素を要求してください。
2番目の試みの目標は、固定データサイズを利用することです。アルゴリズムの実行中にメモリを割り当てる必要はありません。したがって、ボクセルの値を27要素の事前に割り当てられた配列にコピーします。ピボットを選択し、53要素の配列の中央にコピーします。残りの値をピボットのどちらかの側にコピーします。ここでは、2つのポインタ(float* left = base+25, *right=base+27
)を保持しています。現在、3つの可能性があります。左側が大きい、右側が大きい、または両方に12個の要素があります。最後のケースは簡単です。あなたのピボットは中央値です。それ以外の場合は、左側または右側のいずれかでnth_elementを呼び出します。 Nthの正確な値は、ピボットよりも大きいまたは小さい値の数によって異なります。たとえば、分割が12/14の場合、ピボットよりも大きい最小要素が必要なのでNth = 0で、分割が14/12の場合は、ピボットよりも小さい最大要素が必要なので、Nth = 13です。最悪のケースは、ピボットが極端だった26/0と0/26ですが、これらはすべてのケースの2/27でのみ発生します。
3番目の改善(または、Cを使用する必要があり、nth_elementがない場合、最初の改善)は、nth_elementを完全に置き換えます。まだ53要素の配列がありますが、今度はボクセル値から直接入力します(中間コピーをfloat[27]
に保存します)。この最初の反復のピボットは、単なるVoxel [0] [0] [0]です。後続の反復では、2番目に事前に割り当てられたfloat[53]
を使用し(両方が同じサイズであればより簡単)、2つの間で浮動小数点数をコピーします。ここでの基本的な反復ステップはまだです:ピボットを中央にコピーし、残りを左と右に並べ替えます。各ステップの最後に、中央値が現在のピボットよりも小さいか大きいかがわかるので、そのピボットよりも大きいまたは小さいフロートを破棄できます。反復ごとに、これにより1〜12個の要素が削除され、残りの平均は25%になります。
それでも速度を上げる必要がある場合の最後の反復は、ほとんどのボクセルが大幅に重複しているという観察に基づいています。 3x3x1スライスごとに中央値を事前計算します。次に、3x3x3ボクセルキューブの初期ピボットが必要な場合は、3つの中央値を使用します。中央値の中央値(4 + 4 + 1)よりも9ボクセルが小さく、9ボクセルが大きいという演繹的な知識があります。したがって、最初のピボットステップの後の最悪のケースは、9/17と17/9の分割です。したがって、float [26]の12番目または14番目の要素ではなく、float [17]の4番目または13番目の要素を検索するだけで済みます。
背景:左と右のポインタを使用して、最初にピボットをコピーし、次に残りのfloat [N]をfloat [2N-1]にコピーするという考えは、ピボットの周りのfloat [N]サブ配列をすべての要素で埋めることです。左のピボットより小さく(インデックスが低く)、右のピボットより高い(インデックスが高い)。ここで、M番目の要素が必要な場合は、幸運なことに、ピボットよりもM-1要素が小さい場合があります。この場合、ピボットが必要な要素です。ピボットよりも小さい(M-1)個以上の要素がある場合は、M番目の要素がその中に含まれるため、ピボットとピボットよりも大きいものをすべて破棄し、すべての低い値でM番目の要素をシークできます。 以下ピボットより小さい(M-1)要素がある場合は、ピボットよりも高い値を探しています。したがって、ピボットとそれよりも小さいものはすべて破棄します。要素の数lessをピボットよりも大きくします。つまり、ピボットの左側をLとします。次の反復では、(NL-1)floatの(ML-1)番目の要素が必要です。ピボットよりも大きい。
この種類のnth_elementアルゴリズムは、ほとんどの作業が2つの小さな配列間で浮動小数点数をコピーするために費やされ、どちらもキャッシュに入れられるため、状態はほとんどの場合3つのポインター(ソースポインター、左宛先ポインター)で表されるため、かなり効率的です。 、右の宛先ポインタ)。
基本的なコードを表示するには:
float in[27], out[53];
float pivot = out[26] = in[0]; // pivot
float* left = out+25, right = out+27
for(int i = 1; i != 27; ++1)
if((in[i]<pivot)) *left-- = in[i] else *right++ = in[i];
// Post-condition: The range (left+1, right) is initialized.
// There are 25-(left-out) floats <pivot and (right-out)-27 floats >pivot
Bose-Nelsonアルゴリズムを使用して生成されたソートネットワークは、173の比較を使用して、ループ/再帰なしで中央値を直接検出します。ベクトル演算命令の使用など、比較を並行して実行する機能がある場合、比較を28の並列操作にグループ化できる場合があります。
Floatが(qs)NaNではなく正規化されていることが確実な場合は、整数演算を使用して、一部のCPUでより有利に実行できるIEEE-754 floatを比較できます。
このソーティングネットワークをC(gcc 4.2)に直接変換すると、Core i7で最悪の場合388クロックサイクルが発生します。
あなたの最善の策は、既存のソートアルゴリズムを使用して、セットを完全にソートする必要がないようにそれを適応できるかどうかを考えてみることです。中央値を決定するには、並べ替えた値の最大で半分が必要です。下半分または上半分のどちらでも十分です。
original: | 5 | 1 | 9 | 3 | 3 |
sorted: | 1 | 3 | 3 | 5 | 9 |
lower half sorted: | 1 | 3 | 3 | 9 | 5 |
higher half sorted: | 3 | 1 | 3 | 5 | 9 |
残りの半分は、単に並べ替えられていない値のバケットであり、単に大きい/小さい、または最大/最小の並べ替えられた値に等しいというプロパティを共有します。
しかし、そのための準備ができたアルゴリズムはありません。これは、並べ替えでショートカットをどのように取ることができるかについてのアイデアにすぎません。
アレックスステパノフの新しい本 プログラミングの要素 は、実行時のオーバーヘッドを最小限に抑えながら、最小数の平均比較を使用して注文統計を見つけることについてある程度話しています。残念ながら、5つの要素の中央値を計算するためだけに相当な量のコードが必要です。それでも、プロジェクトとして、平均よりも比較の割合が少ない部分を使用する代替ソリューションを見つけるため、それを拡張することを夢見ていません。 27要素の中央値を見つけるためのフレームワーク。そして、この本は2009年6月15日まで入手できません。重要なのは、これは固定サイズの問題であるため、最適と思われる直接比較方法があるということです。
また、このアルゴリズムは個別に1回実行されるのではなく、何度も実行されるという事実があり、ほとんどの実行の間に27個の値のうち9個のみが変更されます。つまり、理論的には一部の作業はすでに完了しています。ただし、この事実を利用する画像処理のメディアンフィルタリングアルゴリズムについては聞いたことがありません。
nth_elementについて言及したすべての人に対して+1ですが、この種のコードは、特定のデータセットを持つ1つのCPUで実行されるその1つのコンパイラーに対して最も効率的なコードを生成したいので、手書きアルゴリズムがSTLより優れています。たとえば、一部のCPUとコンパイラの組み合わせでは、std :: swap(int、int)は、XORを使用した手書きのスワップよりも遅くなる可能性があります(返信する前に、これはおそらく20年前のことですが、 CPU固有のアセンブリコードを手書きすることでパフォーマンスが向上する場合があります。GPUのストリームプロセッサを利用する場合は、それに応じてアルゴリズムを設計する必要があります。
2つのヒープの使用について言及し、挿入時に中央値を追跡しました。それは私がプロジェクトで少し前にやったことです。アレイをインプレースで変更し、1つのヒープのみを使用しました。より高速なアルゴリズムは考えられませんでしたが、メモリ使用量、特にCPUキャッシュメモリについて注意したいと思います。メモリアクセスに注意したい。 CPUキャッシュはページごとにスワップインおよびスワップアウトされるため、CPUキャッシュミスを最小限に抑えるために、アルゴリズムは互いに近接しているメモリにアクセスする必要があります。
たとえば、中央値が必要な100万の異なる値があるとします。それらの100万人のサブセットに基づいて中央値を基にすることは可能ですか、たとえば10%とします。それで、中央値が値を2つの等しい(またはほぼ等しい)サブセットに分割するn番目の要素に近いように?したがって、中央値を見つけるために必要なのはO(n)回未満です(この場合はO(1/10n)であり、これによりO(nlogn )?
Knuthの演習5.3.3.13をご覧ください。 (3/2)n + O(n ^(2/3)log n)比較を使用してn要素の中央値を見つけるフロイドによるアルゴリズムを説明し、O(・)に隠された定数は実際には大きすぎます。
アルゴリズムを見たい場合は、Donald E. Knuthの本を調べてください。
PS。あなたがより良いものを発明したと思うなら、複雑さが既知のアルゴリズムの複雑さと同じかそれ以上であることを示すことができるはずです。 O(n)一方、バケットと基数に基づくバリエーションの場合、クイックソートはO(n.log(n)のみです。20%高速な方法でも、 O(n.log(n))アルゴリズムを表示できるまで:-)
私はあなたがそれらをゼロコストで計算できることを賭けています-ディスクからロードする間(またはそれらは生成されます)別のスレッドで。
私が本当に言っているのは、27の値ではBig O表記が本当の要因となるには不十分であるため、「速度」は少しいじるからもたらされることはないということです。