アメリカ国旗ソートを使用する

アメリカ国旗ソート (American flag sort) はキーを固定幅の記号列(典型例は 1 バイト単位)として見なし、現在位置の記号値ごとにカウンティングしてバケット境界を決め、インプレースの交換で各要素を対応バケットへ集める処理を再帰的に実行するソートアルゴリズムである。

名称は Dijkstra の「オランダ国旗問題」に由来する 3 区分のインプレース分割と、星条旗の複数色帯を連想させる MSD の多区分分割の対応から来ている。基数ソートや文字列向け MSD 整列の実装で、補助配列を増やさずに済ませたい場面に用いられる。

  1. 記号位置の選択: 最上位バイト(または桁)から処理する。部分配列が十分小さければ挿入ソートなどで終える。
  2. 出現回数の集計: 現在位置の記号 0..σ-1(バイトなら σ = 256)について各出現数を数える。
  3. バケット境界の確定: 累積和から各記号の区間 [start, end) を求める。
  4. インプレース配置: 記号 r の区間を左から走査し、属する記号が r でなければ先頭未確定要素と交換して前進させ、すべて r に揃えたら次の記号へ進む。
  5. 再帰: 要素が 2 個以上残った各区間について、次の記号位置で手順 1〜4 を繰り返す。
procedure american_flag_sort(A, byte)
  if length(A) <= THRESHOLD then
    insertion_sort(A)
    return
  if byte >= key_width then
    return
  count[0..σ-1] = 0
  for each x in A
    count[digit(x, byte)]++
  offset[0] = 0
  for r from 1 to σ - 1
    offset[r] = offset[r - 1] + count[r - 1]
  begin[r] = offset[r]
  end[r] = offset[r] + count[r]
  for r from 0 to σ - 1
    if count[r] = 0 then continue
    while begin[r] < end[r]
      b = digit(A[begin[r]], byte)
      if b ≠ r then
        end[b] = end[b] - 1
        swap(A[begin[r]], A[end[b]])
      else
        begin[r] = begin[r] + 1
    american_flag_sort(A[offset[r]..offset[r] + count[r]), byte + 1)

整数キーを usize として整列するときは、最上位バイトから下位バイトへと digit(x, byte) を取り、記号集合サイズ σ = 256 として上記を適用するのが典型である(下の計測コードもこの方式)。

記号幅を w、部分配列サイズを n とすると、各パスはカウンティングとインプレース配置で O(n + σ)、全体は O(w · (n + σ)) 程度と見積もれる。追加配列はカウンティング用の固定サイズ O(σ) が主で、要素数に比例する補助バッファは不要なインプレース志向の実装が多い。等しいキーの相対順序は一般に保証されない(不安定)。

以下のデモでは視認性のため十進の各桁(σ = 10)を上位桁から同じ手順で示す。バイト列版と違いは記号の取り方だけで、バケット形成と再帰の流れは同じである。

文字列整列ではバイト列をそのまま記号列とみなせるため、可変長文字列の MSD 整列や sort 系ユーティリティの内部実装の一要素として言及されることがある。整数だけを扱う場合でも、固定幅のビット列/バイト列として MSD を適用する点で基数ソート(LSD)やカウンティングソートと設計思想が近い。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000012 0.000061 1739 1744
512 0.000017 0.000051 1743 1748
1024 0.000026 0.000079 1751 1756
2048 0.000043 0.000116 1767 1772
4096 0.000078 0.000152 1799 1804
8192 0.000150 0.000373 1863 1868
16384 0.000296 0.000522 1995 2000
32768 0.000599 0.001146 2255 2260
65536 0.001247 0.003208 2767 2772
131072 0.003073 0.009491 3791 3796
262144 0.006327 0.010087 5839 5844
計測に使用したコードを表示する

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 insertion_sort(a: &mut [usize]) {
    for i in 1..a.len() {
        let mut j = i;
        while j > 0 && a[j - 1] > a[j] {
            a.swap(j - 1, j);
            j -= 1;
        }
    }
}



fn american_flag_sort_bytes(a: &mut [usize], byte: usize) {
    const W: usize = 256;
    const THRESHOLD: usize = 16;

    if a.len() <= THRESHOLD {
        insertion_sort(a);
        return;
    }
    if byte >= std::mem::size_of::<usize>() {
        return;
    }

    let shift = (std::mem::size_of::<usize>() - 1 - byte) * 8;

    let mut count = [0usize; W];
    for &value in a.iter() {
        count[((value >> shift) & 0xFF) as usize] += 1;
    }

    let mut begin = [0usize; W];
    let mut sum = 0usize;
    for i in 0..W {
        begin[i] = sum;
        sum += count[i];
    }

    let initial = begin;
    let mut end = [0usize; W];
    for i in 0..W {
        end[i] = begin[i] + count[i];
    }

    for bucket in 0..W {
        if count[bucket] == 0 {
            continue;
        }

        while begin[bucket] < end[bucket] {
            let digit = ((a[begin[bucket]] >> shift) & 0xFF) as usize;
            if digit != bucket {
                end[digit] -= 1;
                a.swap(begin[bucket], end[digit]);
            } else {
                begin[bucket] += 1;
            }
        }

        let start = initial[bucket];
        if count[bucket] > 1 {
            american_flag_sort_bytes(&mut a[start..start + count[bucket]], byte + 1);
        }
    }
}

fn american_flag_sort(a: &mut [usize]) {
    if !a.is_empty() {
        american_flag_sort_bytes(a, 0);
    }
}


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

    american_flag_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