カスケードマージソートを使用する

カスケードマージソート (CascadeMergeSort) は、配列の一部を作業領域として使いながら交換ベースのマージを繰り返すインプレース・マージソートである。整列済み部分列と作業領域の内容を swap で入れ替えながらマージし、作業領域の幅を 1/2 → 1/4 → 1/8 … と段階的に狭めていく(カスケードする)点が名前の由来である。

通常のマージソートが O(n) の補助配列を要するのに対し、追加配列を使わず配列内の未整列区間を作業領域として再利用する。最後の数要素は挿入ソートで仕上げる。

  1. 初回分割: 区間 [l, u) を半分に分け、前半 [l, m) を再帰整列して結果を後半側の作業領域 [w, u)w = l + u - m)へ wsort で書き出す。
  2. カスケード併合: 作業領域 [l, w) に残った未整列部分をさらに半分ずつ整列し、整列済みの右半 [w, u)wmerge で併合する。w を中央付近へ更新し、作業領域を狭めながら繰り返す。
  3. 交換マージ: 2 つの昇順部分列 [i, m)[j, n) について、先頭同士を比較し、小さい方と作業位置 w の要素を交換して確定させる。
  4. 挿入ソート仕上げ: 作業領域が 2 要素以下になったら、残りの未整列 prefix を右方向へ交換しながら昇順 suffix へ挿入する。
procedure wmerge(A, i, m, j, n, w)
  while i < m and j < n
    if A[i] <= A[j] then
      swap(A[w], A[i]); w = w + 1; i = i + 1
    else
      swap(A[w], A[j]); w = w + 1; j = j + 1
  copy rest of [i, m) or [j, n) into A[w ..) via swap

procedure wsort(A, l, u, w)
  if u - l > 1 then
    m = floor((l + u) / 2)
    imsort(A, l, m)
    imsort(A, m, u)
    wmerge(A, l, m, m, u, w)
  else
    move A[l] into working slot A[w] by swap

procedure imsort(A, l, u)
  if u - l <= 1 then return
  m = floor((l + u) / 2)
  w = l + u - m
  wsort(A, l, m, w)
  while w - l > 2
    n = w
    w = l + floor((n - l + 1) / 2)
    wsort(A, w, n, l)
    wmerge(A, l, l + n - w, n, u, w)
  for n from w down to l + 1
    bubble A[n - 1] right into sorted suffix [n, u)

比較回数は O(n log n) に抑えられるが、要素の移動は交換に依存するため定数因子が大きく、補助配列付きマージソートより遅くなりやすい。追加記憶領域は O(1)(再帰スタックは別)で、交換の向き次第では等値キーの相対順序を保たない不安定なソートになる。

補助配列を使わない代わりに交換回数が増え、キャッシュ効率も通常のマージソートに劣りやすい。外部記憶向けのカスケードマージ(多段テープ併合)とは目的が異なり、ここでは主記憶上のインプレース整列を指す。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000008 0.000445 1662 1668
512 0.000019 0.000666 1666 1672
1024 0.000044 0.000494 1674 1680
2048 0.000095 0.000438 1690 1696
4096 0.000212 0.000707 1721 1728
8192 0.000473 0.000777 1785 1792
16384 0.001023 0.001249 1918 1924
32768 0.002249 0.004620 2177 2184
65536 0.005162 0.016857 2689 2696
131072 0.010093 0.014215 3714 3720
262144 0.021324 0.152617 5762 5768
計測に使用したコードを表示する

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 wmerge(a: &mut [usize], i: usize, m: usize, j: usize, n: usize, mut w: usize) {
    let mut i = i;
    let mut j = j;
    while i < m && j < n {
        if a[i] <= a[j] {
            a.swap(w, i);
            w += 1;
            i += 1;
        } else {
            a.swap(w, j);
            w += 1;
            j += 1;
        }
    }
    while i < m {
        a.swap(w, i);
        w += 1;
        i += 1;
    }
    while j < n {
        a.swap(w, j);
        w += 1;
        j += 1;
    }
}

fn wsort(a: &mut [usize], l: usize, u: usize, w: usize) {
    if u - l > 1 {
        let m = l + (u - l) / 2;
        imsort_range(a, l, m);
        imsort_range(a, m, u);
        wmerge(a, l, m, m, u, w);
    } else {
        let mut l = l;
        let mut w = w;
        while l < u {
            a.swap(l, w);
            l += 1;
            w += 1;
        }
    }
}

fn imsort_range(a: &mut [usize], l: usize, u: usize) {
    if u - l <= 1 {
        return;
    }
    let m = l + (u - l) / 2;
    let mut w = l + u - m;
    wsort(a, l, m, w);
    while w - l > 2 {
        let n = w;
        w = l + (n - l + 1) / 2;
        wsort(a, w, n, l);
        wmerge(a, l, l + n - w, n, u, w);
    }
    let mut n = w;
    while n > l {
        let mut m_idx = n;
        while m_idx < u && a[m_idx] < a[m_idx - 1] {
            a.swap(m_idx, m_idx - 1);
            m_idx += 1;
        }
        n -= 1;
    }
}

fn cascade_merge_sort(a: &mut [usize]) {
    if a.len() <= 1 {
        return;
    }
    imsort_range(a, 0, a.len());
}


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

    cascade_merge_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