성능 측정과 최적화 문서의 실험 코드다.
//! 측정 먼저: 할당 횟수(결정적)와 걸린 시간(이 기계 기준)을 잰다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/47_performance-measurement_bench.rs"
//!
//! rs_check는 디버그 빌드로 실행한다. 최적화한 빌드의 숫자가 필요하면 아래 `release_vs_debug`처럼
//! 같은 코드를 rustc로 `-C opt-level=3`(= -O 이상)과 `-C opt-level=0`으로 따로 컴파일해 돌린다.
//! 시간 숫자는 이 기계(aarch64-apple-darwin)에서 잰 한 번의 값이다. 다른 기계에서는 다르다.
use std::alloc::{GlobalAlloc, Layout, System};
use std::fmt::Write as _;
use std::hint::black_box;
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
/// 할당 횟수를 세는 전역 할당자. 시간과 달리 몇 번을 돌려도 같은 값이 나온다.
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
// SAFETY: 실제 일은 System에 그대로 맡기고 횟수만 센다.
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
// SAFETY: 호출자가 지킨 Layout 계약을 그대로 넘긴다.
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// SAFETY: ptr은 위 alloc(=System.alloc)이 준 것이다.
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
// SAFETY: ptr과 layout은 이전 alloc의 것이다.
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
fn count_allocs<R>(f: impl FnOnce() -> R) -> (R, usize) {
let before = ALLOCS.load(Ordering::Relaxed);
let r = f();
(r, ALLOCS.load(Ordering::Relaxed) - before)
}
fn push_n(n: u64) -> Vec<u64> {
let mut v = Vec::new();
for i in 0..n {
v.push(i);
}
v
}
fn push_n_reserved(n: u64) -> Vec<u64> {
let mut v = Vec::with_capacity(n as usize);
for i in 0..n {
v.push(i);
}
v
}
/// 줄마다 format!으로 새 String을 만든다.
fn render_alloc_each(items: &[(u32, &str)]) -> usize {
let mut total = 0;
for (id, name) in items {
let line = format!("{id}:{name}\n");
total += line.len();
}
total
}
/// 버퍼 하나를 바깥에 두고 clear로 다시 쓴다.
fn render_reuse(items: &[(u32, &str)]) -> usize {
let mut total = 0;
let mut line = String::with_capacity(64);
for (id, name) in items {
line.clear();
writeln!(line, "{id}:{name}").unwrap();
total += line.len();
}
total
}
fn capacities(n: usize) -> Vec<usize> {
let mut v: Vec<u64> = Vec::new();
let mut caps = vec![v.capacity()];
for i in 0..n {
v.push(i as u64);
if *caps.last().unwrap() != v.capacity() {
caps.push(v.capacity());
}
}
caps
}
/// 가장 짧은 시간을 고른다. 다른 일이 끼어든 회차는 길어지기만 하므로 최솟값이 잡음이 적다.
fn best_of(runs: usize, mut f: impl FnMut()) -> Duration {
(0..runs)
.map(|_| {
let t = Instant::now();
f();
t.elapsed()
})
.min()
.unwrap()
}
const SUM_PROGRAM: &str = r#"
use std::hint::black_box;
fn main() {
let n: u64 = black_box(30_000_000);
let mut acc: u64 = 0;
for i in 0..n {
acc = acc.wrapping_add(black_box(i) ^ (i >> 3));
}
println!("{acc}");
}
"#;
fn build_and_time(opt: &str, dir: &std::path::Path) -> (String, Duration) {
let src = dir.join("sum.rs");
std::fs::write(&src, SUM_PROGRAM).unwrap();
let exe = dir.join(format!("sum_o{opt}"));
let st = Command::new("rustc")
.args(["--edition", "2024", "-C"])
.arg(format!("opt-level={opt}"))
.arg("-o")
.arg(&exe)
.arg(&src)
.status()
.expect("rustc 실행");
assert!(st.success());
let mut out = String::new();
let d = best_of(3, || {
let o = Command::new(&exe).output().unwrap();
out = String::from_utf8(o.stdout).unwrap();
});
(out, d)
}
fn release_vs_debug() -> (Duration, Duration) {
let dir = std::env::temp_dir().join(format!("vr47_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (out0, d0) = build_and_time("0", &dir);
let (out3, d3) = build_and_time("3", &dir);
assert_eq!(out0, out3, "최적화해도 결과는 같다");
let _ = std::fs::remove_dir_all(&dir);
(d0, d3)
}
fn main() {
// 1) Vec이 자라는 모양
let caps = capacities(100);
println!("Vec<u64> 용량 변화: {caps:?}");
assert_eq!(caps, vec![0, 4, 8, 16, 32, 64, 128]);
// 2) 할당 횟수: with_capacity
let (v1, a1) = count_allocs(|| push_n(1000));
let (v2, a2) = count_allocs(|| push_n_reserved(1000));
assert_eq!(v1, v2);
println!("push 1000번: Vec::new {a1}번 할당, with_capacity {a2}번 할당");
assert_eq!(a2, 1);
assert!(a1 >= 9, "4→8→…→1024로 커지며 여러 번 다시 잡는다");
// 3) 할당 횟수: 버퍼 다시 쓰기
let items: Vec<(u32, &str)> = (0..1000).map(|i| (i, "item")).collect();
let (t1, b1) = count_allocs(|| render_alloc_each(black_box(&items)));
let (t2, b2) = count_allocs(|| render_reuse(black_box(&items)));
assert_eq!(t1, t2);
println!("1000줄 만들기: format! {b1}번 할당, 버퍼 재사용 {b2}번 할당");
assert!(b1 >= 1000, "줄마다 적어도 한 번, 길어지면 realloc까지");
assert_eq!(b2, 1);
// 4) 시간: 디버그 빌드에서 잰 값(참고용)
let d_new = best_of(20, || {
black_box(push_n(black_box(100_000)));
});
let d_cap = best_of(20, || {
black_box(push_n_reserved(black_box(100_000)));
});
println!("디버그 빌드, push 100,000번 최솟값: Vec::new {d_new:?}, with_capacity {d_cap:?}");
// 5) 같은 프로그램을 opt-level 0과 3으로
let (d0, d3) = release_vs_debug();
let ratio = d0.as_secs_f64() / d3.as_secs_f64();
println!("합 계산 3천만 번: opt-level=0 {d0:?}, opt-level=3 {d3:?}, {ratio:.1}배");
assert!(ratio > 1.5, "최적화 빌드가 확실히 빠르다");
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_output_either_way() {
let items = [(1, "a"), (22, "bb")];
assert_eq!(render_alloc_each(&items), render_reuse(&items));
assert_eq!(render_reuse(&items), "1:a\n".len() + "22:bb\n".len());
}
}