본문 바로가기

Rust

[Rust] 실제 코드로 확인해보는 Monomorphization

 

안녕하세요.

오늘은 Rust의 Monomorphization에 대해 알아보겠습니다.


서론

Rust의 Generic은 어떻게 런타임 비용 없이 동작하는가?

 

Generic의 설명을 들었을 때 이런 의문이 들었습니다.

  • Java 처럼 Type Erasure를 사용하는 건지?
  • C++ template 처럼 코드가 복제되는 건지?

이 질문을 따라가다 보면 만나게 되는 개념이 Monomorphization 입니다.

 

이번 글에서는 다음 내용을 다룹니다.

  1. Monomorphization이 무엇인지
  2. 해당 동작이 Rust 컴파일러 파이프라인에서 어디서 수행되는지
  3. MIR / LLVM IR을 직접 열어 해당 동작을 확인하는 방법
  4. dyn trait과 비교하여 장단점

Monomorphization 이란?

Generic 코드를 실제 사용된 타입 별로 컴파일 타임에 구체화 하는 과정입니다.

예제 코드

fn identity<T>(x: T) -> T {
    x
}

fn main() {
    let a = identity(10i32);
    let b = identity(20u64);
}

 

이 코드는 하나의 identity<T> 함수가 있는 것처럼 보입니다.

하지만 실제로 컴파일러는 다음과 같이 함수를 생성합니다.

identity_i32(x: i32) -> i32
identity_u64(x: u64) -> u64

 

즉, Rust의 Generic은 런타임에 타입을 검사하지 않습니다.

컴파일 타임에 타입 별로 함수가 생성됩니다.

 

이 방식은 다음과 같이 장단점이 있습니다.

  • 장점: 런타임 오버헤드 없음
  • 단점: 코드 크기 증가 (Code Bloat)

Rust 컴파일러 파이프라인과 Monomorphization 위치

rustc 가이드 문서를 보면 Rust 컴파일 흐름을 다음과 같이 요약할 수 있습니다.

https://rustc-dev-guide.rust-lang.org/overview.html

https://rustc-dev-guide.rust-lang.org/backend/monomorph.html

 

Overview of the compiler - Rust Compiler Development Guide

This chapter is about the overall process of compiling a program – how everything fits together. The Rust compiler is special in two ways: it does things to your code that other compilers don’t do (e.g. borrow-checking) and it has a lot of unconvention

rustc-dev-guide.rust-lang.org

 

 

Monomorphization - Rust Compiler Development Guide

As you probably know, Rust has a very expressive type system that has extensive support for generic types. But of course, assembly is not generic, so we need to figure out the concrete types of all the generics before the code can execute. Different langua

rustc-dev-guide.rust-lang.org


Source Code
   ↓
AST
   ↓
HIR
   ↓
MIR
   ↓
Monomorphization 📌 여기서 수행
   ↓
LLVM IR
   ↓
Machine Code
  • AST → HIR → MIR 생성
  • MIR 단계에서 monomorphization collector가 실제 사용된 generic instance를 수집 및 concrete instace 생성
  • LLVM IR 단계에서 타입별 함수가 등장

즉, Monomorphization은 MIR 단계에서 수행해 LLVM IR 단계에서 확인할 수 있습니다.


직접 확인해보기

실제로 타입 별로 함수가 생성되는지 확인해보겠습니다.

 

실습 코드

fn identity<T>(x: T) -> T {
    x
}

fn main() {
    let a = identity(10i32);
    let b = identity(20u64);

    println!("{}, {}", a, b);
}

 

MIR 확인하기 (main.mir)

터미널에서 아래와 같이 명령어를 입력해 줍니다.

rustc src/main.rs --emit=mir

 

main.mir 파일이 만들어진 걸 확인할 수 있습니다.

ls

Mode                 LastWriteTime         Length Name                            
----                 -------------         ------ ----                            
d-----      2026-02-18   오후 2:48                .idea                           
d-----      2026-02-18   오후 2:48                src                             
d-----      2026-02-13  오후 11:07                target                          
-a----      2026-02-13  오후 11:06              8 .gitignore                      
-a----      2026-02-13  오후 11:07            152 Cargo.lock                      
-a----      2026-02-13  오후 11:06             79 Cargo.toml                      
-a----      2026-02-18   오후 2:48           2075 main.mir 📌

 

main.mir 파일을 열어보면:

fn identity(_1: T) -> T {
    debug x => _1;
    let mut _0: T;

    bb0: {
        _0 = move _1;
        return;
    }
}
  • 아직 MIR 단계에선 identity<i32>, identity<u64> 가 없습니다.

MIR은 아직 Generic 형태를 유지합니다.

Monomorphization은 아직 수행되지 않았습니다.

 

LLVM IR 확인하기 (main.ll)

터미널에서 아래 명령어를 입력해 LLVM IR을 생성합니다.

rustc src/main.rs --emit=llvm-ir -C opt-level=0                                   

 

그리고 main.ll 파일을 열어보면:

; main::identity
; Function Attrs: uwtable
define internal i32 @_ZN4main8identity17h5bc45dba107331f7E(i32 %x) unnamed_addr #0 {
start:
  ret i32 %x
}

; main::identity
; Function Attrs: uwtable
define internal i64 @_ZN4main8identity17h7e7d0304df833ddbE(i64 %x) unnamed_addr #0 {
start:
  ret i64 %x
}
  • Monomorphization이 수행됐습니다
  • i32 전용 함수 하나
  • i64 전용 함수 하나 (u64가 64비트 정수이므로 i64 형태로 표현됨)

 

심볼 테이블로 검증

더 확실히 보고 싶다면:

rustc src/main.rs -C opt-level=0
nm main | grep identity | rustfilt

 

결과:

0000000000014370 t main::identity
0000000000014380 t main::identit

Generic 함수는 실제로 타입별 코드로 분리되어 존재합니다.


Compare with Dynamic Dispatch

Rust에는 두 가지 다형성이 있습니다.

 

1) Static Dispatch (Monomorphization 기반)

fn foo<T: Trait>(x: T)
  • 컴파일 타임 결정
  • 타입별 코드 생성

 

2) Dynamic Dispatch (vtable 기반)

fn foo(x: &dyn Trait)
  • 런타임 결정
  • vtable 사용
  • 함수 하나만 존재

항목 Static Dynamic

타입 결정 시점 컴파일 타임 런타임
코드 생성 타입별 복제 하나만 존재
호출 방식 direct call indirect call
인라인 가능 거의 불가능
런타임 비용 없음 존재

정리

Rust의 Generic은 Java의 Type Erasure 방식이 아닙니다.

또한 런타임에 타입을 검사하지도 않습니다.

 

Rust는 다음과 같은 전략을 선택했습니다.

  • 컴파일 타임에 타입별 코드를 생성하여 런타임 비용을 제거

 

그 결과:

  • 성능은 C++ template과 동일한 수준
  • 런타임 오버헤드 zero
  • 대신 코드 크기 증가 가능

그리고 이러한 과정은 LLVM IR에서 타입별 함수로 확인할 수 있었습니다.

 

감사합니다.

'Rust' 카테고리의 다른 글

[Rust] 패턴 바인딩 (Pattern Binding)  (0) 2026.02.17
[Rust] Generic Lifetime  (0) 2026.02.16
[Rust] unwrap 이해하기  (0) 2026.02.14
[Rust] 동시성  (0) 2025.04.27
[Rust] 컬렉션 (Collection)  (0) 2025.04.22