5장: 성능 최적화 & 메모리 관리

5.1 벤치마킹

성능을 측정하지 않으면 최적화할 수 없습니다. Criterion은 Rust의 표준 벤치마킹 도구입니다.

5.1.1 Criterion 사용하기

// Cargo.toml
[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "my_benchmark"
harness = false

// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        n => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn fibonacci_iterative(n: u64) -> u64 {
    let mut a = 0;
    let mut b = 1;
    for _ in 0..n {
        let temp = a;
        a = b;
        b = temp + b;
    }
    a
}

fn criterion_benchmark(c: &mut Criterion) {
    c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
    c.bench_function("fib_iter 20", |b| {
        b.iter(|| fibonacci_iterative(black_box(20)))
    });
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

5.1.2 실행하기

$ cargo bench

# 특정 벤치마크만 실행
$ cargo bench --bench my_benchmark

# HTML 리포트 생성
$ cargo bench
$ open target/criterion/report/index.html

5.2 프로파일링

코드의 어느 부분이 느린지 정확히 파악합니다.

5.2.1 Flamegraph

$ cargo install flamegraph

# 프로파일링 실행
$ cargo flamegraph

# 특정 벤치마크 프로파일링
$ cargo flamegraph --bench my_benchmark

5.2.2 perf 사용하기 (Linux)

$ cargo build --release
$ perf record --call-graph=dwarf ./target/release/myapp
$ perf report

5.3 메모리 레이아웃 최적화

5.3.1 구조체 패딩

use std::mem;

// 비효율적인 레이아웃
#[derive(Debug)]
struct Bad {
    a: u8,   // 1 byte + 7 padding
    b: u64,  // 8 bytes
    c: u8,   // 1 byte + 7 padding
    d: u64,  // 8 bytes
}

// 효율적인 레이아웃
#[derive(Debug)]
struct Good {
    b: u64,  // 8 bytes
    d: u64,  // 8 bytes
    a: u8,   // 1 byte
    c: u8,   // 1 byte + 6 padding
}

fn main() {
    println!("Bad size: {}", mem::size_of::<Bad>());   // 32 bytes
    println!("Good size: {}", mem::size_of::<Good>()); // 24 bytes

    // 25% 메모리 절약!
}

// repr(C)로 C 호환 레이아웃
#[repr(C)]
struct FFI {
    x: u32,
    y: u32,
}

// repr(packed)로 패딩 제거 (주의해서 사용)
#[repr(packed)]
struct Packed {
    a: u8,
    b: u64,
}

5.3.2 Cow (Clone on Write)

use std::borrow::Cow;

fn process_text(text: Cow<str>) -> Cow<str> {
    if text.contains("Rust") {
        // 이미 있으면 복사 안 함
        text
    } else {
        // 필요할 때만 복사
        Cow::Owned(format!("{} and Rust", text))
    }
}

fn main() {
    let owned = String::from("Hello Rust");
    let borrowed = "Hello World";

    // 빌림만 발생
    let result1 = process_text(Cow::Borrowed(&owned));
    println!("{}", result1);

    // 복사 발생
    let result2 = process_text(Cow::Borrowed(borrowed));
    println!("{}", result2);
}

5.4 이터레이터 최적화

5.4.1 제로 코스트 추상화

// 느림: 여러 번 할당
fn slow_sum(numbers: &[i32]) -> i32 {
    let mut doubled = vec![];
    for &n in numbers {
        doubled.push(n * 2);
    }

    let mut filtered = vec![];
    for &n in &doubled {
        if n > 10 {
            filtered.push(n);
        }
    }

    filtered.iter().sum()
}

// 빠름: 할당 없음, 한 번의 패스
fn fast_sum(numbers: &[i32]) -> i32 {
    numbers
        .iter()
        .map(|&n| n * 2)
        .filter(|&n| n > 10)
        .sum()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_equivalence() {
        let numbers = vec![1, 5, 10, 15, 20];
        assert_eq!(slow_sum(&numbers), fast_sum(&numbers));
    }
}

5.4.2 collect 최적화

// 크기를 미리 알 때
fn optimized_collect(size: usize) -> Vec<i32> {
    let mut vec = Vec::with_capacity(size);
    for i in 0..size {
        vec.push(i as i32);
    }
    vec
}

// 이터레이터로 더 간결하게
fn iterator_collect(size: usize) -> Vec<i32> {
    (0..size).map(|i| i as i32).collect()
}

5.5 SIMD (Single Instruction Multiple Data)

하나의 명령어로 여러 데이터를 동시에 처리합니다.

// portable_simd (nightly 필요)
#![feature(portable_simd)]
use std::simd::f32x4;

fn dot_product_scalar(a: &[f32], b: &[f32]) -> f32 {
    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}

fn dot_product_simd(a: &[f32], b: &[f32]) -> f32 {
    assert_eq!(a.len(), b.len());
    assert_eq!(a.len() % 4, 0);

    let mut sum = f32x4::splat(0.0);

    for i in (0..a.len()).step_by(4) {
        let a_vec = f32x4::from_slice(&a[i..i + 4]);
        let b_vec = f32x4::from_slice(&b[i..i + 4]);
        sum += a_vec * b_vec;
    }

    sum.reduce_sum()
}

// 실제 사용 예
fn main() {
    let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
    let b = vec![8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0];

    println!("Scalar: {}", dot_product_scalar(&a, &b));
    println!("SIMD: {}", dot_product_simd(&a, &b));
}

5.6 인라인 최적화

// 작은 함수는 자동으로 인라인되지만, 명시적으로 지정 가능

#[inline]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 항상 인라인 (신중하게 사용)
#[inline(always)]
fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

// 인라인 금지
#[inline(never)]
fn complex_function() {
    // 큰 함수는 인라인하지 않는 게 좋을 수 있음
}

// 크로스 크레이트 인라인을 위해 제네릭 사용
#[inline]
pub fn generic_add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b
}

5.7 캐시 지역성

// 나쁜 예: 캐시 미스 많음
fn bad_matrix_sum(matrix: &[[i32; 1000]; 1000]) -> i32 {
    let mut sum = 0;
    for col in 0..1000 {
        for row in 0..1000 {
            sum += matrix[row][col]; // 열 우선 접근 (나쁨)
        }
    }
    sum
}

// 좋은 예: 캐시 히트 높음
fn good_matrix_sum(matrix: &[[i32; 1000]; 1000]) -> i32 {
    let mut sum = 0;
    for row in 0..1000 {
        for col in 0..1000 {
            sum += matrix[row][col]; // 행 우선 접근 (좋음)
        }
    }
    sum
}

// 더 좋은 예: 이터레이터 사용
fn best_matrix_sum(matrix: &[[i32; 1000]; 1000]) -> i32 {
    matrix.iter().flat_map(|row| row.iter()).sum()
}

5.8 실전 예제: 고성능 해시맵

use std::collections::HashMap;
use std::hash::{Hash, BuildHasher};

// FxHash는 std HashMap보다 빠름 (비암호화)
use rustc_hash::FxHashMap;

fn benchmark_hashmaps() {
    use std::time::Instant;

    // 표준 HashMap
    let start = Instant::now();
    let mut std_map: HashMap<i32, i32> = HashMap::new();
    for i in 0..1_000_000 {
        std_map.insert(i, i * 2);
    }
    println!("std HashMap: {:?}", start.elapsed());

    // FxHashMap (더 빠름)
    let start = Instant::now();
    let mut fx_map: FxHashMap<i32, i32> = FxHashMap::default();
    for i in 0..1_000_000 {
        fx_map.insert(i, i * 2);
    }
    println!("FxHashMap: {:?}", start.elapsed());

    // 용량 미리 할당 (더더 빠름)
    let start = Instant::now();
    let mut reserved_map: FxHashMap<i32, i32> =
        FxHashMap::with_capacity_and_hasher(1_000_000, Default::default());
    for i in 0..1_000_000 {
        reserved_map.insert(i, i * 2);
    }
    println!("Reserved FxHashMap: {:?}", start.elapsed());
}

fn main() {
    benchmark_hashmaps();
}
🎯 핵심 요약: