自己インデックスソートを使用する

自己インデックスソート (Self-Indexed Sort, SIS) はキー値をソート空間 (sorting space) 内の相対オフセットとして直接使い、整列結果をそこへ散在配置したあと、元の配列へ順序保持圧縮して戻す 3 段階で動く非比較ソートである。

  1. ソート空間の初期化: キーの取りうる値域 [min, max] に対応する長さ m = max - min + 1 の補助配列 ss を用意し、空(またはゼロ)にする。
  2. 自己インデックス配置: 入力を走査し、各キー xss[x - min] へ写す。同一キーが複数ある場合は出現回数を数える(または連鎖で衝突を解決する)。
  3. 順序保持圧縮: ssmin から昇順に走査し、非空スロットの値を左から元配列へ詰め直す。入力を左から走査して配置した実装では同値の相対順序を保てる。
procedure self_indexed_sort(A)
  if length(A) = 0 then return
  minVal = minimum(A)
  maxVal = maximum(A)
  m = maxVal - minVal + 1
  ss[0..m-1] = 0
  for each x in A
    ss[x - minVal] = ss[x - minVal] + 1
  idx = 0
  for v from 0 to m - 1
    repeat ss[v] times
      A[idx] = minVal + v
      idx = idx + 1

キーをアドレスとして扱う点は カウンティングソート と同型である。原論文は時間 O(n)・空間 O(n + m) を主張するが、フェーズ 1 と 3 で長さ m の配列を初期化・走査するため、厳密には O(n + m) の時間 と見なす解釈も一般的である(mn よりはるかに大きい値域だと不利になる)。

整数キーで m が入力長と同程度に収まるとき(下の計測も 1..n の並べ替え)に有利になりうる。比較ソートの Ω(n log n) 下限には当てはまらない。

カウンティングソート と実装が一致する場合が多いが、自己インデックスソートはキー=ソート空間内アドレスという発想と 3 フェーズの枠組みを明示する。値域 m が極端に広い整数キーでは補助配列だけでメモリを消費するため、汎用比較ソートへの切り替えを検討する必要がある。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000001 0.000077 1682 1688
512 0.000002 0.000044 1685 1692
1024 0.000004 0.000039 1698 1704
2048 0.000008 0.000054 1722 1728
4096 0.000016 0.000071 1770 1776
8192 0.000046 0.000210 1866 1872
16384 0.000094 0.001117 1934 1940
32768 0.000180 0.000569 2243 2276
65536 0.000363 0.000562 3012 3044
131072 0.000744 0.005917 4547 4580
262144 0.001589 0.007850 7622 7764
計測に使用したコードを表示する

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 self_indexed_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;

    // Phase 1: initialize sorting space
    let mut ss = vec![0usize; span];

    // Phase 2: self-indexed arrangement (key maps to offset in ss)
    for &x in a.iter() {
        ss[x - min] += 1;
    }

    // Phase 3: order-preserved compression back into the original array
    let mut idx = 0;

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


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

    self_indexed_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