Skip to content

Parallel Bitonic Sort

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 AA of size nn, sort the elements in nondecreasing order using parallel compare and exchange steps.

For the cleanest version, assume nn 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, y

This primitive is independent for many pairs, which makes the algorithm suitable for SIMD, GPU, and sorting network implementations.

Complexity

measurevalue
comparisonsO(nlog2n)O(n \log^2 n)
parallel depthO(log2n)O(\log^2 n)
extra spaceO(1)O(1) for in place array version
best casesame 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 nn 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]
	}
}