ブリックソートを使用する

ブリックソート (brick sort) は、隣接する2要素だけを比較し、逆順なら入れ替える操作を、奇数インデックス側と偶数インデックス側の2種類の組に分けて繰り返す比較ソートである。奇偶転置ソート (odd-even sort) やパリティソート (parity sort) とも呼ばれ、レンガを積むように交互の組で整列が進むイメージからこの名前が付いている。

  1. 偶数組: インデックス (0,1), (2,3), (4,5), … のペアを左サイドから順に見て、右の要素の方が小さければ入れ替える。
  2. 奇数組: インデックス (1,2), (3,4), (5,6), … のペアについて同様に比較・交換する。
  3. 収束: 偶数組と奇数組をまとめて 1 ラウンドとみなし、あるラウンドで一度も交換が起きなければ全体は昇順に整っているので終了する。

同一組のペアは互いに干渉しないため、ラウンド内では飛び飛びの隣接ペアを並列に処理できる。逐次実行では最悪時間計算量は O(n²)(ラウンド数は O(n)、各ラウンドで O(n) 本の隣接ペア)、追加の配列を使わず O(1) の補助記憶でよいインプレースな実装が可能である。隣接交換のみで安定なソートになる。

procedure brick_sort(A)
  n = length(A)
  sorted = false
  while not sorted
    sorted = true
    for i from 0 to n - 2 by 2
      if A[i] > A[i + 1] then
        swap(A[i], A[i + 1])
        sorted = false
    for i from 1 to n - 2 by 2
      if A[i] > A[i + 1] then
        swap(A[i], A[i + 1])
        sorted = false

バブルソートと同様に局所的な交換だけで進むが、奇数組・偶数組を交互に適用する点が異なる。並列プロセッサ向けに設計されたアルゴリズムとして知られ、各組の比較・交換は互いに独立して実行できる。

奇数組と偶数組を交互に進めるため、バブルソートの単方向走査とは異なり、小さい値も大きい値も両端へ同時に寄せやすい。偶・奇の組に分けて常に隣同士だけを見る点は、先頭から順に走査するバブルソートとの違いとしてもはっきりする。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000039 0.000651 1662 1668
512 0.000131 0.000471 1665 1672
1024 0.000443 0.000872 1673 1680
2048 0.001551 0.003336 1690 1696
4096 0.005781 0.011730 1722 1728
8192 0.020974 0.030058 1786 1792
16384 0.082163 0.191235 1918 1924
32768 0.332577 0.644139 2178 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 brick_sort(a: &mut [usize]) {
    let mut sorted = false;
    while !sorted {
        sorted = true;
        for i in (1..a.len()).step_by(2) {
            if a[i - 1] > a[i] {
                a.swap(i - 1, i);
                sorted = false;
            }
        }
        for i in (2..a.len()).step_by(2) {
            if a[i - 1] > a[i] {
                a.swap(i - 1, i);
                sorted = false;
            }
        }
    }
}


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

    brick_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