カウンティングソートを使用する

カウンティングソート (counting sort) は、要素同士を直接比較するのではなく、各値が何回現れるかを数え、その出現回数からソート後の並びを復元するソートアルゴリズムである。キーの取りうる値の範囲 k が入力長 n と同程度かそれより小さいとき、比較ソートの O(n log n) より有利になりうる。

  1. 出現回数の集計: 入力配列を走査し、値 v ごとの出現回数 count[v] を増やす。
  2. 累積和(安定版): 安定ソートにする場合は count を累積和に変換し、各値の書き込み開始位置を決める。
  3. 出力への配置: count に従い、出力配列(または元配列の上書き)へ値を順に書き込む。安定版では入力を後ろから走査して同値の相対順序を保つ。
procedure counting_sort(A)
  if length(A) = 0 then return
  minVal = minimum(A)
  maxVal = maximum(A)
  range = maxVal - minVal + 1
  count[0..range-1] = 0
  for each x in A
    count[x - minVal] = count[x - minVal] + 1
  idx = 0
  for v from 0 to range - 1
    repeat count[v] times
      A[idx] = minVal + v
      idx = idx + 1

時間計算量は O(n + k)k は値域の幅)、追加のカウント配列に O(k) の空間を要する。比較ソートの下限 O(n log n) には当てはまらない非比較ソートの代表例である。上の擬似コードは簡略版で、同値の相対順序を保つ安定実装では累積和と後方走査が加わる。

整数のように値域が狭いデータや、バケットのインデックスに直結できるキーに向く。一方で k が極端に大きいと補助配列だけでメモリを大量に消費するため、汎用の比較ソートに置き換える判断が必要になる。

基数ソートやバケットソートと同様、キーの分布や値域の広さに強く依存する。比較に基づくクイックソートやマージソートとは用途が異なり、整数のように範囲が見積もれる入力でのみ採用を検討するのが現実的である。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000001 0.000072 1665 1672
512 0.000002 0.000138 1669 1676
1024 0.000003 0.000068 1681 1688
2048 0.000007 0.000386 1705 1712
4096 0.000015 0.000413 1754 1760
8192 0.000044 0.001774 1849 1856
16384 0.000085 0.002388 1918 1924
32768 0.000173 0.000962 2180 2184
65536 0.000434 0.022050 2944 2944
131072 0.000665 0.002231 4480 4480
262144 0.001343 0.003989 7555 7664
計測に使用したコードを表示する

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(a: &mut [usize]) {
    if a.is_empty() {
        return;
    }

    let min = *a.iter().min().unwrap();
    let max = *a.iter().max().unwrap();
    let span = max - min + 1;
    let mut count = vec![0usize; span];

    for &x in a.iter() {
        count[x - min] += 1;
    }

    let mut idx = 0;

    for (offset, &cnt) in count.iter().enumerate() {
        let value = min + offset;
        for _ in 0..cnt {
            a[idx] = value;
            idx += 1;
        }
    }
}


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

    counting_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