Samplesort is a sorting algorithm that is a divide and conquer algorithm often used in parallel processing systems. Conventional divide and conquer sorting algorithms partitions the array into sub-intervals or buckets. The buckets are then sorted individually and then concatenated together. However, if the array is non-uniformly distributed, the performance of these sorting algorithms can be significantly throttled. Samplesort addresses this issue by selecting a sample of size s from the n-element sequence, and determining the range of the buckets by sorting the sample and choosing "p"−1 ; "s" elements from the result. These elements (called splitters) then divide the array into p approximately equal-sized buckets. Samplesort is described in the 1970 paper, "Samplesort: A Sampling Approach to Minimal Storage Tree Sorting", by W. D. Frazer and A. C. McKellar.
Algorithm.
Samplesort is a generalization of quicksort. Where quicksort partitions its input into two parts at each step, based on a single value called the pivot, samplesort instead takes a larger sample from its input and divides its data into buckets accordingly. Like quicksort, it then recursively sorts the buckets.
To devise a samplesort implementation, one needs to decide on the number of buckets p. When this is done, the actual algorithm operates in three phases:
The full sorted output is the concatenation of the buckets.
A common strategy is to set p equal to the number of processors available. The data is then distributed among the processors, which perform the sorting of buckets using some other, sequential, sorting algorithm.
Pseudocode.
The following listing shows the above mentioned three step algorithm as pseudocode and shows how the algorithm works in principle. In the following, A is the unsorted data, k is the oversampling factor, discussed later, and p is the number of splitters.
 function sampleSort(A[1..n], k, p)
 // if average bucket size is below a threshold switch to e.g. quicksort
 if "n" / "k" ; threshold then smallSort(A) 
 /* Step 1 */
 select "S" = ["S"1, ..., "S"("p"−1)"k"] randomly from // select samples
 sort S // sort sample
 ["s"0, "s"1, ..., s"p"−1, "s""p"] ;- [-∞, "S""k", "S"2"k", ..., "S"("p"−1)"k", ∞] // select splitters
 /* Step 2 */
 for each "a" in "A"
 find j such that "s""j"−1 ; "a" ;= "s""j"
 place a in bucket "b""j"
 /* Step 3 and concatenation */
 return concatenate(sampleSort("b"1), ..., sampleSort("b""k"))
The pseudo code is different from the original Frazer and McKellar algorithm. In the pseudo code, samplesort is called recursively. Frazer and McKellar called samplesort just once and used quicksort in all following iterations.
Complexity.
The complexity, given in Big O notation, for a parallelized implementation with formula_1 processors:
Find the splitters.
formula_2
Send to buckets.
formula_3 for reading all nodes
formula_4 for broadcasting
formula_5 for binary search for all keys
formula_6 to send keys to bucket
Sort buckets. 
formula_7 where formula_8 is the complexity of the underlying sequential sorting method. Often formula_9.
The number of comparisons, performed by this algorithm, approaches the information theoretical optimum formula_10 for big input sequences. In experiments, conducted by Frazer and McKellar, the algorithm needed 15% fewer comparisons than quicksort.
Sampling the data.
The data may be sampled through different methods. Some methods include:
Oversampling.
The oversampling ratio determines how many times more data elements to pull as samples, before determining the splitters. The goal is to get a good representation of the distribution of the data. If the data values are widely distributed, in that there are not many duplicate values, then a small sampling ratio is sufficient. In other cases where there are many duplicates in the distribution, a larger oversampling ratio will be necessary. In the ideal case, after step 2, each bucket contains formula_11 elements. In this case, no bucket takes longer to sort than the others, because all buckets are of equal size.
After pulling formula_12 times more samples than necessary, the samples are sorted. Thereafter, the splitters used as bucket boundaries are the samples at position formula_13 of the sample sequence (together with formula_14 and formula_15 as left and right boundaries for the left most and right most buckets respectively). This provides a better heuristic for good splitters than just selecting formula_1 splitters randomly.
Bucket size estimate.
With the resulting sample size, the expected bucket size and especially the probability of a bucket exceeding a certain size can be estimated. The following will show that for an oversampling factor of formula_17 the probability that no bucket has more than formula_18 elements is larger than formula_19.
To show this let formula_20 be the input as a sorted sequence. For a processor to get more than formula_21 elements, there has to exist a subsequence of the input of length formula_21, of which a maximum of S samples are picked. These cases constitute the probability formula_23. This can be represented as the random variable:
formula_24
For the expected value of formula_25 holds:
formula_26
This will be used to estimate formula_23:
formula_28
Using the Chernoff bound now, it can be shown:
formula_29
Many identical keys.
In case of many identical keys, the algorithm goes through many recursion levels where sequences are sorted, because the whole sequence consists of identical keys. This can be counteracted by introducing equality buckets. Elements equal to a pivot are sorted into their respective equality bucket, which can be implemented with only one additional conditional branch. Equality buckets are not further sorted. This works, since keys occurring more than formula_30 times are likely to become pivots.
Uses in parallel systems.
Samplesort is often used in parallel systems, including distributed systems such as bulk synchronous parallel machines. 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 formula_1. Samplesort is efficient in parallel systems because each processor receives approximately the same bucket size formula_11. 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 formula_12 elements on each processor, sorting the resulting formula_34 elements with a distributed sorting algorithm, taking every formula_12-th element and broadcasting the result to all processors. This costs formula_36 for sorting the formula_34 elements on formula_1 processors, as well as formula_39 for distributing the formula_1 chosen splitters to formula_1 processors.
With the resulting splitters, each processor places its own input data into local buckets. This takes formula_42 with binary search. Thereafter, the local buckets are redistributed to the processors. Processor formula_43 gets the local buckets formula_44 of all other processors and sorts these locally. The distribution takes formula_45 time, where formula_46 is the size of the biggest bucket. The local sorting takes formula_47.
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. On latter-day GPUs, the algorithm may be less effective than its alternatives.
Efficient Implementation of Samplesort.
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". The implementation proposed in the paper uses two arrays of size formula_48 (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.
In each recursion step, the data gets copied to the other array in a partitioned fashion. If the data is in the temporary array in the last recursion step, then the data is copied back to the original array.
Determining buckets.
In a comparison based sorting algorithm the comparison operation is the most performance critical part. In Samplesort this corresponds to determining the bucket for each element. This needs formula_49 time for each element.
Super Scalar Sample Sort uses a balanced search tree which is implicitly stored in an array t. The root is stored at 0, the left successor of formula_50 is stored at formula_51 and the right successor is stored at formula_52. Given the search tree t, the algorithm calculates the bucket number j of element formula_53 as follows (assuming formula_54 evaluates to 1 if it is "true" and 0 otherwise):
 "j" := 1
 repeat log2("p") times
 "j" := 2"j" + ("a" ; "t""j")
 "j" := "j" − "p" + 1
Since the number of buckets k is known at compile time, this loop can be unrolled by the compiler. The comparison operation is implemented with predicated instructions. Thus, there occur no branch mispredictions, which would slow down the comparison operation significantly.
Partitioning.
For an efficient partitioning of the elements, the algorithm needs to know the sizes of the buckets in advance. To partition the elements of the sequence and put them into the array, we need to know the size of the buckets in advance. A naive algorithm could count the number of elements of each bucket. Then the elements could be inserted to the other array at the right place. Using this, one has to determine the bucket for each elements twice (one time for counting the number of elements in a bucket, and one time for inserting them).
To avoid this doubling of comparisons, Super Scalar Sample Sort uses an additional array formula_55 (called oracle) which assigns each index of the elements to a bucket. First, the algorithm determines the contents of formula_55 by determining the bucket for each element and the bucket sizes, and then placing the elements into the bucket determined by formula_55. The array formula_55 also incurs cost in storage space, but as it only needs to store formula_59 bits, these cost are small compared to the space of the input array.
In-place samplesort.
A key disadvantage of the efficient Samplesort implementation shown above is that it is not in-place and requires a second temporary array of the same size as the input sequence during sorting. Efficient implementations of e.g. quicksort are in-place and thus more space efficient. However, Samplesort can be implemented in-place as well.
The in-place algorithm is separated into four phases:
One obvious disadvantage of this algorithm is that it reads and writes every element twice, once in the classification phase and once in the block permutation phase. However, the algorithm performs up to three times faster than other state of the art in-place competitors and up to 1.5 times faster than other state of the art sequential competitors. As sampling was already discussed above, the three later stages will be further detailed in the following.
Local classification.
In a first step, the input array is split up into formula_1 stripes of blocks of equal size, one for each processor. Each processor additionally allocates formula_12 buffers that are of equal size to the blocks, one for each bucket. Thereafter, each processor scans its stripe and moves the elements into the buffer of the according bucket. If a buffer is full, the buffer is written into the processors stripe, beginning at the front. There is always at least one buffer size of empty memory, because for a buffer to be written (i.e. buffer is full), at least a whole buffer size of elements more than elements written back had to be scanned. Thus, every full block contains elements of the same bucket. While scanning, the size of each bucket is kept track of.
Block permutation.
Firstly, a prefix sum operation is performed that calculates the boundaries of the buckets. However, since only full blocks are moved in this phase, the boundaries are rounded up to a multiple of the block size and a single overflow buffer is allocated. Before starting the block permutation, some empty blocks might have to be moved to the end of its bucket. Thereafter, a write pointer formula_62 is set to the start of the bucket formula_44 subarray for each bucket and a read pointer formula_64 is set to the last non empty block in the bucket formula_44 subarray for each bucket.
To limit work contention, each processor is assigned a different primary bucket formula_66 and two swap buffers that can each hold a block. In each step, if both swap buffers are empty, the processor decrements the read pointer formula_67 of its primary bucket and reads the block at formula_68 and places it in one of its swap buffers. After determining the destination bucket formula_69 of the block by classifying the first element of the block, it