双方向挿入ソートを使用する

双方向挿入ソート (two-way insertion sort) は、整列済み区間へ要素を取り込み、新しい値が最小側・最大側・中間のどれに当たるかで挿入方向を選ぶ。

挿入ソートと同系統の比較ソートである。

  1. 整列済み領域: 先頭要素だけを整列済みとみなし、last = 0 から始める。
  2. 左端への挿入: 位置 i の値が A[0] より小さいとき、区間 [0, last] を右へ1つずつずらし、空いた先頭へ置く。
  3. 右端への挿入: 値が A[last] 以上のとき、last を1つ伸ばし、右端側へシフトしてから A[last] に置く。
  4. 中間への挿入: それ以外は [0, last] 内を線形探索し、見つけた位置の右側をシフトして挿入する。
  5. 終了: すべての i について繰り返すと [0, last] が配列全体となり昇順になる。
procedure two_way_insertion_sort(A)
  n = length(A)
  if n <= 1 then
    return
  last = 0
  for i from 1 to n - 1
    if A[i] < A[0] then
      key = A[i]
      for j from last down to 0
        A[j + 1] = A[j]
      A[0] = key
      last = last + 1
    else if A[i] >= A[last] then
      last = last + 1
      key = A[i]
      for j from i - 1 down to last
        A[j + 1] = A[j]
      A[last] = key
    else
      k = last
      while A[k] > A[i]
        k = k - 1
      key = A[i]
      for j from i - 1 down to k + 1
        A[j + 1] = A[j]
      A[k + 1] = key
      last = last + 1

整列済みに近い入力では右端への追記が多く、逆順に近いと左端への挿入が多くなる。最悪計算量 O(n²) となる。

類似アルゴリズムとの相違点

挿入ソートは左端からだけ接頭辞を伸ばす。双方向は最小なら左端、最大なら右端へ先に追記する分岐を持つ。二分挿入ソートは挿入位置の探索を二分に置き換える。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000012 0.000649 1662 1668
512 0.000048 0.000613 1666 1672
1024 0.000184 0.002053 1673 1680
2048 0.000713 0.005327 1690 1696
4096 0.002678 0.009174 1721 1728
8192 0.010626 0.024431 1786 1792
16384 0.041445 0.142906 1918 1924
32768 0.170173 0.729362 2177 2184
計測に使用したコードを表示する

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 = 15;
const RUNS: usize = 8192;


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

    let mut last = 0usize;

    for i in 1..a.len() {
        if a[i] < a[0] {
            let key = a[i];
            for j in (0..=last).rev() {
                a[j + 1] = a[j];
            }
            a[0] = key;
            last += 1;
        } else if a[i] >= a[last] {
            last += 1;
            let key = a[i];
            for j in (last..i).rev() {
                a[j + 1] = a[j];
            }
            a[last] = key;
        } else {
            let mut k = last;
            while a[k] > a[i] {
                k -= 1;
            }
            let key = a[i];
            for j in (k + 1..i).rev() {
                a[j + 1] = a[j];
            }
            a[k + 1] = key;
            last += 1;
        }
    }
}


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

    two_way_insertion_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 {
    // VmHWM (peak RSS, KiB). Reported memory subtracts a per-size baseline that only
    // holds the input array, so the table reflects auxiliary space during sorting.
    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 input_array(size: usize, seed: u64) -> Vec<usize> {
    shuffled(size, seed)
}

fn run_baseline(size: usize) -> usize {
    let _hold = input_array(size, 1);
    memory_usage_kb()
}

fn run_once(size: usize, seed: usize) -> (u128, usize) {
    let mut array = input_array(size, seed as u64);

    let start = Instant::now();

    benchmark_sort(&mut array);

    let elapsed = start.elapsed();
    let mem = memory_usage_kb();

    let expected: Vec<usize> = (1..=size).collect();
    if array != expected {
        panic!(
            "sort failed with seed {} for size {}",
            seed,
            size
        );
    }

    (micros(elapsed), mem)
}

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

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 == "--baseline-once") {
        run_baseline_child(&args);
        return;
    }
    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 baseline_output = Command::new(env::current_exe().expect("failed to find current executable"))
            .arg("--baseline-once")
            .arg(size.to_string())
            .output()
            .expect("failed to run benchmark baseline process");

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

        let baseline_stdout = String::from_utf8(baseline_output.stdout)
            .expect("baseline process returned non-UTF-8 output");
        let baseline_mem = baseline_stdout
            .split_whitespace()
            .next()
            .expect("missing baseline memory usage")
            .parse::<usize>()
            .expect("invalid baseline memory usage");

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

            let aux_mem = mem.saturating_sub(baseline_mem);

            total_mem += aux_mem;

            if aux_mem > max_mem {
                max_mem = aux_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