基数ソートを使用する

基数ソート (radix sort) は、キーを桁(または固定幅のビット列)ごとに分割し、各桁についてカウンティングソートなどの安定な部分ソートを繰り返す非比較ソートである。最下位桁から順に処理する LSD(Least Significant Digit)方式がよく用いられる。

  1. 桁の決定: 最大値から必要な桁数(または基数 r に対するパス数)を求める。
  2. 桁ごとの安定ソート: 現在の桁 exp(1, 10, 100, …)について、各要素のその桁の値 0..r-1 をキーに安定なカウンティングソートを行う。
  3. 桁の更新: exp を基数倍し、最上位桁まで 2 を繰り返す。
procedure radix_sort(A)
  if length(A) = 0 then return
  maxVal = maximum(A)
  exp = 1
  while maxVal / exp > 0 do
    stable_counting_sort_by_digit(A, exp)
    exp = exp * 10

各桁パスはカウンティングソートと同様に、出現回数の集計・累積和・後方からの配置で構成される。LSD かつ各パスが安定であれば、上位桁の整列結果を下位桁のソートが壊さないため、全体が昇順になる。

整数を十進の各桁で処理する場合、桁数を d、基数を r(十進なら r = 10)とすると時間計算量は O(d · (n + r))、 補助配列を含む空間計算量は O(n + r) である。値域が n に対して多桁でも、桁数 dlog_r(max) 程度に抑えられるとき、 カウンティングソート単体より値域が広いキーにも適用しやすい。文字列や固定長キー、ビット列を基数 256 などで処理する MSD(Most Significant Digit)版もある。

カウンティングソートと同様、キーの表現と基数の選び方に依存する。負の数や浮動小数点は符号・指数・仮数部への分解など前処理が必要になる。

計算時間量および空間計算量を計測する

Size Average time Maximum time Average memory Maximum memory
256 0.000002 0.000027 1666 1672
512 0.000003 0.000528 1670 1676
1024 0.000011 0.000583 1682 1688
2048 0.000019 0.000594 1706 1712
4096 0.000034 0.000585 1754 1760
8192 0.000073 0.001300 1849 1856
16384 0.000216 0.000508 2046 2052
32768 0.000388 0.001164 2434 2440
65536 0.000742 0.001181 3201 3208
131072 0.001972 0.005915 4738 4744
262144 0.004096 0.012222 7814 7944
計測に使用したコードを表示する

set -euo pipefail

WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT

cat > "$WORKDIR/Dockerfile" <<'EOF'
FROM rust:1.95.0

WORKDIR /app

RUN mkdir -p src

RUN cat > Cargo.toml <<'CARGO'
[package]
name = "rust-benchmark"
version = "0.1.0"
edition = "2021"

[profile.release]
lto = true
codegen-units = 1
panic = "abort"
CARGO

RUN cat > src/main.rs <<'RUST'
use std::{
    env,
    process::Command,
    time::{Duration, Instant},
};
const MIN_POWER: u32 = 8;
const MAX_POWER: u32 = 18;
const RUNS: usize = 8192;


fn counting_sort_by_digit(a: &mut [usize], exp: usize) {
    let n = a.len();
    let mut output = vec![0usize; n];
    let mut count = [0usize; 10];

    for &x in a.iter() {
        count[(x / exp) % 10] += 1;
    }

    for i in 1..10 {
        count[i] += count[i - 1];
    }

    for i in (0..n).rev() {
        let digit = (a[i] / exp) % 10;
        count[digit] -= 1;
        output[count[digit]] = a[i];
    }

    a.copy_from_slice(&output);
}

fn radix_sort(a: &mut [usize]) {
    if a.is_empty() {
        return;
    }

    let max = *a.iter().max().unwrap();
    let mut exp = 1usize;

    while max / exp > 0 {
        counting_sort_by_digit(a, exp);
        exp *= 10;
    }
}


fn benchmark_sort(array: &mut [usize]) {

    radix_sort(array);

}

fn shuffled(size: usize, seed: u64) -> Vec<usize> {
    let mut v: Vec<usize> = (1..=size).collect();

    let mut state = seed;

    for i in (1..size).rev() {
        state ^= state << 13;
        state ^= state >> 7;
        state ^= state << 17;

        let j = (state as usize) % (i + 1);

        v.swap(i, j);
    }

    v
}

fn memory_usage_kb() -> usize {
    let contents = std::fs::read_to_string("/proc/self/status")
        .unwrap_or_default();

    for line in contents.lines() {
        if let Some(rest) = line.strip_prefix("VmHWM:") {
            let kb = rest
                .split_whitespace()
                .next()
                .unwrap_or("0")
                .parse::<usize>()
                .unwrap_or(0);

            return kb;
        }
    }

    0
}

fn micros(d: Duration) -> u128 {
    d.as_micros()
}

fn run_once(size: usize, seed: usize) -> (u128, usize) {
    let expected: Vec<usize> = (1..=size).collect();
    let mut array = shuffled(size, seed as u64);

    let start = Instant::now();

    benchmark_sort(&mut array);

    let elapsed = start.elapsed();

    if array != expected {
        panic!(
            "sort failed with seed {} for size {}",
            seed,
            size
        );
    }

    (micros(elapsed), memory_usage_kb())
}

fn run_child(args: &[String]) {
    let size = args[2].parse::<usize>().expect("invalid size");
    let seed = args[3].parse::<usize>().expect("invalid seed");
    let (elapsed_us, mem) = run_once(size, seed);
    println!("{} {}", elapsed_us, mem);
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.get(1).is_some_and(|arg| arg == "--run-once") {
        run_child(&args);
        return;
    }

    println!(
        "| {:>10} | {:>15} | {:>15} | {:>15} | {:>15} |",
        "Size",
        "Average time",
        "Maximum time",
        "Average memory",
        "Maximum memory"
    );

    println!(
        "|{:-<11}:|{:-<16}:|{:-<16}:|{:-<16}:|{:-<16}:|",
        "",
        "",
        "",
        "",
        ""
    );

    for power in MIN_POWER..=MAX_POWER {
        let size = 1usize << power;

        let mut total_time: u128 = 0;
        let mut max_time: u128 = 0;

        let mut total_mem: usize = 0;
        let mut max_mem: usize = 0;

        for seed in 1..=RUNS {
            let output = Command::new(env::current_exe().expect("failed to find current executable"))
                .arg("--run-once")
                .arg(size.to_string())
                .arg(seed.to_string())
                .output()
                .expect("failed to run benchmark child process");

            if !output.status.success() {
                panic!(
                    "benchmark child process failed: {}",
                    String::from_utf8_lossy(&output.stderr)
                );
            }

            let stdout = String::from_utf8(output.stdout)
                .expect("child process returned non-UTF-8 output");
            let mut fields = stdout.split_whitespace();
            let elapsed_us = fields
                .next()
                .expect("missing elapsed time")
                .parse::<u128>()
                .expect("invalid elapsed time");
            let mem = fields
                .next()
                .expect("missing memory usage")
                .parse::<usize>()
                .expect("invalid memory usage");

            total_time += elapsed_us;

            if elapsed_us > max_time {
                max_time = elapsed_us;
            }

            total_mem += mem;

            if mem > max_mem {
                max_mem = mem;
            }
        }

        let avg_time = total_time / RUNS as u128;
        let avg_mem = total_mem / RUNS;

        println!(
            "| {:>10} | {:>15} | {:>15} | {:>15} | {:>15} |",
            size,
            format!("{}.{:06}", avg_time / 1_000_000, avg_time % 1_000_000),
            format!("{}.{:06}", max_time / 1_000_000, max_time % 1_000_000),
            avg_mem,
            max_mem
        );
    }
}
RUST

RUN cargo build --release

CMD ["./target/release/rust-benchmark"]
EOF

docker build -t rust-benchmark "$WORKDIR"
docker run --rm --init rust-benchmark