Samplesort は、並列処理システムでよく使用される分割統治アルゴリズムであるソートアルゴリズムです。 [ 1 ]従来の分割統治ソートアルゴリズムは、配列をサブインターバルまたはバケットに分割します。次に、バケットを個別にソートし、それらを連結します。しかし、配列が不均一に分布している場合、これらのソートアルゴリズムのパフォーマンスは大幅に低下する可能性があります。Samplesort は、n個の要素のシーケンスからサイズsのサンプルを選択し、サンプルをソートして結果からp −1 < s個の要素を選択することでバケットの範囲を決定することにより、この問題を解決します。これらの要素 (スプリッターと呼ばれます) は、配列をp 個のほぼ等しいサイズのバケットに分割します。[ 2 ] Samplesort は、WD Frazer と AC McKellar による 1970 年の論文「Samplesort: A Sampling Approach to Minimal Storage Tree Sorting」で説明されています。[ 3 ]
サンプルソートはクイックソートの一般化です。クイックソートは、ピボットと呼ばれる単一の値に基づいて、各ステップで入力を2つの部分に分割しますが、サンプルソートは入力からより大きなサンプルを取り出し、それに応じてデータをバケットに分割します。クイックソートと同様に、その後、バケットを再帰的にソートします。
サンプルソートの実装を考案するには、バケットの数pを決定する必要があります。これが完了すると、実際のアルゴリズムは 3 つのフェーズで動作します。[ 4 ]
完全にソートされた出力は、バケットを連結したものです。
一般的な戦略としては、pを利用可能なプロセッサの数に設定する方法がある。データはプロセッサ間で分散され、各プロセッサは別の逐次的なソートアルゴリズムを用いてバケットのソートを実行する。
以下のリストは、上述の3ステップアルゴリズムを擬似コードとして示し、アルゴリズムが原理的にどのように動作するかを示しています。[ 5 ]以下では、Aはソートされていないデータ、kは後述するオーバーサンプリング係数、pはスプリッターの数です。
function sampleSort(A[1..n], k , p ) // 平均バケットサイズがしきい値を下回る場合は、例えばクイックソートに切り替える n / k < しきい値の場合、 smallSort(A)を実行します。 /* ステップ 1 */ select S = [ S 1 , ..., S ( p −1) k ] randomly from // サンプルを選択 ソートS // ソート例 [ s 0 , s 1 , ..., s p −1 , s p ] <- [-∞, S k , S 2 k , ..., S ( p −1) k , ∞] // スプリッタを選択 /* ステップ 2 */ Aの各aについて、 s j −1 < a <= s jを満たすjを 見つけ、 a をバケットb jに 入れる。 /* ステップ 3 と連結 */ return concatenate(sampleSort( b 1 ), ..., sampleSort( b k ))
擬似コードは、元の Frazer と McKellar のアルゴリズムとは異なります。[ 3 ]擬似コードでは、samplesort が再帰的に呼び出されます。Frazer と McKellar は samplesort を一度だけ呼び出し、以降のすべての反復でquicksort を使用しました。
並列化された実装の複雑さは、ビッグオー記法で表され、プロセッサ:
スプリッターを見つけてください。
バケットに送信する。
ソートバケット。
このアルゴリズムによって実行される比較回数は、情報理論上の最適値に近づく。大きな入力シーケンスの場合、このアルゴリズムはクイックソートよりも比較回数が15%少ない。フレイザーとマッケラーが行った実験では、クイックソートよりも15%少ない比較回数で済むことが示された。
データはさまざまな方法でサンプリングされる可能性があります。いくつかの方法には以下が含まれます。
オーバーサンプリング比率は、スプリッターを決定する前に、サンプルとして取得するデータ要素の数を決定します。目標は、データの分布を適切に表現することです。データ値が広く分布していて、重複値が少ない場合は、小さなサンプリング比率で十分です。分布に重複が多い場合は、より大きなオーバーサンプリング比率が必要になります。理想的な場合、ステップ2の後、各バケットには要素。この場合、すべてのバケットのサイズが同じであるため、どのバケットも他のバケットよりもソートに時間がかかることはありません。
引っ張った後必要以上に多くのサンプルが処理され、サンプルはソートされます。その後、バケット境界として使用されるスプリッターは、位置にあるサンプルです。サンプル配列の(そして(それぞれ左端と右端のバケットの左境界と右境界として)。これは、単に選択するよりも優れたスプリッターのためのより良いヒューリスティックを提供する。スプリッターはランダムに動作します。
得られたサンプルサイズから、期待されるバケットサイズ、特にバケットが特定のサイズを超える確率を推定できます。以下では、オーバーサンプリング係数の場合、どのバケツにもそれ以上のものが入らない確率要素はより大きい。
これを示すために入力はソートされたシーケンスとして扱われます。プロセッサがそれ以上のものを処理するためには要素、入力の長さの部分列が存在する必要があるそのうち最大S個のサンプルが選ばれる。これらのケースは確率を構成する。これは、次の確率変数として表すことができます。
期待値については保持する:
これは推定に使用されます:
チェルノフ限界を用いると、次のことが示される。
同一キーが多数存在する場合、シーケンス全体が同一キーで構成されているため、アルゴリズムはシーケンスをソートする多くの再帰レベルを経ます。これは、等価バケットを導入することで対処できます。ピボットと等しい要素は、それぞれの等価バケットにソートされます。これは、条件分岐を1つ追加するだけで実装できます。等価バケットはそれ以上ソートされません。これは、キーが1つ以上出現する場合に機能します。時代は転換点となる可能性が高い。

Samplesort is often used in parallel systems, including distributed systems such as bulk synchronous parallel machines.[6][4][7] Due to the variable amount of splitters (in contrast to only one pivot in Quicksort), Samplesort is very well suited and intuitive for parallelization and scaling. Furthermore, Samplesort is also more cache-efficient than implementations of e.g. quicksort.
Parallelization is implemented by splitting the sorting for each processor or node, where the number of buckets is equal to the number of processors . Samplesort is efficient in parallel systems because each processor receives approximately the same bucket size . Since the buckets are sorted concurrently, the processors will complete the sorting at approximately the same time, thus not having a processor wait for others.
On distributed systems, the splitters are chosen by taking elements on each processor, sorting the resulting elements with a distributed sorting algorithm, taking every -th element and broadcasting the result to all processors. This costs for sorting the elements on processors, as well as for distributing the chosen splitters to processors.
With the resulting splitters, each processor places its own input data into local buckets. This takes with binary search. Thereafter, the local buckets are redistributed to the processors. Processor gets the local buckets of all other processors and sorts these locally. The distribution takes time, where is the size of the biggest bucket. The local sorting takes .
Experiments performed in the early 1990s on Connection Machine supercomputers showed samplesort to be particularly good at sorting large datasets on these machines, because its incurs little interprocessor communication overhead.[8] On latter-day GPUs, the algorithm may be less effective than its alternatives.[9]

As described above, the samplesort algorithm splits the elements according to the selected splitters. An efficient implementation strategy is proposed in the paper "Super Scalar Sample Sort".[5] The implementation proposed in the paper uses two arrays of size (the original array containing the input data and a temporary one) for an efficient implementation. Hence, this version of the implementation is not an in-place algorithm.
各再帰ステップにおいて、データは分割された形式で別の配列にコピーされます。最後の再帰ステップでデータが一時配列にある場合、データは元の配列にコピーされます。
比較ベースのソートアルゴリズムでは、比較演算がパフォーマンス上最も重要な部分です。Samplesortでは、これは各要素のバケットを決定することに相当します。これには各要素にかかる時間。
スーパースカラーサンプルソートは、配列tに暗黙的に格納されるバランスのとれた探索木を使用します。ルートは 0 に格納され、左の次の要素は保存場所そして、正しい後継者は次の場所に保存されます。探索木tが与えられた場合、アルゴリズムは要素のバケット番号jを計算します。以下のように((真であれば1 、そうでなければ0と評価される):
j := 1 log 2 ( p ) を 繰り返すj := 2j + ( a > tj ) j := j − p + 1
バケット数kはコンパイル時に既知であるため、このループはコンパイラによって展開できます。比較演算は述語付き命令で実装されています。そのため、比較演算を著しく遅くする分岐予測の誤りは発生しません。
要素を効率的に分割するには、アルゴリズムはバケットのサイズを事前に知っておく必要があります。シーケンスの要素を分割して配列に格納するには、バケットのサイズを事前に知っておく必要があります。単純なアルゴリズムでは、各バケットの要素数を数えることができます。その後、要素を適切な場所に別の配列に挿入できます。この方法では、各要素のバケットを2回決定する必要があります(1回はバケット内の要素数を数えるため、もう1回は要素を挿入するため)。
比較回数の重複を避けるため、スーパースカラーサンプルソートは追加の配列を使用します。(オラクルと呼ばれる)は、要素の各インデックスをバケットに割り当てます。まず、アルゴリズムは、各要素のバケットとバケットのサイズを決定し、次に要素を決定されたバケットに配置することによって配列保管スペースにもコストがかかりますが、保管する必要があるのはビット単位の場合、これらのコストは入力配列のスペースに比べて小さい。
上記の効率的な Samplesort 実装の主な欠点は、インプレースではないため、ソート中に入力シーケンスと同じサイズの 2 番目の一時配列が必要になることです。クイックソートなどの効率的な実装はインプレースであるため、よりスペース効率が良いです。ただし、Samplesort もインプレースで実装できます。[ 10 ]
インプレースアルゴリズムは4つのフェーズに分かれています。
このアルゴリズムの明らかな欠点の1つは、分類フェーズとブロック順列フェーズでそれぞれ1回ずつ、すべての要素を2回読み書きすることです。しかし、このアルゴリズムは、他の最先端のインプレース方式の競合アルゴリズムよりも最大3倍、他の最先端のシーケンシャル方式の競合アルゴリズムよりも最大1.5倍高速に動作します。サンプリングについては既に上で説明したので、後の3つのステージについては以下でさらに詳しく説明します。
最初のステップでは、入力配列を分割します。同じサイズのブロックのストライプが、プロセッサごとに1つずつ割り当てられます。各プロセッサはさらにブロックと同じサイズのバッファが、バケットごとに1つずつ用意されます。その後、各プロセッサは自身のストライプをスキャンし、要素を対応するバケットのバッファに移動します。バッファがいっぱいの場合、バッファの内容は先頭からプロセッサのストライプに書き込まれます。バッファに書き込む(つまりバッファがいっぱいになる)には、書き戻される要素よりも少なくともバッファサイズ分の要素をスキャンする必要があるため、常に少なくとも1つのバッファサイズの空きメモリが存在します。したがって、すべての満杯のブロックには、同じバケットの要素が含まれます。スキャン中は、各バケットのサイズが追跡されます。
まず、バケットの境界を計算するプレフィックス和演算が実行されます。ただし、このフェーズでは完全なブロックのみが移動するため、境界はブロックサイズの倍数に切り上げられ、単一のオーバーフローバッファが割り当てられます。ブロック順列を開始する前に、いくつかの空のブロックをバケットの末尾に移動する必要がある場合があります。その後、書き込みポインタがバケットの開始位置に設定されます各バケットのサブ配列と読み取りポインタバケット内の最後の空でないブロックに設定されます各バケットに対応するサブ配列。
作業の競合を制限するため、各プロセッサには異なるプライマリバケットが割り当てられます。そして、それぞれ1ブロックを格納できる2つのスワップバッファがある。各ステップで、両方のスワップバッファが空の場合、プロセッサは読み取りポインタをデクリメントする。プライマリバケットのブロックを読み取ります。そして、それをスワップバッファの1つに配置します。宛先バケットを決定した後ブロックの最初の要素を分類することで、書き込みポインタが増加します。ブロックを読み取ります別のスワップバッファにブロックを書き込み、その宛先バケットにブロックを書き込みます。スワップバッファは再び空になります。そうでない場合は、スワップバッファに残っているブロックを宛先バケットに挿入する必要があります。
プロセッサのプライマリバケットのサブアレイ内のすべてのブロックが正しいバケットにある場合、次のバケットがプライマリバケットとして選択されます。プロセッサがすべてのバケットを一度プライマリバケットとして選択した場合、そのプロセッサの処理は終了します。
ブロック順列フェーズではブロック全体のみが移動されるため、一部の要素がバケット境界付近に誤って配置される可能性があります。配列には各要素のための十分なスペースが必要であるため、これらの誤って配置された要素は、オーバーフローバッファを考慮した上で、左から右へと空きスペースに移動されます。
フレイザーとマッケラーのサンプルソートとその派生手法:
並列コンピュータでの使用に適応: