똑바른 날개

Rust에서의 Zero-cost Abstraction 본문

프로그래밍/Rust

Rust에서의 Zero-cost Abstraction

Upright_wing 2025. 7. 17. 11:08
반응형

개요

Zero-cost abstraction은 Rust의 철학 중 하나로, “고수준의 추상화를 제공하지만, 런타임 성능 손실은 없다”는 개념이다. 즉, 컴파일 타임에 모든 추상화를 제거하여, 추상화를 사용하지 않은 코드와 동일한 성능을 갖도록 한다.

C++에서도 등장한 개념이지만, Rust는 이를 더 발전시켰다.

 

왜 중요한가?

일반적으로 프로그래밍 언어에서 추상화(예: trait, 제네릭, iterator 등)를 사용하면 가독성과 재사용성이 좋아지지만, 성능 저하라는 대가가 따른다.

하지만 Rust에서는 Zero-cost abstraction을 통해 다음을 동시에 얻을 수 있다:

  • C만큼 빠른 실행 성능
  • 추상화에 기반한 깔끔한 코드 구조
  • 메모리 안전성 보장 (no GC)

예시 1: Iterator vs for-loop

일반적인 for-loop

let nums = vec![1, 2, 3, 4, 5];
let mut sum = 0;
for i in 0..nums.len() {
    sum += nums[i];
}

Iterator 기반 추상화

let nums = vec![1, 2, 3, 4, 5];
let sum: i32 = nums.iter().sum();

Rust에서는 iter() 와 sum() 같은 메서드는 trait 기반 고수준 추상화이다. 하지만 컴파일러는 이 코드를 분석하여 for-loop와 동일한 기계어 코드로 최적화한다. 추상화의 비용이 없기 때문에 이것이 바로 "zero-cost abstraction"이다.

예시 2: generic 함수

fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b
}

이런 함수는 런타임에 T가 결정되지 않는다. 대신 모노모피제이션(monomorphization) 과정을 통해 컴파일 타임에 각 타입별 함수가 생성된다:

// 내부적으로 컴파일러가 아래처럼 만든다 (예시)
fn add_i32(a: i32, b: i32) -> i32 { a + b }
fn add_f64(a: f64, b: f64) -> f64 { a + b }

결과적으로 generic 함수는 런타임 오버헤드 없이 컴파일 타임에 구체화된다.


예시 3: Trait 객체 vs 제네릭

Trait을 사용할 때 두 방식이 있다.

Trait 객체 (dynamic dispatch)

trait Animal {
    fn speak(&self);
}

fn make_speak(a: &dyn Animal) {
    a.speak(); // vtable을 통한 동적 호출
}

이 경우에는 런타임에 vtable을 통해 호출되므로 zero-cost는 아니다.

제네릭 (static dispatch)

fn make_speak<T: Animal>(a: &T) {
    a.speak(); // 컴파일 타임에 결정
}

이 경우는 정적으로 호출될 함수가 결정되므로 zero-cost abstraction이다.

 

핵심 요약

기능 추상화 방식 비용

Iterators Zero-cost
Generics Zero-cost
Trait Object - dynamic dispatch 비용 있음

 

마무리

Rust의 zero-cost abstraction은 성능을 포기하지 않고도 추상화된 코드를 작성할 수 있게 해준다. 이는 성능이 중요한 시스템 프로그래밍에서도 안전성과 유지보수성을 모두 만족시키는 데 핵심 역할을 한다.

Rust로 코딩할 때 우리는 추상화와 성능 사이에서 양자택일이 아닌, 둘 다를 취할 수 있다.

반응형