- Authors

- Name
- Youngju Kim
- @fjvbn20031
개요
컴파일러 기술은 프로그래밍 언어의 발전, 하드웨어의 변화, 새로운 응용 분야의 등장에 따라 끊임없이 진화하고 있습니다. 이번 글에서는 현대 컴파일러의 아키텍처, 주요 컴파일러 도구 체인, 그리고 보안, AI/ML, 웹 등 다양한 분야에서의 컴파일러 기술 응용을 살펴봅니다.
1. LLVM 아키텍처
1.1 LLVM이란
LLVM은 컴파일러 인프라 프로젝트로, 모듈화된 컴파일러 도구 체인을 제공합니다. 원래 "Low Level Virtual Machine"의 약자였지만, 현재는 프로젝트 전체를 지칭하는 이름으로 사용됩니다.
1.2 3단계 아키텍처
LLVM의 핵심 설계 철학은 프론트엔드-미들엔드-백엔드의 3단계 분리입니다.
소스 코드 프론트엔드 LLVM IR 미들엔드 LLVM IR 백엔드 기계어
(최적화)
C/C++ --> Clang --> --> --> --> x86
Rust --> rustc --> 공통 IR --> 공통 최적화 --> 공통 IR --> ARM
Swift --> swiftc --> --> --> --> RISC-V
Fortran --> flang --> --> --> --> WebAssembly
이 구조의 장점:
- 새 언어를 지원하려면 프론트엔드만 구현
- 새 아키텍처를 지원하려면 백엔드만 구현
- 최적화는 모든 언어와 아키텍처가 공유
1.3 LLVM IR
LLVM IR은 SSA 기반의 저수준 중간 표현입니다.
; LLVM IR 예시: 두 수의 합
define i32 @add(i32 %a, i32 %b) {
entry:
%result = add i32 %a, %b
ret i32 %result
}
; 조건문 예시
define i32 @max(i32 %a, i32 %b) {
entry:
%cmp = icmp sgt i32 %a, %b
br i1 %cmp, label %then, label %else
then:
br label %merge
else:
br label %merge
merge:
%result = phi i32 [%a, %then], [%b, %else]
ret i32 %result
}
LLVM IR의 세 가지 표현 형태:
- 텍스트 형태 (
.ll파일): 사람이 읽을 수 있는 형태 - 바이트코드 (
.bc파일): 효율적인 직렬화 형태 - 인메모리 표현: 컴파일러 내부에서 사용하는 C++ 객체
1.4 LLVM 최적화 패스
LLVM의 최적화는 패스(pass) 단위로 구성됩니다.
주요 최적화 패스:
- mem2reg: 메모리 접근을 레지스터로 승격 (SSA 구성)
- instcombine: 명령어 조합 최적화
- gvn: 전역 값 번호화
- licm: 루프 불변 코드 이동
- indvars: 유도 변수 단순화
- loop-unroll: 루프 펼침
- inline: 함수 인라이닝
- sccp: 희소 조건부 상수 전파
- dce: 죽은 코드 제거
- simplifycfg: 제어 흐름 간소화
최적화 수준에 따른 패스 구성:
-O0: 최적화 없음 (디버깅용)
-O1: 기본 최적화 (빠른 컴파일)
-O2: 표준 최적화 (대부분의 경우 권장)
-O3: 공격적 최적화 (코드 크기 증가 허용)
-Os: 코드 크기 최적화
-Oz: 극한 크기 최적화
2. GCC vs LLVM vs Clang
2.1 GCC (GNU Compiler Collection)
역사: 1987년 Richard Stallman이 시작
언어: C, C++, Fortran, Go, Ada 등
특징:
- 40년 가까운 역사, 방대한 아키텍처 지원
- GIMPLE(중간 표현) -> RTL(저수준 표현) 2단계 구조
- 강력한 최적화 (특히 Fortran)
- GPL 라이선스
2.2 LLVM/Clang
역사: 2003년 Chris Lattner가 시작 (UIUC)
Clang: LLVM의 C/C++/Objective-C 프론트엔드
특징:
- 모듈화된 라이브러리 설계
- 더 나은 에러 메시지
- 빠른 컴파일 속도
- Apache 2.0 라이선스 (상업 사용에 유리)
- IDE 통합이 용이 (libclang, clangd)
2.3 비교
| 측면 | GCC | LLVM/Clang |
|---|---|---|
| 에러 메시지 | 기본적 | 상세하고 친절 |
| 컴파일 속도 | 보통 | 빠름 |
| 코드 품질 | 우수 | 우수 |
| 아키텍처 지원 | 매우 넓음 | 넓음 (확장 중) |
| 확장성 | 어려움 | 쉬움 (라이브러리) |
| 정적 분석 | 기본적 | 강력 (Clang Static Analyzer) |
| 라이선스 | GPL | Apache 2.0 |
실무에서의 선택:
- 임베디드/레거시 시스템: GCC (넓은 아키텍처 지원)
- iOS/macOS 개발: Clang (Apple의 공식 컴파일러)
- 정적 분석/도구 개발: LLVM (모듈화된 라이브러리)
- 고성능 컴퓨팅: 둘 다 사용 (벤치마크로 결정)
3. JIT 컴파일
3.1 AOT vs JIT
AOT (Ahead-Of-Time) 컴파일:
소스 코드 -> [컴파일] -> 기계어 -> [실행]
예: C/C++ (gcc, clang), Rust, Go
JIT (Just-In-Time) 컴파일:
소스 코드 -> [인터프리터로 실행 시작] -> [핫스팟 감지] -> [실행 중 컴파일] -> [최적화된 코드로 전환]
예: Java (HotSpot), JavaScript (V8), .NET (RyuJIT)
3.2 JIT의 장점
// 1. 프로파일 기반 최적화 (PGO)
// 실행 중 수집한 정보로 최적화 결정
if (type == "string") { // 95%의 경우 true
// JIT: 이 분기를 최적화 (인라인 캐시)
}
// 2. 추측적 최적화 (Speculative Optimization)
// 가정이 맞는 동안 빠른 코드 실행
// 가정이 틀리면 탈최적화(deoptimization) 후 인터프리터로 복귀
// 3. 적응적 최적화 (Adaptive Optimization)
// 핫한 코드만 최적화, 콜드 코드는 인터프리터로 실행
// 컴파일 시간과 실행 시간의 균형
3.3 주요 JIT 엔진
Java HotSpot JVM:
실행 흐름:
1. 바이트코드 인터프리터로 시작
2. 호출 횟수가 임계값을 초과하면 C1 컴파일러 (빠른 컴파일, 간단한 최적화)
3. 더 많이 실행되면 C2 컴파일러 (느린 컴파일, 공격적 최적화)
계층적 컴파일 (Tiered Compilation):
Level 0: 인터프리터
Level 1: C1 (프로파일링 없음)
Level 2: C1 (제한적 프로파일링)
Level 3: C1 (전체 프로파일링)
Level 4: C2 (최적화 코드)
JavaScript V8:
실행 흐름:
1. 파서: JavaScript -> AST
2. Ignition (인터프리터): AST -> 바이트코드 실행
3. Sparkplug (베이스라인 JIT): 빠른 기계어 생성
4. Maglev (중간 계층 JIT): 중간 수준 최적화
5. TurboFan (최적화 JIT): 공격적 최적화
핵심 기법:
- Hidden Classes: 동적 타입 객체에 구조 부여
- Inline Caching: 프로퍼티 접근 최적화
- Deoptimization: 추측이 틀리면 인터프리터로 복귀
4. 현대 언어 기능과 컴파일 과제
4.1 제네릭 (Generics)
// 제네릭 구현 전략:
// 1. 단형화 (Monomorphization) - Rust, C++
// 각 구체 타입에 대해 별도의 코드 생성
// 장점: 빠른 실행 (인라이닝, 특화 최적화)
// 단점: 코드 크기 증가 (code bloat)
// 2. 타입 소거 (Type Erasure) - Java, Kotlin
// 컴파일 시 제네릭 타입 정보 제거, Object로 처리
// 장점: 코드 크기 작음
// 단점: 박싱/언박싱 오버헤드, 런타임 타입 정보 손실
// 3. 사전 전달 (Dictionary Passing) - Haskell
// 타입 클래스의 메서드 테이블을 인수로 전달
// 장점: 코드 크기 작음
// 단점: 간접 호출 오버헤드
4.2 클로저 (Closure)
// 클로저: 자유 변수를 캡처하는 함수
// 컴파일 시 처리:
// 1. 캡처된 변수를 구조체(환경)에 저장
// 2. 클로저 = 함수 포인터 + 환경 포인터
// 예시 (의사 코드):
// 원본:
// fn make_adder(x):
// return fn(y): x + y
// 컴파일 후:
// struct Env { int x; }
// int closure_fn(Env* env, int y) { return env->x + y; }
// Closure make_adder(int x) {
// Env* env = alloc(Env);
// env->x = x;
// return (closure_fn, env);
// }
4.3 패턴 매칭 (Pattern Matching)
// 패턴 매칭 컴파일 전략:
// 1. 결정 트리 (Decision Tree)
// 각 패턴을 순차적으로 테스트
// 검사 횟수를 최소화하도록 트리 구성
// 2. 백트래킹 오토마톤
// 여러 패턴을 동시에 매칭
// 메모리 효율적이지만 구현 복잡
// 예시:
// match value {
// (0, y) => ...,
// (x, 0) => ...,
// (x, y) => ...,
// }
//
// 결정 트리:
// value.0 == 0?
// yes -> 패턴 1 (y = value.1)
// no -> value.1 == 0?
// yes -> 패턴 2 (x = value.0)
// no -> 패턴 3 (x = value.0, y = value.1)
5. 보안 분야의 컴파일러 기술
5.1 정적 분석 (Static Analysis)
컴파일러 기술을 활용하여 코드를 실행하지 않고 버그를 찾습니다.
주요 정적 분석 도구:
- Clang Static Analyzer: 경로 민감 분석, 메모리 버그 탐지
- Coverity: 상용 정적 분석 도구
- Infer (Meta): 대규모 코드베이스에서 메모리 안전성 검사
- CodeQL (GitHub): 쿼리 기반 코드 분석
탐지 가능한 문제:
- 널 포인터 역참조
- 버퍼 오버플로우
- 메모리 누수
- 사용 후 해제 (use-after-free)
- 데이터 레이스
5.2 새니타이저 (Sanitizers)
컴파일 시 검사 코드를 삽입하여 런타임에 버그를 탐지합니다.
주요 새니타이저 (LLVM/GCC 지원):
AddressSanitizer (ASan):
- 메모리 접근 오류 탐지 (버퍼 오버플로우, use-after-free)
- 약 2배의 성능 오버헤드
- 컴파일: clang -fsanitize=address
MemorySanitizer (MSan):
- 초기화되지 않은 메모리 읽기 탐지
- 약 3배의 성능 오버헤드
ThreadSanitizer (TSan):
- 데이터 레이스 탐지
- 약 5-15배의 성능 오버헤드
UndefinedBehaviorSanitizer (UBSan):
- 정의되지 않은 동작 탐지 (정수 오버플로우, 잘못된 시프트 등)
- 최소한의 성능 오버헤드
ASan의 동작 원리:
// 원본 코드:
int a[10];
a[15] = 42; // 버퍼 오버플로우!
// ASan이 삽입하는 코드 (개념적):
// 1. 메모리 주변에 "레드 존" 설정
// 2. 모든 메모리 접근 전 경계 검사
// 3. 접근이 레드 존에 닿으면 오류 보고
// 실행 시 출력:
// ERROR: AddressSanitizer: stack-buffer-overflow
// WRITE of size 4 at address ...
// [스택 트레이스]
5.3 제어 흐름 무결성 (CFI)
간접 분기의 대상을 검증하여 코드 재사용 공격(ROP, JOP)을 방지합니다.
// 제어 흐름 무결성 (LLVM CFI):
// 함수 포인터를 통한 호출이 유효한 대상만 호출하도록 검증
// clang -fsanitize=cfi
// 간접 호출 시 대상 함수의 시그니처를 검증
6. AI/ML 분야의 컴파일러
6.1 딥러닝 컴파일러의 필요성
전통적 방식:
PyTorch/TensorFlow -> 프레임워크 런타임 -> cuDNN/MKL -> GPU/CPU
컴파일러 방식:
모델 정의 -> 그래프 IR -> 최적화 -> 코드 생성 -> GPU/CPU/TPU/NPU
장점:
- 하드웨어 특화 최적화 자동화
- 새 하드웨어에 대한 빠른 지원
- 연산 융합(operator fusion)으로 메모리 접근 최소화
6.2 XLA (Accelerated Linear Algebra)
Google이 개발한 딥러닝 컴파일러로, TensorFlow와 JAX에서 사용됩니다.
XLA의 최적화:
1. 연산 융합 (Operation Fusion)
- 여러 원소별 연산을 하나의 커널로 합침
- 중간 텐서의 메모리 할당 제거
2. 레이아웃 최적화
- 텐서의 메모리 레이아웃을 하드웨어에 최적화
3. 상수 폴딩
- 컴파일 시 결정 가능한 텐서 연산을 미리 계산
6.3 TVM (Tensor Virtual Machine)
Apache TVM은 다양한 하드웨어를 대상으로 한 오픈소스 딥러닝 컴파일러입니다.
TVM 스택:
프론트엔드: PyTorch, TensorFlow, ONNX 모델 가져오기
|
Relay IR: 고수준 그래프 표현
|
Relay 최적화: 그래프 수준 최적화 (융합, 양자화 등)
|
Tensor IR (TIR): 저수준 텐서 연산 표현
|
AutoTVM/Ansor: 자동 성능 튜닝 (스케줄 탐색)
|
코드 생성: CUDA, OpenCL, Metal, LLVM 등
6.4 기타 ML 컴파일러
- MLIR (Multi-Level IR): LLVM 프로젝트의 다계층 IR 프레임워크
다양한 추상화 수준을 지원하는 통합 컴파일러 인프라
- Triton: GPU 커널 작성을 위한 Python 기반 언어/컴파일러
PyTorch 2.0의 torch.compile 백엔드
- IREE: ML 모델을 임베디드/모바일 환경에 배포하기 위한 컴파일러
- StableHLO: ML 모델의 이식 가능한 직렬화 형식
7. WebAssembly 컴파일
7.1 WebAssembly (Wasm) 개요
WebAssembly는 웹 브라우저에서 네이티브에 가까운 속도로 실행되는 바이너리 명령어 형식입니다.
특징:
- 스택 기반 가상 머신
- 정적 타입 시스템
- 메모리 안전 (선형 메모리 모델)
- 다양한 언어에서 컴파일 대상으로 사용 가능
- 브라우저뿐 아니라 서버, 임베디드에서도 사용 확대
7.2 Wasm으로의 컴파일 파이프라인
C/C++ -> Emscripten -> LLVM -> Wasm 백엔드 -> .wasm
Rust -> rustc -> LLVM -> Wasm 백엔드 -> .wasm
Go -> TinyGo -> LLVM -> Wasm 백엔드 -> .wasm
Kotlin -> Kotlin/Wasm -> .wasm
7.3 Wasm 텍스트 형식 예시
;; WAT (WebAssembly Text Format) 예시: 피보나치
(module
(func $fib (param $n i32) (result i32)
(if (i32.lt_s (local.get $n) (i32.const 2))
(then (return (local.get $n)))
)
(i32.add
(call $fib (i32.sub (local.get $n) (i32.const 1)))
(call $fib (i32.sub (local.get $n) (i32.const 2)))
)
)
(export "fib" (func $fib))
)
7.4 Wasm의 최적화 과제
주요 과제:
1. GC 통합: 참조 타입과 GC 지원 (Wasm GC 제안)
2. SIMD: 벡터 연산 지원 (128비트 SIMD 구현됨)
3. 스레드: 공유 메모리와 원자적 연산
4. 예외 처리: 제로 비용 예외 처리
5. 꼬리 호출: 함수형 언어 지원
최적화 도구:
- Binaryen: Wasm-specific 최적화 (wasm-opt)
- 죽은 코드 제거
- 함수 인라이닝
- 상수 폴딩
- 코드 크기 최적화
7.5 WASI (WebAssembly System Interface)
브라우저 밖에서 Wasm을 실행하기 위한 시스템 인터페이스입니다.
응용 분야:
- 서버리스 컴퓨팅: Cloudflare Workers, Fastly Compute
- 컨테이너 대안: Docker + Wasm
- 플러그인 시스템: 안전한 확장 실행 환경
- 임베디드 시스템: 리소스 제한 환경
8. 미래 전망
8.1 컴파일러 기술의 발전 방향
1. AI 기반 컴파일러 최적화
- 강화 학습으로 최적화 패스 순서 결정
- 신경망 기반 레지스터 할당
- LLM 기반 코드 분석 및 최적화 제안
2. 도메인 특화 컴파일러 (DSL Compiler)
- 특정 분야(ML, 그래프, 데이터베이스)에 최적화된 컴파일러
- Halide (이미지 처리), GraphIt (그래프 알고리즘)
3. 검증된 컴파일러 (Verified Compiler)
- CompCert: 수학적으로 검증된 C 컴파일러
- 최적화의 정확성을 형식적으로 증명
4. 이종 컴퓨팅 지원
- CPU + GPU + FPGA + NPU를 통합 관리하는 컴파일러
- SYCL, OneAPI (Intel) 등의 표준
5. 보안 내장 컴파일러
- 메모리 안전 언어(Rust) 확산
- 하드웨어 보안 기능(ARM MTE, Intel CET) 활용
정리
| 개념 | 설명 |
|---|---|
| LLVM | 모듈화된 컴파일러 인프라, 3단계 아키텍처 |
| Clang | LLVM 기반 C/C++ 프론트엔드 |
| JIT 컴파일 | 실행 중 핫 코드를 기계어로 컴파일 |
| 단형화 | 제네릭을 구체 타입별로 코드 생성 |
| 정적 분석 | 실행 없이 코드의 버그를 찾는 기법 |
| 새니타이저 | 런타임 검사 코드를 삽입하여 버그 탐지 |
| XLA | Google의 ML 컴파일러 (TensorFlow/JAX) |
| TVM | 다양한 하드웨어 대상 오픈소스 ML 컴파일러 |
| WebAssembly | 웹 브라우저용 이식 가능한 바이너리 형식 |
| WASI | Wasm의 시스템 인터페이스 표준 |
컴파일러 기술은 단순히 소스 코드를 기계어로 변환하는 것을 넘어, 보안, AI/ML, 웹 기술 등 다양한 분야에서 핵심적인 역할을 하고 있습니다. 특히 이종 하드웨어의 등장과 AI 워크로드의 폭증으로 인해 컴파일러의 중요성은 더욱 커지고 있습니다. 이 시리즈에서 다룬 기본 원리들(어휘 분석부터 코드 최적화까지)이 이 모든 현대적 응용의 기반이 됩니다.