제네릭과 트레이트 객체 비교 문서의 실험 코드다.

//! 19 제네릭과 트레이트 객체 비교: (1) 제네릭은 섞인 타입을 한 Vec에 못 담는다(E0308),
//! (2) 함수 사본 개수: 제네릭은 타입 수만큼, dyn은 하나(LLVM IR 정의 개수),
//! (3) 같은 일을 정적 디스패치·dyn·enum으로 했을 때 걸린 시간(-C opt-level=3로 따로 컴파일해 이 기계에서 잰 값).
//!     넓이를 f64로 더할 때와 u64로 더할 때를 따로 잰다.
//! (4) 타입 40개로 부를 때 목적 파일 크기와 컴파일 시간(opt-level=2, 이 기계에서 잰 값).
//! (5) 왜 f64 합에서는 차이가 작은가: 인라인된 합 루프의 어셈블리에서 합을 쌓는 레지스터 수를 센다
//!     (u64는 여러 갈래로 나눠 더하고, f64는 한 줄로 차례로 더한다. aarch64 macOS에서만 확인),
//!     실수 덧셈은 묶는 순서에 따라 결과가 달라진다는 것도 확인한다.
//! 시간은 기계와 부하에 따라 바뀌므로 시간의 크고 작음은 단언하지 않고, 계산 결과가 같은지,
//! 함수 사본 개수, 목적 파일 크기의 크고 작음만 단언한다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/19_generics-vs-trait-objects_bench.rs"

use std::collections::BTreeSet;
use std::fmt::Write as _;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};

static SNIPPET: AtomicUsize = AtomicUsize::new(0);

fn snippet_dir() -> std::path::PathBuf {
    let n = SNIPPET.fetch_add(1, Ordering::Relaxed);
    let dir = std::env::temp_dir().join(format!("vr19_{}_{}", std::process::id(), n));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

fn rustc(dir: &std::path::Path, code: &str, extra: &[&str]) -> std::process::Output {
    let file = dir.join("snippet.rs");
    std::fs::write(&file, code).unwrap();
    std::process::Command::new("rustc")
        .args(["--edition", "2024"])
        .args(extra)
        .arg("--out-dir")
        .arg(dir)
        .arg(&file)
        .output()
        .expect("rustc 실행")
}

fn count_definitions(code: &str, name: &str) -> usize {
    let dir = snippet_dir();
    let out = rustc(
        &dir,
        code,
        &["--crate-type=lib", "-C", "opt-level=2", "--emit=llvm-ir"],
    );
    assert!(
        out.status.success(),
        "{}",
        String::from_utf8_lossy(&out.stderr)
    );
    std::fs::read_to_string(dir.join("snippet.ll"))
        .unwrap()
        .lines()
        .filter(|l| l.starts_with("define") && l.contains(name))
        .count()
}

// NUM 자리에 f64나 u64를 넣어 두 번 쓴다.
const BENCH: &str = r#"
use std::hint::black_box;
use std::time::Instant;

trait Shape { fn area(&self) -> NUM; }
#[derive(Clone, Copy)] struct Circle { r: NUM }
#[derive(Clone, Copy)] struct Square { s: NUM }
impl Shape for Circle { fn area(&self) -> NUM { 3 as NUM * self.r * self.r } }
impl Shape for Square { fn area(&self) -> NUM { self.s * self.s } }
enum AnyShape { C(Circle), S(Square) }
impl AnyShape { fn area(&self) -> NUM { match self { AnyShape::C(c) => c.area(), AnyShape::S(s) => s.area() } } }

fn total_generic<T: Shape>(xs: &[T]) -> NUM { xs.iter().map(|x| x.area()).sum() }
fn total_dyn(xs: &[&dyn Shape]) -> NUM { xs.iter().map(|x| x.area()).sum() }
fn total_box(xs: &[Box<dyn Shape>]) -> NUM { xs.iter().map(|x| x.area()).sum() }
fn total_enum(xs: &[AnyShape]) -> NUM { xs.iter().map(|x| x.area()).sum() }

fn time<F: FnMut() -> NUM>(mut f: F) -> (NUM, u128) {
    let mut best = u128::MAX; let mut v = 0 as NUM;
    for _ in 0..7 { let t = Instant::now(); v = black_box(f()); best = best.min(t.elapsed().as_micros()); }
    (v, best)
}

fn main() {
    let n = 2_000_000u64;
    let circles: Vec<Circle> = (0..n).map(|i| Circle { r: (i % 7) as NUM }).collect();
    let refs: Vec<&dyn Shape> = circles.iter().map(|c| c as &dyn Shape).collect();
    let boxes: Vec<Box<dyn Shape>> = circles.iter().map(|&c| Box::new(c) as Box<dyn Shape>).collect();
    let enums: Vec<AnyShape> = circles.iter().map(|&c| AnyShape::C(c)).collect();
    let mixed: Vec<Box<dyn Shape>> = (0..n).map(|i| if i % 2 == 0 { Box::new(Circle { r: (i % 7) as NUM }) as Box<dyn Shape> } else { Box::new(Square { s: (i % 7) as NUM }) }).collect();
    let mixed_enum: Vec<AnyShape> = (0..n).map(|i| if i % 2 == 0 { AnyShape::C(Circle { r: (i % 7) as NUM }) } else { AnyShape::S(Square { s: (i % 7) as NUM }) }).collect();
    let (a, ta) = time(|| total_generic(black_box(&circles)));
    let (b, tb) = time(|| total_dyn(black_box(&refs)));
    let (c, tc) = time(|| total_box(black_box(&boxes)));
    let (d, td) = time(|| total_enum(black_box(&enums)));
    let (e, te) = time(|| total_box(black_box(&mixed)));
    let (f, tf) = time(|| total_enum(black_box(&mixed_enum)));
    println!("{a} {ta} {b} {tb} {c} {tc} {d} {td} {e} {te} {f} {tf}");
}
"#;

/// 넓이 타입을 `num`으로 바꾼 BENCH를 최적화해 컴파일하고 실행한 뒤 숫자 12개를 돌려준다.
fn run_bench(num: &str) -> Vec<f64> {
    let dir = snippet_dir();
    let out = rustc(&dir, &BENCH.replace("NUM", num), &["-C", "opt-level=3"]);
    assert!(
        out.status.success(),
        "{}",
        String::from_utf8_lossy(&out.stderr)
    );
    let run = std::process::Command::new(dir.join("snippet"))
        .output()
        .expect("bench 실행");
    String::from_utf8_lossy(&run.stdout)
        .split_whitespace()
        .map(|x| x.parse().unwrap())
        .collect()
}

const TYPES: usize = 40;

// 크기 비교용: 같은 몸통을 제네릭 함수와 dyn 함수로 만들고, 타입 40개에서 부른다.
const REPORT_BODY: &str = r#"{
    let a = x.area();
    let mut parts = Vec::new();
    for k in 1..=4 { parts.push(format!("{}x{} = {:.3}", x.label(), k, a * k as f64)); }
    if a > 100.0 { parts.push(format!("big {}", x.label())); } else { parts.push(format!("small {:e}", a)); }
    parts.join(", ")
}"#;

fn size_snippet(dynamic: bool) -> String {
    let mut s = String::from(
        "pub trait Area { fn area(&self) -> f64; fn label(&self) -> &'static str; }
         macro_rules! shape { ($n:ident, $k:expr) => {
             pub struct $n(pub f64);
             impl Area for $n {
                 fn area(&self) -> f64 { self.0 * $k }
                 fn label(&self) -> &'static str { stringify!($n) }
             }
         } }\n",
    );
    for i in 0..TYPES {
        writeln!(s, "shape!(S{i}, {}.0);", i + 1).unwrap();
    }
    let params = if dynamic {
        "(x: &dyn Area)"
    } else {
        "<T: Area>(x: &T)"
    };
    writeln!(
        s,
        "#[inline(never)] fn report{params} -> String {REPORT_BODY}"
    )
    .unwrap();
    for i in 0..TYPES {
        writeln!(s, "pub fn run_s{i}(x: &S{i}) -> String {{ report(x) }}").unwrap();
    }
    s
}

/// 목적 파일을 세 번 만들어 가장 짧은 컴파일 시간과 파일 크기를 돌려준다.
fn object_size_and_time(code: &str) -> (u64, Duration) {
    let dir = snippet_dir();
    let mut best = Duration::MAX;
    for _ in 0..3 {
        let t = Instant::now();
        let out = rustc(
            &dir,
            code,
            &["--crate-type=lib", "-C", "opt-level=2", "--emit=obj"],
        );
        best = best.min(t.elapsed());
        assert!(
            out.status.success(),
            "{}",
            String::from_utf8_lossy(&out.stderr)
        );
    }
    let size = std::fs::metadata(dir.join("snippet.o")).unwrap().len();
    (size, best)
}

// 인라인된 합 루프: 정수와 실수 두 가지.
const SUMS: &str = "
    pub struct CircleU { pub r: u64 }
    pub struct CircleF { pub r: f64 }
    #[unsafe(no_mangle)] pub fn sum_u(xs: &[CircleU]) -> u64 { xs.iter().map(|c| 3 * c.r * c.r).sum() }
    #[unsafe(no_mangle)] pub fn sum_f(xs: &[CircleF]) -> f64 { xs.iter().map(|c| 3.0 * c.r * c.r).sum() }";

/// 어셈블리에서 함수 `func`의 몸통을 잘라, `acc = acc + x` 꼴(결과 레지스터가 입력에도 있음)의
/// `op` 명령이 쓰는 결과 레지스터를 모은다. 합을 몇 갈래로 나눠 쌓는지 알 수 있다.
fn accumulators(asm: &str, func: &str, op: &str) -> BTreeSet<String> {
    let start = format!("_{func}:");
    let mut set = BTreeSet::new();
    let body = asm
        .lines()
        .skip_while(|l| !l.starts_with(&start))
        .take_while(|l| !l.contains(".cfi_endproc"));
    for line in body {
        let Some(rest) = line.trim().strip_prefix(op) else {
            continue;
        };
        let ops: Vec<&str> = rest.split(", ").collect();
        if ops.len() == 3 && (ops[0] == ops[1] || ops[0] == ops[2]) {
            set.insert(ops[0].to_string());
        }
    }
    set
}

fn main() {
    // 1. 제네릭 Vec<T>에는 한 가지 타입만
    let dir = snippet_dir();
    let out = rustc(
        &dir,
        "pub trait Shape {} pub struct Circle; pub struct Square;
         impl Shape for Circle {} impl Shape for Square {}
         pub fn all<T: Shape>(_xs: Vec<T>) {}
         pub fn f() { all(vec![Circle, Square]); }",
        &["--crate-type=lib", "--emit=metadata"],
    );
    let err = String::from_utf8_lossy(&out.stderr);
    println!(
        "[제네릭 Vec에 두 타입] {}",
        err.lines().find(|l| l.starts_with("error")).unwrap()
    );
    assert!(err.contains("error[E0308]"));

    // 2. 함수 사본 개수
    let generic = "
        pub trait Area { fn area(&self) -> f64; }
        macro_rules! shape { ($n:ident, $k:expr) => {
            pub struct $n(pub f64);
            impl Area for $n { fn area(&self) -> f64 { self.0 * $k } }
        } }
        shape!(A, 1.0); shape!(B, 2.0); shape!(C, 3.0); shape!(D, 4.0); shape!(E, 5.0); shape!(F, 6.0);
        #[inline(never)] fn total<T: Area>(xs: &[T]) -> f64 { xs.iter().map(|x| x.area()).sum() }
        pub fn run(a: &[A], b: &[B], c: &[C], d: &[D], e: &[E], f: &[F]) -> f64 {
            total(a) + total(b) + total(c) + total(d) + total(e) + total(f)
        }";
    let dynamic = "
        pub trait Area { fn area(&self) -> f64; }
        macro_rules! shape { ($n:ident, $k:expr) => {
            pub struct $n(pub f64);
            impl Area for $n { fn area(&self) -> f64 { self.0 * $k } }
        } }
        shape!(A, 1.0); shape!(B, 2.0); shape!(C, 3.0); shape!(D, 4.0); shape!(E, 5.0); shape!(F, 6.0);
        #[inline(never)] fn total(xs: &[&dyn Area]) -> f64 { xs.iter().map(|x| x.area()).sum() }
        pub fn run(a: &[&dyn Area], b: &[&dyn Area], c: &[&dyn Area], d: &[&dyn Area], e: &[&dyn Area], f: &[&dyn Area]) -> f64 {
            total(a) + total(b) + total(c) + total(d) + total(e) + total(f)
        }";
    let g = count_definitions(generic, "5total");
    let d = count_definitions(dynamic, "5total");
    println!("total 정의 개수: 제네릭(타입 6개) {g}, dyn {d}");
    assert_eq!((g, d), (6, 1));

    // 3. 시간 재기: 최적화한 별도 프로그램을 만들어 돌린다.
    let labels = [
        "제네릭 Vec<Circle>",
        "&dyn Shape (값은 한 Vec에 붙어 있음)",
        "Box<dyn Shape> (값마다 따로 힙)",
        "enum Vec<AnyShape>",
        "Box<dyn Shape> 원·정사각형 섞음",
        "enum 원·정사각형 섞음",
    ];
    for num in ["f64", "u64"] {
        let v = run_bench(num);
        println!(
            "원소 2,000,000개 넓이({num}) 합, 7번 잰 것 중 가장 짧은 시간 (이 기계, opt-level=3):"
        );
        for (i, l) in labels.iter().enumerate() {
            println!("  {l:<40} {:>8} µs", v[2 * i + 1]);
        }
        // 같은 원만 담은 네 방식의 결과는 같아야 한다.
        assert_eq!(v[0], v[2]);
        assert_eq!(v[0], v[4]);
        assert_eq!(v[0], v[6]);
        // 섞은 두 방식의 결과도 같아야 한다.
        assert_eq!(v[8], v[10]);
    }

    // 4. 타입 40개로 부를 때 코드 크기와 컴파일 시간
    let generic40 = size_snippet(false);
    let dynamic40 = size_snippet(true);
    let gr = count_definitions(&generic40, "6report");
    let dr = count_definitions(&dynamic40, "6report");
    let (gs, gt) = object_size_and_time(&generic40);
    let (ds, dt) = object_size_and_time(&dynamic40);
    println!(
        "타입 {TYPES}개에서 부른 report (opt-level=2, 이 기계, 컴파일은 3번 중 가장 짧은 시간):"
    );
    println!(
        "  제네릭: 정의 {gr}개, 목적 파일 {gs}바이트, 컴파일 {} ms",
        gt.as_millis()
    );
    println!(
        "  dyn   : 정의 {dr}개, 목적 파일 {ds}바이트, 컴파일 {} ms",
        dt.as_millis()
    );
    assert_eq!((gr, dr), (TYPES, 1));
    assert!(gs > ds, "제네릭 쪽 목적 파일이 더 커야 한다: {gs} vs {ds}");

    // 5. 합을 몇 갈래로 쌓는가
    if cfg!(all(target_arch = "aarch64", target_os = "macos")) {
        let dir = snippet_dir();
        let out = rustc(
            &dir,
            SUMS,
            &["--crate-type=lib", "-C", "opt-level=3", "--emit=asm"],
        );
        assert!(
            out.status.success(),
            "{}",
            String::from_utf8_lossy(&out.stderr)
        );
        let asm = std::fs::read_to_string(dir.join("snippet.s")).unwrap();
        let int_acc = accumulators(&asm, "sum_u", "add\t");
        let float_acc = accumulators(&asm, "sum_f", "fadd\t");
        println!(
            "합을 쌓는 레지스터: u64 {}개 {int_acc:?}, f64 {}개 {float_acc:?}",
            int_acc.len(),
            float_acc.len()
        );
        assert!(int_acc.len() >= 2);
        assert_eq!(float_acc.len(), 1);
    } else {
        println!("합 갈래 확인은 aarch64 macOS에서만 한다");
    }
    // 실수 덧셈은 묶는 순서를 바꾸면 결과가 달라질 수 있다. 그래서 컴파일러가 순서를 바꾸지 않는다.
    let left = (0.1f64 + 0.2) + 0.3;
    let right = 0.1f64 + (0.2 + 0.3);
    println!("(0.1 + 0.2) + 0.3 = {left}, 0.1 + (0.2 + 0.3) = {right}");
    assert_ne!(left, right);

    println!("ALL CHECKS PASSED");
}