Sort data by building and merging bitonic sequences through a regular compare and exchange network.
Parallel bitonic sort is a sorting network based algorithm. It sorts by repeatedly forming bitonic sequences and then merging them into sorted order.
A bitonic sequence first increases and then decreases, or can be rotated into that shape. The algorithm works especially well on parallel hardware because the compare and exchange pattern is regular and independent of the input values.
Problem
Given an array of size , sort the elements in nondecreasing order using parallel compare and exchange steps.
For the cleanest version, assume is a power of two.
Bitonic Sequence
A sequence is bitonic when it consists of two monotone parts, one increasing and one decreasing.
Examples:
[1, 4, 8, 9, 7, 3, 2]
[9, 7, 3, 1, 2, 4, 8]Bitonic sort creates such sequences recursively, then applies a bitonic merge.
Algorithm
The recursive form sorts one half ascending and the other half descending. This creates a bitonic sequence. Then it merges the whole range in the desired direction.
bitonic_sort(A, lo, n, dir):
if n <= 1:
return
k = n / 2
parallel:
bitonic_sort(A, lo, k, ascending)
bitonic_sort(A, lo + k, k, descending)
bitonic_merge(A, lo, n, dir)The merge step compares elements separated by half the range length.
bitonic_merge(A, lo, n, dir):
if n <= 1:
return
k = n / 2
parallel for i from lo to lo + k - 1:
compare_exchange(A[i], A[i + k], dir)
parallel:
bitonic_merge(A, lo, k, dir)
bitonic_merge(A, lo + k, k, dir)Compare and Exchange
compare_exchange(x, y, ascending):
if ascending and x > y:
swap x, y
if descending and x < y:
swap x, yThis primitive is independent for many pairs, which makes the algorithm suitable for SIMD, GPU, and sorting network implementations.
Complexity
| measure | value |
|---|---|
| comparisons | |
| parallel depth | |
| extra space | for in place array version |
| best case | same as worst case |
Unlike quicksort or mergesort, bitonic sort performs the same compare and exchange schedule regardless of input order.
Correctness
The recursive sort step produces two sorted halves in opposite directions, so their concatenation is bitonic. Bitonic merge has the property that after comparing pairs separated by half the length, each half remains bitonic and all elements in the first half belong before all elements in the second half for the chosen direction. Recursively merging both halves therefore produces a sorted sequence.
Practical Considerations
- Works best when is a power of two.
- Padding can handle other sizes.
- Performs more comparisons than comparison sorts such as quicksort or merge sort.
- Regular memory access makes it attractive on GPUs and SIMD units.
- Runtime is data independent, which can help when predictable control flow matters.
When to Use
Use parallel bitonic sort when:
- the hardware favors regular compare and exchange patterns
- data size is moderate
- SIMD or GPU execution is available
- predictable branch free execution matters
- a sorting network is desired
For large CPU based sorting, parallel quicksort, parallel merge sort, or parallel sample sort usually performs less work.
Implementation (Go, simplified)
func ParallelBitonicSort(a []int) {
if len(a) <= 1 {
return
}
bitonicSort(a, 0, len(a), true)
}
func bitonicSort(a []int, lo, n int, ascending bool) {
if n <= 1 {
return
}
k := n / 2
done := make(chan struct{}, 2)
go func() {
bitonicSort(a, lo, k, true)
done <- struct{}{}
}()
go func() {
bitonicSort(a, lo+k, k, false)
done <- struct{}{}
}()
<-done
<-done
bitonicMerge(a, lo, n, ascending)
}
func bitonicMerge(a []int, lo, n int, ascending bool) {
if n <= 1 {
return
}
k := n / 2
for i := lo; i < lo+k; i++ {
compareExchange(a, i, i+k, ascending)
}
bitonicMerge(a, lo, k, ascending)
bitonicMerge(a, lo+k, k, ascending)
}
func compareExchange(a []int, i, j int, ascending bool) {
if ascending {
if a[i] > a[j] {
a[i], a[j] = a[j], a[i]
}
return
}
if a[i] < a[j] {
a[i], a[j] = a[j], a[i]
}
}