← 홈으로 돌아가기
Swift Performance Guide

Swift Dispatch 메커니즘과 Existential Container

any / Generic / Witness Table / Existential Container / Boxing 완전 정리 — 언제 뭘 써야 하는가?

Performance Type System Best Practice
핵심 질문

"제네릭은 기본적으로 정적 디스패치인데 다른 모듈에서 사용 시 동적 디스패치이다" — 이 말은 틀렸다. Witness Table은 동적 디스패치가 아니다. 이 문서는 이러한 혼동을 정리한다.

Why This Work Exists

개발자들이 anyGeneric의 차이를 헷갈려서 성능 문제를 만들거나, Witness Table을 동적 디스패치로 오해해서 잘못된 최적화를 시도한다.

이 문서는 Swift의 디스패치 메커니즘을 정확히 이해하고, 상황에 맞는 선택을 할 수 있도록 돕는다.

Scope / Non-scope

✅ 이번 문서 범위:

  • Static Dispatch vs Dynamic Dispatch 정확한 정의
  • Witness Table의 동작 원리 (Generic 사용 시)
  • Existential Container 구조와 Boxing/Unboxing
  • any vs Generic 성능 비교와 선택 기준

❌ 제외:

  • Compiler optimization flags (-O, -Osize)
  • SIL (Swift Intermediate Language) 분석
  • Assembly 레벨 디버깅

1. 용어 사전 (필수 개념)

용어 정의 핵심 포인트
Static Dispatch 컴파일 타임에 호출할 함수가 결정됨 가장 빠름, 인라인 최적화 가능
Dynamic Dispatch 런타임에 타입 확인 후 함수 결정 vtable 또는 런타임 타입 체킹 필요
Witness Table Protocol 요구사항 → 구체 타입 구현 매핑 테이블 컴파일 타임에 결정됨 (Generic 사용 시)
Existential Container any Protocol 값을 저장하는 구조 (단일 프로토콜 기준 40 bytes) Value Buffer(24B) + Type Metadata ptr + PWT ptr
Boxing 구체 타입 → Existential Container 변환 오버헤드 발생
Unboxing Existential Container → 구체 타입 추출 런타임 타입 체킹 필요
Specialization 컴파일러가 Generic을 구체 타입 버전으로 최적화 Static Dispatch 가능
Vtable Virtual Table - Class의 메서드 주소들을 담은 테이블 Dynamic Dispatch에 사용
Inline 저장 값을 Value Buffer에 직접 저장 (≤ 24 bytes) 캐시 효율적, 힙 할당 없음

As-is (잘못된 이해)

❌ 오해 1:
"Generic은 다른 모듈에서 사용하면 동적 디스패치다"

❌ 오해 2:
"Witness Table = Dynamic Dispatch"

❌ 오해 3:
"any를 쓰면 무조건 느리니까 절대 쓰지 마라"

To-be (정확한 이해)

✅ 사실 1:
"Generic은 Witness Table을 사용하지만, 컴파일 타임에 결정된다"

✅ 사실 2:
"Witness Table ≠ Dynamic Dispatch (런타임 타입 체킹 없음)"

✅ 사실 3:
"any는 다양한 타입을 섞어 저장해야 할 때 정당한 선택"

What The Developer Must Do Next

개발 시 의사결정 순서:

  1. 질문 1: 다양한 타입을 하나의 배열/변수에 섞어서 저장해야 하는가?
    • YES → any Protocol 사용
    • NO → 질문 2로
  2. 질문 2: 한 가지 타입만 다루는가?
    • YES → Generic<T: Protocol> 사용
    • NO → 질문 1로 돌아가기
  3. 질문 3: Struct로 충분한가?
    • YES → struct 사용 (Static Dispatch)
    • NO (상속 필요) → class 사용 (vtable)

2. Static Dispatch (정적 디스패치)

정의: 컴파일 타임에 호출할 함수가 정확히 결정되는 방식

특징

언제 사용되나?

1. Struct (Value Type)

struct Dog {
    func bark() {
        print("멍멍")
    }
}

let dog = Dog()
dog.bark()  // ← Static Dispatch ✅

2. final class (상속 불가)

final class Cat {  // ← final = 상속 불가
    func meow() {
        print("야옹")
    }
}

let cat = Cat()
cat.meow()  // ← Static Dispatch ✅
왜 final이 중요한가?
final 키워드 = 상속 불가 선언
• meow()가 오버라이드될 가능성 0%
• 컴파일러가 Cat.meow() 주소를 확정할 수 있음
• → Static Dispatch + 인라이닝 가능 ✅

3. Generic (Specialization)

func makeSound<T: Animal>(animal: T) {
    animal.sound()  // ← Specialization → Static Dispatch ✅
}

let dog = Dog()
makeSound(animal: dog)  // 컴파일러가 T = Dog 알고 있음

Specialization 과정

원본 코드:
func process<T: Animal>(animal: T)
↓ 컴파일러가 T = Dog로 Specialization
최적화된 버전:
func process_Dog(animal: Dog)
→ 직접 호출 가능, 인라인 최적화 가능

컴파일러 최적화: Private 메서드

class MyClass {
    private func helper() {  // ← private: 이 클래스 안에서만 사용
        print("도움말")
    }

    func publicMethod() {
        helper()  // ← 컴파일러가 Static Dispatch로 최적화 가능 ✅
    }
}
왜 private은 Static으로 최적화되나?
private = 이 클래스 안에서만 접근 가능
• 서브클래스에서 오버라이드 불가
• 컴파일러가 "helper()는 항상 이 구현만 호출된다"고 확신
• → vtable 없이 Static Dispatch로 최적화 가능 ✅

참고: WMO(Whole Module Optimization) 활성화 시 더 적극적으로 최적화됩니다.

3. Dynamic Dispatch (동적 디스패치)

정의: 런타임에 실제 타입을 확인하고 호출할 함수를 결정하는 방식

특징

언제 사용되나?

1. Class (상속 가능)

class Animal {
    func sound() {
        print("...")
    }
}

class Dog: Animal {
    override func sound() {
        print("멍멍")
    }
}

let animal: Animal = Dog()
animal.sound()  // ← Dynamic Dispatch (vtable 사용) ✅

2. any (Existential Type)

protocol Drawable {
    func draw()
}

let drawable: any Drawable = Circle()
drawable.draw()  // ← Dynamic Dispatch (PWT 경유 간접 호출, 타입 비교 아님) ✅

Vtable (Virtual Table) 이란?

정의: Class의 메서드 주소들을 담은 테이블 (가상 함수 테이블)

Vtable 구조

Animal의 Vtable:
• sound() → 0x1000 (Animal.sound 주소)
• eat() → 0x2000 (Animal.eat 주소)
Dog's Vtable:
• sound() → 0x3000 (Dog.sound 주소) ← 오버라이드됨
• eat() → 0x2000 (Animal.eat 주소) ← 상속받음
Cat's Vtable:
• sound() → 0x4000 (Cat.sound 주소) ← 오버라이드됨
• eat() → 0x2000 (Animal.eat 주소) ← 상속받음

인스턴스 메모리 구조

Dog 인스턴스:
┌─────────────────┐
│ vtable pointer │ ──→ Dog's Vtable
│ stored data │
└─────────────────┘
animal.sound() 호출 과정 ↓
Step 1: animal의 vtable pointer 읽기 → Dog's Vtable
Step 2: vtable에서 sound() 슬롯 확인 → 0x3000
Step 3: 0x3000 주소로 점프 → Dog.sound() 실행
왜 vtable이 필요한가?
let animal: Animal = Dog() ← 선언 타입은 Animal, 실제는 Dog
• 컴파일 타임에는 어떤 sound()를 호출할지 모름
• 런타임에 vtable을 보고 "아, Dog.sound()를 호출해야 하는구나!" 판단
• → 다형성(Polymorphism) 구현

중요: 구체 타입으로 선언해도 Dynamic Dispatch

class Dog {  // ← final 아님, 상속 가능
    func bark() { print("멍멍") }
}

let animal: Dog = Dog()  // ← 구체 타입으로 선언
animal.bark()  // ← 여전히 Dynamic Dispatch (vtable 사용) ❌
주의: Dog 타입으로 선언해도, final이 아니면 Dynamic Dispatch입니다!

이유:
• Dog가 상속 가능 → 서브클래스가 bark()를 오버라이드할 수 있음
• 컴파일러는 안전을 위해 vtable 사용
• WMO(Whole Module Optimization) 활성화 시 최적화 가능

Static Dispatch를 원하면: final class Dog로 선언 ✅

4. Witness Table (Protocol용) ⭐

정의: Protocol 요구사항을 구체 타입의 구현으로 매핑하는 테이블

핵심 포인트

Witness Table vs Dynamic Dispatch

Witness Table (Generic) Dynamic Dispatch (any)
테이블 결정 시점 컴파일 타임 ✅ 런타임 ❌
런타임 타입 체킹 불필요 ✅ 필요 ❌
인라인 가능 불가 (간접 호출) 불가 (간접 호출)
성능 빠름 느림
⚠️ 중요: Witness Table은 간접 호출이지만 "동적 디스패치"가 아닙니다! 컴파일 타임에 어떤 테이블을 쓸지 결정되기 때문입니다.

Witness Table 구조

Protocol 정의

protocol Drawable {
    func draw()
    func erase()
}

각 타입마다 Witness Table 생성

Circle's Witness Table (for Drawable)
• draw() → Circle.draw 주소
• erase() → Circle.erase 주소
Square's Witness Table (for Drawable)
• draw() → Square.draw 주소
• erase() → Square.erase 주소

Generic과 Witness Table

// Generic 함수
func render<T: Drawable>(shape: T) {
    shape.draw()
}

// 컴파일러가 내부적으로 하는 일 (개념):
func render<T: Drawable>(
    shape: T,
    witnessTable: DrawableWitnessTable  // 컴파일 타임에 전달 ✅
) {
    witnessTable.draw(shape)  // 간접 호출
}

// 호출
let circle = Circle()
render(shape: circle)
// → 컴파일러가 Circle의 Witness Table을 자동으로 전달

5. Existential Container ⭐

정의: any Protocol 타입의 값을 저장하는 특수한 메모리 구조 (단일 프로토콜 기준 40 bytes — 채택 프로토콜 수만큼 PWT 슬롯이 늘어 가변)

왜 필요한가?

// 문제: Protocol 타입으로 다양한 크기의 값을 저장해야 함
protocol Drawable { }

struct Point: Drawable {
    var x: Int  // 8 bytes
    var y: Int  // 8 bytes
    // 총 16 bytes
}

struct Circle: Drawable {
    var center: Point  // 16 bytes
    var radius: Double // 8 bytes
    var color: Color   // 4 bytes
    // 총 28 bytes
}

// 크기가 다른 값들을 어떻게 하나의 타입으로 저장?
var drawable: any Drawable = Point(x: 0, y: 0)
drawable = Circle(...)  // 크기가 달라도 저장 가능해야 함
핵심 문제: Point는 16 bytes, Circle은 28 bytes인데, any Drawable 변수 하나로 둘 다 저장할 수 있어야 합니다. 이를 해결하는 게 Existential Container입니다.

Existential Container 구조 (단일 프로토콜 기준 40 bytes)

전체 구조

1. Value Buffer (24 bytes / 3 words)
• 작은 값 (≤ 24 bytes): 여기에 직접 저장 (inline)
• 큰 값 (> 24 bytes): 힙에 할당하고 포인터 저장
→ 실제 값 또는 힙 포인터
2. Type Metadata Pointer (8 bytes)
• 그 값의 타입 정보(크기·정렬·종류)
• 값 관리 함수표(VWT: copy/destroy 등)는 이 metadata를 통해 접근 — VWT가 컨테이너에 별도 슬롯으로 들어가는 게 아님
→ "이 값이 무슨 타입이고, VWT는 여기서 찾는다"
3. Protocol Witness Table Pointer (8 bytes) [PWT]
• Protocol 메서드 구현 주소들
• draw(), erase() 등의 실제 구현 주소
→ "draw()를 어떻게 호출하나?"

비유로 이해하기 🎁

Existential Container = 택배 상자
  • 상자 크기: 항상 40 bytes (고정)
  • Value Buffer (24 bytes): 상자 안 공간
    • 작은 물건: 상자에 직접 넣음
    • 큰 물건: 창고(힙)에 보관하고 주소표(포인터)만 상자에 넣음
  • VWT (8 bytes): "취급 주의" 스티커
    • "이 물건을 어떻게 복사하나?"
    • "이 물건을 어떻게 버리나?"
  • PWT (8 bytes): "사용 설명서" 스티커
    • "draw() 버튼은 어디 있나?"
    • "erase() 기능은 어떻게 쓰나?"

Container 저장 위치: 스택 vs 힙

Container 자체는 기본적으로 스택에 저장됩니다
// 일반적인 경우: Container는 스택에
func example() {
    let drawable: any Drawable = Circle(radius: 5.0)
    // ↑ Container (40 bytes)는 스택에 저장
}

// 클래스 프로퍼티: Container는 힙에
class ViewModel {
    var drawable: any Drawable  // ← 클래스는 힙에 있으므로 Container도 힙에
}

// 클로저 캡처: escaping이면 힙으로 이동
let closure = {
    let drawable: any Drawable = Circle(radius: 5.0)
    // escaping closure면 힙으로 이동
}
중요: Container 자체의 위치와, Value Buffer 안의 값이 힙에 있는지는 별개입니다.

• Container는 스택에, Value Buffer 안의 큰 값은 힙에 있을 수 있습니다.

Value Buffer 동작

작은 값 (≤ 24 bytes) - Inline 저장

struct Point: Drawable {
    var x: Int  // 8 bytes
    var y: Int  // 8 bytes
}

var drawable: any Drawable = Point(x: 10, y: 20)

// Container 내부:
// [Value Buffer: x=10, y=20, (unused)]  ← 값을 직접 저장 (Inline)
// [VWT pointer]
// [PWT pointer]

Inline 저장: 연속된 메모리 (캐시 효율적)

스택 (연속된 메모리):
주소 0x1000: [x: 10]
주소 0x1008: [y: 20]
주소 0x1010: [unused]
주소 0x1018: [VWT]
주소 0x1020: [PWT]
→ 한 번에 CPU 캐시에 로드 가능 ✅
Inline 저장 장점:
캐시 효율적: Container 전체가 연속된 메모리에 있어서 CPU 캐시에 한 번에 로드됨
캐시 히트: 값 접근 시 캐시에 이미 있음 (빠름, ~1-2 사이클)
힙 할당 없음: 메모리 할당/해제 오버헤드 없음

큰 값 (> 24 bytes) - 힙 할당

struct BigShape: Drawable {
    var center: Point    // 16 bytes
    var radius: Double   // 8 bytes
    var color: UInt32    // 4 bytes
    var opacity: Double  // 8 bytes
    // 총 36 bytes > 24 bytes
}

var drawable: any Drawable = BigShape(...)

// Container 내부:
// [Value Buffer: heap pointer → 힙의 BigShape, (unused), (unused)]
// [VWT pointer]
// [PWT pointer]

Non-Inline 저장: 메모리 점프 (캐시 미스 가능)

스택 (Container):
주소 0x1000: [heap pointer: 0x5000]
주소 0x1008: [unused]
주소 0x1010: [unused]
주소 0x1018: [VWT]
주소 0x1020: [PWT]
메모리 점프 ↓ (멀리 떨어진 주소로 이동)
힙 (실제 값):
주소 0x5000: [BigShape 데이터 36 bytes]
→ 캐시에 없을 확률 높음 (캐시 미스) ❌
Non-Inline 저장 단점:
메모리 점프: 포인터 따라가면 멀리 떨어진 힙 영역으로 이동
캐시 미스: 힙 데이터가 캐시에 없어서 메인 메모리까지 가야 함 (느림, ~100-300 사이클)
힙 할당 오버헤드: 메모리 할당/해제 비용 발생

캐시 효율성 설명

CPU 캐시란? 메모리보다 훨씬 빠른 임시 저장소

속도 비교

CPU 레지스터 (가장 빠름)
~1 사이클
L1 캐시 (매우 빠름)
~1-2 사이클
L2/L3 캐시 (빠름)
~10-40 사이클
메인 메모리 (느림)
~100-300 사이클
구분 Inline (≤ 24 bytes) Non-Inline (> 24 bytes)
값 위치 Value Buffer에 직접 저장 힙에 저장, 포인터만 Buffer에
메모리 레이아웃 연속된 메모리 Container와 값이 멀리 떨어짐
첫 접근 시 Container 전체가 캐시에 로드 Container만 캐시에 로드 (값은 힙에)
값 사용 시 이미 캐시에 있음 (캐시 히트) 다시 힙 메모리 확인 (캐시 미스)
캐시 효율 높음 (캐시 히트) 낮음 (캐시 미스 가능)
힙 할당 없음 ✅ 있음 ❌
성능 빠름 (~1-2 사이클) 느림 (~100-300 사이클)
한 줄 요약:
Inline: CPU가 64 bytes씩 메모리를 읽을 때 Container에 데이터가 inline하게 들어가 있으면, 처음 읽을 때부터 캐시에 저장되어서 사용할 때 캐시 히트로 바로 사용 가능 ✅

Non-Inline: 처음에 Container만 캐시에 로드되고, 실제 값은 힙에 있어서 사용할 때 다시 힙 메모리에서 데이터를 확인해야 하기 때문에 느림 ❌

Array의 특수한 경우

주의: Swift Array는 복잡한 구조입니다
struct ShapeWithArray: Drawable {
    var data: [Double]
}

// Array의 내부 구조 (간소화):
// - buffer pointer (8 bytes)  ← 힙의 실제 배열 데이터를 가리킴
// - count (8 bytes)
// - capacity (8 bytes)
// 총 약 24 bytes (메타데이터만)

Array를 담은 Existential Container

Container (스택):
[Value Buffer: Array 구조체 24 bytes]
  └─ buffer pointer → 힙: [1.0, 2.0, 3.0, ...]
[VWT]
[PWT]
결과:
• Array 구조체 자체는 Value Buffer에 inline 저장 가능 (24 bytes)
• 하지만 실제 배열 요소는 항상 힙에 저장됨
• 결국 포인터를 따라가야 하므로 Non-Inline처럼 동작
오버헤드 정리:
• Existential Container는 항상 40 bytes 차지
• 큰 값 (> 24 bytes)은 힙 할당 추가 오버헤드
• Array는 inline 저장되어도 요소 접근 시 힙 접근 필요
• Value Witness Table 호출 오버헤드
• 캐시 미스로 인한 성능 저하

VWT (Value Witness Table) 상세

역할: "이 값을 어떻게 관리하나?"에 대한 답변을 담은 함수 포인터 테이블

VWT가 제공하는 함수들

Circle's Value Witness Table
allocate → "Circle을 어떻게 메모리 할당?"
copy → "Circle을 어떻게 복사?"
destroy → "Circle을 어떻게 파괴?"
assignWithCopy → "Circle을 어떻게 대입?"
assignWithTake → "Circle을 어떻게 이동?"
→ 타입마다 다른 관리 방법
// 예시: Circle 복사할 때
var drawable1: any Drawable = Circle(radius: 5.0)
var drawable2 = drawable1  // 복사 발생

// 내부 동작:
// 1. drawable1의 VWT pointer 찾기
// 2. VWT에서 copy() 함수 주소 찾기
// 3. copy() 호출해서 Circle 복사
// 4. drawable2의 Container에 복사된 값 저장
왜 필요한가? Swift 컴파일러는 any Drawable 변수를 복사할 때, 실제 타입이 Circle인지 Point인지 모릅니다. VWT가 "이 타입을 복사하는 방법"을 알려줍니다.

PWT (Protocol Witness Table) 상세

역할: "이 Protocol 메서드를 어떻게 호출하나?"에 대한 답변을 담은 함수 포인터 테이블

PWT가 제공하는 함수들

Circle's Protocol Witness Table (for Drawable)
draw() → Circle.draw() 함수 주소 (0x1000)
erase() → Circle.erase() 함수 주소 (0x1100)
→ Protocol 요구사항 → 실제 구현 매핑
// 예시: draw() 호출할 때
let drawable: any Drawable = Circle(radius: 5.0)
drawable.draw()

// 내부 동작:
// 1. drawable의 PWT pointer 찾기
// 2. PWT에서 draw() 함수 주소 찾기 (0x1000)
// 3. Value Buffer에서 Circle 값 꺼내기
// 4. 0x1000 주소로 점프 (Circle.draw() 실행)

VWT vs PWT 비교

구분 VWT (Value Witness Table) PWT (Protocol Witness Table)
목적 값 관리 (복사, 삭제 등) Protocol 메서드 호출
언제 사용 변수 대입, 복사, 해제 시 Protocol 메서드 호출 시
함수 예시 allocate, copy, destroy draw(), erase(), validate()
누가 정의 Swift 컴파일러 (자동 생성) 개발자 (Protocol 구현)

6. Boxing과 Unboxing ⭐

Boxing (Boxing Operation)

정의: 구체 타입 (Circle, Point 등)의 값을 Existential Container에 포장하는 과정
struct Circle: Drawable {
    var radius: Double  // 8 bytes
}

let circle = Circle(radius: 5.0)  // 스택에 Circle 값 (8 bytes)

// Boxing 발생 ↓
let drawable: any Drawable = circle  // Container에 포장 (40 bytes)

Boxing 과정 상세

Step 1: Existential Container 할당
스택에 40 bytes 공간 확보
Step 2: 값 복사
circle 값 (radius: 5.0)을 Value Buffer에 복사
→ 8 bytes만 사용, 16 bytes 남음
Step 3: VWT 설정
Circle의 Value Witness Table 주소 저장
→ "Circle을 어떻게 복사/삭제하나?"
Step 4: PWT 설정
Circle's Drawable Witness Table 주소 저장
→ "draw()를 어떻게 호출하나?"
완료: Existential Container
[Value Buffer: radius=5.0, unused, unused]
[VWT: Circle's VWT 주소]
[PWT: Circle's Drawable PWT 주소]
// Boxing 비용 발생 시점
let drawable: any Drawable = Circle(radius: 5.0)  // ← Boxing ✅

// 배열에 넣을 때도 각각 Boxing
let shapes: [any Drawable] = [
    Circle(radius: 5.0),   // ← Boxing ✅
    Point(x: 10, y: 20),   // ← Boxing ✅
    Square(side: 15)       // ← Boxing ✅
]
// 총 3번의 Boxing 발생

Unboxing (Unboxing Operation)

정의: Existential Container에서 실제 값을 꺼내거나, 메서드를 호출하는 과정

Case 1: 메서드 호출 시 Unboxing

let drawable: any Drawable = Circle(radius: 5.0)

// 메서드 호출 시 Unboxing 발생
drawable.draw()

Unboxing 과정 상세 (메서드 호출)

Step 1: PWT 찾기
Container의 PWT pointer 읽기
Step 2: 메서드 주소 찾기
PWT에서 draw() 함수 주소 찾기 (0x1000)
Step 3: 값 꺼내기
Value Buffer에서 Circle 값 꺼내기
→ radius: 5.0
Step 4: 함수 호출
0x1000 주소로 점프 (Circle.draw() 실행)

Case 2: 타입 캐스팅으로 Unboxing

let drawable: any Drawable = Circle(radius: 5.0)

// 타입 캐스팅으로 완전히 Unboxing
if let circle = drawable as? Circle {
    // Circle 타입으로 Unboxing 성공
    print(circle.radius)  // 5.0
    // 이제 circle은 일반 Circle 타입 (Container 없음)
}

타입 캐스팅 Unboxing 과정

Step 1: 런타임 타입 체킹
Container의 메타데이터에서 실제 타입 확인
→ "이게 Circle이 맞나?"
Step 2: 타입 일치 확인
Circle이 맞으면 성공, 아니면 nil
Step 3: 값 추출
Value Buffer에서 Circle 값 복사
→ circle 변수에 Circle(radius: 5.0) 할당

Boxing/Unboxing 오버헤드

성능 비용:
Boxing: Container 할당 (40 bytes) + 값 복사 + VWT/PWT 설정
Unboxing (메서드 호출): PWT 조회 + 간접 호출
Unboxing (타입 캐스팅): 런타임 타입 체킹 + 값 복사
반복적인 Boxing/Unboxing: 심각한 성능 저하

성능 비교 예시

❌ 나쁜 예: 반복적인 Boxing

func process(shapes: [any Drawable]) {
    for shape in shapes {
        shape.draw()  // 매번 Unboxing
    }
}

✅ 좋은 예: Generic 사용

func process<T: Drawable>(shapes: [T]) {
    for shape in shapes {
        shape.draw()  // Witness Table 사용 (Boxing 없음)
    }
}

7. 실전 비교: any vs Generic

any (Existential Type)

// any 사용
func drawAll(shapes: [any Drawable]) {
    for shape in shapes {
        shape.draw()
    }
}

let shapes: [any Drawable] = [
    Circle(),
    Square(),
    Triangle()
]

drawAll(shapes: shapes)

/*
동작:
1. 각 shape은 Existential Container에 Boxing됨
2. draw() 호출 시마다:
   - 런타임 타입 체킹
   - Protocol Witness Table 조회
   - Unboxing
   - 함수 호출
*/

Generic

// Generic 사용
func drawAll<T: Drawable>(shapes: [T]) {
    for shape in shapes {
        shape.draw()
    }
}

let circles = [Circle(), Circle(), Circle()]
drawAll(shapes: circles)

/*
동작:
1. 컴파일러가 T = Circle로 Specialization
2. Circle의 Witness Table이 컴파일 타임에 결정됨
3. draw() 호출 시:
   - 런타임 타입 체킹 없음 ✅
   - Witness Table 직접 사용
   - Boxing/Unboxing 없음 ✅
*/

성능 비교

항목 any (Existential) Generic
타입 결정 런타임 컴파일 타임
Boxing ✅ 필요 ❌ 불필요
런타임 타입 체킹 ✅ 필요 ❌ 불필요
Existential Container ✅ 사용 (40 bytes) ❌ 사용 안 함
힙 할당 큰 값은 힙 할당 스택 사용
성능 느림 빠름
유연성 높음 (타입 섞어 저장 가능) 낮음 (한 타입만)

언제 뭘 쓸까?

any 사용: 다양한 타입을 하나의 배열/변수에 저장해야 할 때
// any: 여러 타입을 섞어서 저장
let shapes: [any Drawable] = [
    Circle(),
    Square(),
    Triangle()
]
Generic 사용: 성능이 중요하고 한 가지 타입만 다룰 때
// Generic: 한 타입만 다룸, 성능 좋음
func process<T: Drawable>(shapes: [T]) {
    for shape in shapes {
        shape.draw()
    }
}

QA Checklist (코드 리뷰 시 확인)

Q1. any를 사용했는가?

다양한 타입을 섞어 저장하는 정당한 이유가 있는지 확인. 단일 타입이면 Generic으로 변경.

Q2. 반복문에서 Boxing이 발생하는가?

배열 순회 시 any를 사용하면 매번 Unboxing. 성능 병목이면 Generic 고려.

Q3. Struct 대신 Class를 써야 하는가?

상속이 필요한 경우만 Class. 그 외에는 Struct로 Static Dispatch 활용.

Q4. final 키워드를 추가할 수 있는가?

상속하지 않는 Class는 final로 선언해서 Static Dispatch 가능하게.

Q5. 프로파일링을 했는가?

추측하지 말고 Instruments로 실제 병목 지점 확인 후 최적화.

Q6. Existential Container 크기를 고려했는가?

24 bytes 이하의 작은 Struct는 inline 저장. 큰 값은 힙 할당 고려.

❓ 자주 묻는 질문 (FAQ)

Q. Existential Container가 뭔가요?

답: any Protocol 타입의 값을 저장하는 특수한 메모리 구조입니다.

• 고정 크기: 40 bytes
• 구조: Value Buffer (24B) + VWT (8B) + PWT (8B)
• 택배 상자처럼 "다양한 크기의 값을 하나의 상자에 담는 방법"

Point는 16 bytes, Circle은 28 bytes인데 any Drawable 변수 하나로 둘 다 저장할 수 있는 이유가 바로 이 Container 덕분입니다.

Q. VWT와 PWT는 뭐가 다른가요?

답:
VWT (Value Witness Table): "이 값을 어떻게 복사/삭제하나?" (값 관리)
PWT (Protocol Witness Table): "draw()를 어떻게 호출하나?" (메서드 호출)

비유:
VWT = "취급 주의" 스티커
PWT = "사용 설명서" 스티커

Q. Boxing이 정확히 뭔가요?

답: 구체 타입 (Circle)을 Existential Container에 포장하는 과정입니다.

과정:
1. Container 할당 (40 bytes)
2. 값 복사 (Value Buffer에)
3. VWT 주소 저장
4. PWT 주소 저장

원래 스택에 8 bytes로 저장될 Circle이 40 bytes Container에 담기게 됩니다.

Q. Unboxing은 언제 발생하나요?

답: 2가지 경우에 발생합니다.

1. 메서드 호출:
drawable.draw()
→ PWT 조회 → Value Buffer에서 값 꺼내기 → 메서드 실행

2. 타입 캐스팅:
if let circle = drawable as? Circle
→ 런타임 타입 체킹 → 값 추출

모두 런타임에 발생합니다.

Q. "구체 타입 → Existential Container 변환"이 무슨 뜻인가요?

답: Boxing을 다르게 표현한 것입니다.

구체 타입: Circle, Point처럼 실제 타입 이름이 있는 것
변환: Circle (8 bytes) → Container (40 bytes)로 형태가 바뀜

마치 책상 위의 책 (Circle)을 택배 상자에 넣는 것과 같습니다.
타입도 Circleany Drawable로 바뀝니다.

Q. Boxing은 컴파일 타임인가요, 런타임인가요?

답: 둘 다 맞습니다.

컴파일 타임: 컴파일러가 "여기에 Boxing이 필요하다"고 판단하고 코드 삽입
런타임: 실제로 Container에 값을 복사하는 실행

컴파일러가 미리 알고 준비해두고, 실제 실행은 런타임에 발생합니다.

Q. 왜 any는 느린가요?

답: Boxing/Unboxing 오버헤드 때문입니다.

Generic:
• Boxing 없음
• 런타임 타입 체킹 없음
• Witness Table은 컴파일 타임에 결정

any:
• 매번 Boxing (40 bytes 할당 + 복사)
• Unboxing (PWT 조회 + 값 꺼내기)
• 큰 값은 힙 할당까지

하지만 "여러 타입을 섞어 저장"할 때는 any가 필수입니다.

Q. 24 bytes 이하 값은 왜 빠른가요?

답: Value Buffer에 직접 저장되기 때문입니다.

작은 값 (≤ 24 bytes):
Container의 Value Buffer에 inline 저장 (힙 할당 없음) ✅

큰 값 (> 24 bytes):
힙에 할당하고 포인터만 Value Buffer에 저장 (힙 할당 오버헤드) ❌

따라서 작은 Struct를 사용하는 게 성능에 유리합니다.

Q. Existential Container는 스택/힙 어디에 저장되나요?

답: 기본적으로 스택에 저장됩니다.

일반 변수:
let drawable: any Drawable = ... → 스택 ✅

클래스 프로퍼티:
class ViewModel { var drawable: any Drawable } → 힙 (클래스가 힙에 있으므로)

escaping 클로저 캡처: → 힙

중요: Container 자체의 위치와, Value Buffer 안의 값이 힙에 있는지는 별개입니다!

Q. Inline이 정확히 뭔가요?

답: Value Buffer에 **데이터를 직접** 저장하는 방식입니다. (≤ 24 bytes)

Inline 저장 (메모리 점프 없음):
• Container의 Value Buffer 안에 데이터를 직접 저장
• 연속된 메모리 주소에 값이 나란히 배치
• Container 읽을 때 **메모리 점프 없이** 바로 데이터 접근 ✅
• 예: [x: 10, y: 20] ← Value Buffer에 직접 저장

Non-Inline 저장 (메모리 점프 발생):
• Value Buffer에는 **힙 포인터만** 저장
• 실제 데이터는 힙의 다른 주소에 위치
• 데이터 읽으려면 **메모리 점프 발생** (Container → 힙) ❌
• 예: [heap pointer → 0x7ffeab3c] → 힙의 데이터

핵심: Inline = 연속된 메모리, Non-Inline = 포인터 따라가야 함 (캐시 미스 가능성)

Q. 캐시 미스가 왜 느린가요?

답: CPU 캐시에 데이터가 없어서 느린 메인 메모리까지 가야 하기 때문입니다.

속도 차이:
• 캐시 히트 (데이터가 캐시에 있음): ~1-10 사이클 ✅
• 캐시 미스 (메인 메모리까지 가야 함): ~100-300 사이클 ❌

Inline 저장: 연속된 메모리 → 캐시에 한 번에 로드 → 캐시 히트
Non-Inline: 메모리 점프 (스택 → 힙) → 캐시 미스 가능성 높음

약 10-30배 속도 차이!

Q. Array를 담으면 어떻게 되나요?

답: Array는 특수한 경우입니다.

Array 구조:
• buffer pointer (힙 가리킴) + count + capacity
• 약 24 bytes (메타데이터만)

결과:
• Array 구조체는 Value Buffer에 inline 저장 가능
• 하지만 실제 배열 요소는 항상 힙에 저장됨
• 결국 포인터를 따라가야 하므로 Non-Inline처럼 동작

따라서 Array는 inline 저장되어도 성능상 큰 이득은 없습니다.

Q. 24 bytes 이하로 inline 저장되면 Static Dispatch인가요?

답: 아니요! Inline 저장 ≠ Static Dispatch입니다.

Inline 저장 (≤ 24 bytes):
• 메모리 할당 방식 (힙 할당 없음)
• 캐시 효율성에만 영향
• → 메모리 성능 개선 ✅

Static Dispatch:
• 메서드 호출 방식 (컴파일 타임 결정)
• 타입에 따라 결정됨 (Struct, final class, Generic)
• → 메서드 호출 성능 개선 ✅

중요: any Protocol은 값이 inline 저장되어도, 메서드 호출은 여전히 PWT를 통한 Dynamic Dispatch입니다!

let drawable: any Drawable = Point()
→ Point는 inline 저장됨 (메모리 빠름)
→ 하지만 drawable.draw()는 Dynamic Dispatch (메서드 호출 느림)

🔬 심화 개념 (Advanced Concepts)

1. Dynamic Dispatch가 느린 정확한 이유

핵심: 런타임에 메서드를 결정하기 때문에 느립니다.

세 가지 오버헤드:

// Static Dispatch (빠름):
dog.bark()
  ↓ 컴파일
call 0x3000  // 바로 점프 ✅

// Dynamic Dispatch (느림):
dog.bark()
  ↓ 런타임
1. dog의 vtable pointer 읽기  // 메모리 접근 1
2. vtable에서 bark() 슬롯 읽기  // 메모리 접근 2
3. 메서드 주소 읽기 (0x5000)
4. 0x5000으로 점프  // 여러 단계 거침 ❌

① 간접 호출 (Indirect Call):

② 인라이닝 최적화 불가:

// Static Dispatch - 인라이닝 가능:
func callSound(dog: Dog) {  // Dog 타입 확정
    dog.sound()
}
// 컴파일러 최적화 후:
// func callSound(dog: Dog) {
//     print("멍멍")  // ← 함수 호출 없이 코드 직접 삽입
// }

// Dynamic Dispatch - 인라이닝 불가:
func callSound(animal: Animal) {  // 실제 타입 모름
    animal.sound()  // 런타임까지 모르니까 인라이닝 불가 ❌
}
용어 주의: 여기서 "인라이닝" = 함수 인라이닝 (코드 직접 삽입)
이것은 "Inline 저장" (메모리 배치)과는 완전히 다른 개념입니다!

③ 메모리 접근 추가:

상대적 속도:

방식 상대 속도 비고
Static Dispatch 1x (기준) 가장 빠름
Dynamic Dispatch (vtable) 1.5~2x Class 메서드
Dynamic Dispatch (any + PWT) 2~3x Protocol 메서드
실무 관점: 나노초 단위 차이이므로 대부분의 경우 체감 불가. 반복문에서 수천/수만 번 호출할 때만 영향.

2. Class 인스턴스 메모리 구조 (vtable pointer 위치)

질문: 클래스 객체가 힙에 저장될 때 vtable pointer도 힙에 같이 저장되는가?
답변: 네, 맞습니다! ✅

힙에 저장되는 Dog 인스턴스

힙 메모리:
┌─────────────────────┐
│ vtable pointer │ ← 8 bytes (Dog's vtable 주소)
├─────────────────────┤
│ reference count │ ← 8 bytes (ARC)
├─────────────────────┤
│ stored property 1 │ ← 실제 데이터들
│ stored property 2 │
└─────────────────────┘
vtable pointer가 가리키는 곳 ↓
데이터 세그먼트 (Dog's vtable):
┌─────────────────────┐
│ sound() → 0x3000 │
│ eat() → 0x2000 │
└─────────────────────┘

3. "인라인"의 두 가지 완전히 다른 의미

주의: "인라인"이라는 단어가 두 가지 완전히 다른 문맥에서 사용됩니다!

A. Inline 저장 (메모리 개념)

의미: 연속된 메모리에 데이터를 직접 저장

// Existential Container
let drawable: any Drawable = Point()

Value Buffer에 데이터를
"연속된 메모리에 직접" 저장
→ Inline 저장 (메모리 점프 없음)

영향: 캐시 효율성, 메모리 성능

B. 함수 인라이닝 (컴파일러 최적화)

의미: 함수 호출을 제거하고 코드를 직접 삽입

// 원본:
func add(a: Int, b: Int) -> Int {
    return a + b
}
let result = add(a: 5, b: 3)

// 인라이닝 후:
let result = 5 + 3
// 함수 호출 없음!

영향: 함수 호출 오버헤드 제거

핵심: 이 두 개념은 완전히 별개입니다!
• Inline 저장 = 메모리 배치 방식
• 함수 인라이닝 = 컴파일러 최적화 기법

4. vtable이 데이터 세그먼트에 있는 이유

질문: vtable이 코드 세그먼트가 아닌 데이터 세그먼트에 있는 이유?
답변: vtable은 "함수 주소 배열"이므로 데이터입니다!

메모리 세그먼트 구조

Code Segment (코드 세그먼트):
실행 가능한 기계어 코드만 저장

0x3000: push rbp ← Dog.sound()의 실제 기계어
0x3001: mov rbp, rsp
0x3010: call print
0x3020: ret
Data Segment (데이터 세그먼트):
전역 변수, 상수, vtable 저장

Dog's vtable:
[0x3000, 0x2000] ← 주소 값들 (8 bytes 포인터들)

vtable의 본질:

// vtable = 함수 주소들을 담은 배열 (개념적)
Dog's vtable = [
    sound: 0x3000,  // ← Dog.sound() 함수가 있는 주소
    eat:   0x2000   // ← Animal.eat() 함수가 있는 주소
]

// vtable 자체는 "데이터" (함수 코드가 아님)
비유:
코드 세그먼트 = 사무실 (실제 일하는 공간)
데이터 세그먼트 = 안내판 (사무실 호수들)
힙 = 사물함 (안내판 위치 메모지)

5. Struct vs Class 메모리 구조 차이

Struct (값 타입)

struct Dog {
    var name: String
    func bark() { }
}

let dog = Dog(name: "Max")

메모리 구조:

스택:
┌─────────────────────┐
│ name (String)       │ ← 16 bytes
└─────────────────────┘

vtable pointer 없음!
데이터만 있음!
  • vtable이 애초에 없음
  • 메서드 주소를 가지지 않음
  • 컴파일러가 코드에 주소 하드코딩

Class (참조 타입)

class Dog {
    var name: String
    func bark() { }
}

let dog = Dog(name: "Max")

메모리 구조:

힙:
┌─────────────────────┐
│ vtable pointer      │ ← 8 bytes
│ reference count     │ ← 8 bytes
│ name (String)       │ ← 16 bytes
└─────────────────────┘

vtable pointer 있음!
  • vtable pointer를 가짐
  • 런타임에 vtable 조회
  • Dynamic Dispatch

6. "포함하는 객체에 직접 저장"의 의미

질문: "값이 스택 또는 포함하는 객체에 직접 저장" 이게 무슨 뜻?
답변: Struct는 별도 할당 없이 포함하는 곳에 직접 저장됩니다.

예시:

struct Point {
    var x: Double  // 8 bytes
    var y: Double  // 8 bytes
}

class ViewModel {
    var point: Point  // ← Point가 "포함됨"
}

let vm = ViewModel()

메모리 구조

힙 (ViewModel 인스턴스):
┌─────────────────────┐
│ vtable pointer │
│ reference count │
│ point.x (8 bytes) │ ← Point가 직접 여기!
│ point.y (8 bytes) │
└─────────────────────┘

Point는 별도로 할당되지 않음!
ViewModel 안에 직접 포함되어 저장됨!

vs Class 프로퍼티:

class Dog {
    var name: String
}

class ViewModel {
    var dog: Dog  // ← Dog는 별도 힙 할당
}

메모리 구조

힙 (ViewModel):
┌─────────────────────┐
│ vtable pointer │
│ dog pointer │ ──→ 별도 Dog 인스턴스
└─────────────────────┘
힙 (Dog, 다른 주소):
┌─────────────────────┐
│ vtable pointer │
│ name │
└─────────────────────┘

차이점:

7. Struct 메서드 호출 메커니즘

질문: Struct에 있는 메서드는 구조체에서 코드 세그먼트의 주소를 가지고 있나?
답변: 아니요! Struct 인스턴스는 메서드 주소를 가지지 않습니다.

Struct 인스턴스 = 데이터만:

struct Dog {
    var name: String
    func bark() { print("멍멍") }
}

let dog = Dog(name: "Max")

메모리 구조

스택 (dog 인스턴스):
┌─────────────────────┐
│ name (String) │ ← 16 bytes
└─────────────────────┘

메서드 주소 없음!
데이터만 있음!

메서드 호출 과정:

dog.bark()

// 컴파일러가 생성하는 코드 (개념적):
Dog.bark(&dog)  // ← 전역 함수처럼 호출, dog를 인자로 전달

// 어셈블리 레벨:
lea rdi, [rbp-16]     // dog의 주소를 rdi 레지스터에
call 0x3000           // Dog.bark() 코드 주소로 직접 점프
핵심:
• Struct 메서드 = 실제로는 전역 함수처럼 동작
• 인스턴스는 메서드 주소를 가지지 않음
• 컴파일러가 코드에 주소를 직접 하드코딩
• → Static Dispatch

8. call 0x3000의 0x3000은 어디?

질문: call 0x3000에서 0x3000은 코드 영역인가?
답변: 네, 코드 세그먼트 주소입니다! ✅

전체 메모리 구조

Code Segment (코드 세그먼트):
실행 가능한 기계어 코드

0x3000: Dog.bark() 함수 코드 ← 여기!
  push rbp
  mov rbp, rsp
  call print
  ret
Data Segment (데이터 세그먼트):
Dog's vtable (Class인 경우만)
[0x3000, 0x2000]
Stack (스택):
dog 인스턴스 (Struct인 경우)
[name: "Max"]

호출 과정 비교:

방식 호출 과정
Static Dispatch (Struct) dog.bark()
↓ 컴파일
call 0x3000 ← 코드 세그먼트 주소 직접
↓ 실행
바로 코드 세그먼트 0x3000으로 점프 ✅
Dynamic Dispatch (Class) dog.bark()
↓ 런타임
1. 힙에서 vtable pointer 읽기
2. 데이터 세그먼트에서 vtable의 bark 슬롯 읽기
3. 코드 주소 얻음 (0x5000)
4. 코드 세그먼트 0x5000으로 점프

힙 → 데이터 → 코드 (3단계) ❌
정리:
• 0x3000 = 코드 세그먼트 주소 (실제 함수 코드)
call 0x3000 = 코드 세그먼트의 0x3000으로 점프
• Static Dispatch는 이 주소를 코드에 직접 하드코딩
• Dynamic Dispatch는 런타임에 vtable에서 주소를 읽어옴

📝 Existential Container 한 줄 정리

Existential Containerany Protocol 타입을 다룰 때 사용되는 메모리 구조로서 고정 40 bytes를 가진다.

구성:
24 bytes = Value Buffer
8 bytes = VWT pointer
8 bytes = PWT pointer

Value Buffer 동작:
• 값이 24 bytes 이하면 → Value Buffer에 인라인 저장
• 값이 24 bytes 초과면 → 에 저장하고 포인터만 저장

VWT (Value Witness Table):
이 타입의 복사, 삭제 등 메모리 관리 관련 메타데이터의 포인터

PWT (Protocol Witness Table):
이 타입이 해당 프로토콜의 메소드를 구현한 주소 목록의 포인터
(타입의 모든 메소드가 아니라, 프로토콜이 요구하는 메소드만 포함)

이 과정이 Boxing이다.

전체 메모리 구조

Existential Container (40 bytes)
┌─────────────────────────────────────┐
│  Value Buffer (24 bytes)            │  ← 값 저장 (≤24B: 인라인 / >24B: 힙 포인터)
├─────────────────────────────────────┤
│  VWT Pointer (8 bytes) ─────────────────→ Data Segment: VWT (copy, destroy, size...)
├─────────────────────────────────────┤
│  PWT Pointer (8 bytes) ─────────────────→ Data Segment: PWT (draw→0x1000, erase→0x1100)
└─────────────────────────────────────┘                              │
                                                                     ▼
                                                          Code Segment: 실제 함수 코드

📌 핵심 정리

1. Static Dispatch

2. Dynamic Dispatch

3. Witness Table

4. Existential Container

5. 성능 순서

빠름 → Static Dispatch > Witness Table (Generic) > vtable (Class) > Existential (any) → 느림

6. 실무 가이드

🔄 Generic과 Witness Table 심화 Q&A

Q. Existential Container 구조를 정리하면?

답: any 타입/프로토콜을 따르는 구현체를 담는 메모리 구조입니다.

구성 (총 40 bytes):
Value Buffer (24B): 실제 값 저장 (작으면 inline, 크면 힙 포인터)
VWT pointer (8B): 복사/삭제 등 메모리 관리 방법
PWT pointer (8B): 프로토콜 메서드들의 주소

┌─────────────────────────────────────┐
│  Value Buffer (24 bytes)            │
├─────────────────────────────────────┤
│  VWT Pointer (8 bytes)              │
├─────────────────────────────────────┤
│  PWT Pointer (8 bytes)              │
└─────────────────────────────────────┘

Q. VWT는 타입마다 1개, PWT는 (타입, 프로토콜) 조합마다 1개라는 게 무슨 말?

답: 테이블의 개수가 다르게 생성됩니다.

VWT - 타입마다 1개:
"이 타입을 어떻게 복사/삭제하나"만 알면 됨
→ Circle용 VWT 1개, Square용 VWT 1개

PWT - (타입, 프로토콜) 조합마다 1개:
"이 타입이 이 프로토콜 메서드를 어디서 구현했나"
→ Circle+Drawable PWT, Circle+Printable PWT, Square+Drawable PWT

// Circle이 Drawable, Printable 채택 시
Circle용 VWT: 1개 (복사/삭제 방법)
Circle+Drawable PWT: 1개 (draw 주소)
Circle+Printable PWT: 1개 (print 주소)

Q. Circle이 두 개의 프로토콜 conform하면 2개의 PWT가 있어?

답: 네, 맞습니다! ✅

struct Circle: Drawable, Printable { ... }

// PWT 2개 생성됨:
┌─────────────────────────────┐
│ Circle + Drawable PWT       │  ← draw() 주소
└─────────────────────────────┘

┌─────────────────────────────┐
│ Circle + Printable PWT      │  ← print() 주소
└─────────────────────────────┘
어떤 PWT가 사용되는지는 변수 타입에 따라 결정:
let a: any Drawable = Circle() → Circle+Drawable PWT
let b: any Printable = Circle() → Circle+Printable PWT

Q. Generic에서 Witness Table 동작 원리는?

답: Generic 함수 호출 시 PWT가 파라미터로 함께 전달됩니다.

func render<T: Drawable>(_ item: T) {
    item.draw()
}

// 컴파일러가 내부적으로 변환:
func render(_ item: T, pwt: Drawable_PWT, vwt: T_VWT) {
    pwt.draw(item)  // PWT를 통해 draw 호출
}

// 호출 시:
render(Circle())  // Circle + Drawable PWT 전달
render(Square())  // Square + Drawable PWT 전달
Existential Container와 차이:
• any: PWT가 컨테이너 안에 저장
• Generic: PWT가 함수 파라미터로 전달

Q. Generic을 사용하면 PWT를 함수에 넘기는데, any보다 성능이 좋아?

답: 네, 둘 다 PWT를 사용하지만 Generic이 더 빠릅니다!

any Drawable:
• Value Buffer에 값 복사 (boxing)
• 큰 값 (>24B)은 힙 할당 필요
• 40바이트 컨테이너 전체를 넘김

<T: Drawable> (Generic):
• 값 그대로 전달 (boxing 없음)
• 힙 할당 없음
• Specialization 가능 (인라인)

핵심: PWT 사용 여부가 아니라 Existential Container의 boxing 오버헤드가 성능 차이의 원인!

Q. 인라인이란 런타임에 구현체가 바로 사용되는 걸 의미해?

답: 아니요! 인라인은 컴파일 타임 최적화입니다.

인라인 = 함수 호출을 함수 본문으로 대체

// 원본 코드
func square(_ x: Int) -> Int {
    return x * x
}
let result = square(5)

// 컴파일러가 인라인 적용 후
let result = 5 * 5  // 함수 호출 자체가 사라짐
정리:
• 인라인 = 컴파일 타임에 함수 본문을 호출 위치에 복사
• 런타임에 함수 호출 자체가 없어짐
• "런타임에 구현체가 바로 사용"이 아니라 "컴파일 타임에 함수 호출이 제거됨"

Q. PWT의 주소를 파라미터에 함께 보내면 실제 PWT 데이터는 코드 영역에 있어?

답: 아니요! PWT는 데이터 영역(Data Segment)에 있습니다.

┌─────────────────────────────────┐
│         Code Segment            │  ← 실제 함수 코드
│  Circle.draw: 0x1000            │
├─────────────────────────────────┤
│         Data Segment            │  ← PWT, VWT 테이블들
│  Circle+Drawable PWT: 0x5000    │
│    └─ draw → 0x1000 (포인터)    │
├─────────────────────────────────┤
│         Stack / Heap            │  ← 실제 값들
└─────────────────────────────────┘
호출 흐름:
1. render 호출 시 PWT 주소 (0x5000) 전달
2. PWT(0x5000)에서 draw 항목 조회 → 0x1000
3. 0x1000 (Code Segment)으로 점프해서 실행

Q. VTable이랑 PWT가 비슷해 보이는데?

답: 맞습니다! 구조가 거의 동일합니다.

공통점:
• 둘 다 Data Segment에 저장
• 둘 다 Code Segment의 함수 주소를 가리킴
• 둘 다 함수 포인터 테이블

차이점:

VTablePWT
포인터 위치인스턴스 내부 (힙)파라미터 or Container
용도클래스 상속 다형성프로토콜 다형성
테이블 개수클래스당 1개(타입, 프로토콜)당 1개

핵심: VTable = 클래스용, PWT = 프로토콜용. 동작 방식은 같음!

Q. Generic은 항상 Specialization이 가능해?

답: 아니요! 함수 본문을 볼 수 있어야 합니다.

Specialization = 함수 복제
컴파일러가 Generic 함수를 타입별로 복사하는 것

// 원본
func render<T: Drawable>(_ item: T) {
    item.draw()
}

// Specialization 후 (컴파일러가 복제)
func render_Circle(_ item: Circle) {
    item.draw()  // ← 본문 복사됨
}
본문을 못 보면 → 복사할 게 없음 → Specialization 불가

Q. 다른 모듈에서 Generic 함수 호출하면 어떻게 돼?

답: 기본적으로 Specialization이 안 됩니다.

// 모듈 B
func print<T: Drawable>(_ item: T) {
    item.draw()
}

// 모듈 A에서 호출
import ModuleB
print(Circle())  // ❌ Specialization 불가
                 // → PWT 전달 방식으로 컴파일
모듈 A 컴파일러 입장:
"T = Circle인 건 알겠어"
"근데 print 함수 안에 뭐가 있는지 몰라"
"복제할 코드가 없으니 그냥 PWT 넘길게"

Q. 모듈 B도 Circle 함수 내용 모르는 거 아냐?

답: 두 가지를 구분해야 합니다!

1. Specialization = "T가 뭔지 아는 것"
T = Circle만 알면 → Circle.draw() 주소로 직접 호출 가능

2. 인라인 = "함수 본문을 복사하는 것"
Circle.draw() 본문까지 복사하려면 그 코드가 보여야 함

// 모듈 B에서 print(Circle()) 호출 시:

Specialization 후 생성되는 코드:
func print_Circle(_ item: Circle) {
    item.draw()  // Circle.draw() "직접 호출"
                 // PWT 조회 없음 ✅
                 // draw() 본문 인라인 ❌ (다른 모듈)
}
결론: Circle 구현 몰라도 Specialization은 가능
(단, Circle.draw() 완전 인라인은 안 됨)

Q. @inlinable은 뭐야?

답: 다른 모듈에 함수 본문을 공개하는 키워드입니다.

// 모듈 B
@inlinable
public func print<T: Drawable>(_ item: T) {
    item.draw()
}

// 모듈 A
import ModuleB
print(Circle())  // ✅ Specialization 가능!
비유:
• 모듈 B = 비밀 레시피 보유 (함수 본문)
• 모듈 A = "Circle 재료로 만들어줘"
• 레시피 안 보여주면 → 모듈 B에게 부탁 (PWT 전달)
• @inlinable = 레시피 공개 → 모듈 A가 직접 복제 가능

Q. Specialization 조건 정리

Specialization이 적용되려면:

조건Specialization
같은 모듈 내에서 호출✅ 적용 가능
다른 모듈에서 호출❌ 기본 불가
@inlinable 표시된 함수✅ 다른 모듈에서도 가능

Specialization 필수 조건:

1. T가 뭔지 알아야 함 ✅ (호출부에서 Circle 전달)
2. 함수 본문을 봐야 함 ✅ (복제할 코드 필요)

→ 둘 다 충족해야 Specialization 가능

➕ 보강 Q&A (2026-06-27) — vtable 위치·가시성·캐시·Array existential

① vtable은 어디 있나?

읽기 전용 데이터 영역(클래스 메타데이터의 일부). vtable은 클래스당 하나, 컴파일/링크 시 정적 생성 → 인스턴스마다 복제 안 됨.
영역무엇
Code(text)실제 메서드 기계어
Data(읽기전용)클래스 메타데이터 + vtable(메서드 주소들)
Heap인스턴스(맨 앞 isa 포인터 + 프로퍼티)

인스턴스(힙)는 isa로 메타데이터를 가리키고, 메타데이터 안 vtable로 메서드를 찾는다.

② let animal: Animal = Dog() — Dog 고유 메서드 호출?

class Animal { func eat() {} }
class Dog: Animal { func bark() {} }   // bark는 Dog 고유

let animal: Animal = Dog()
animal.eat()    // ✅ Animal에 있음
animal.bark()   // ❌ 컴파일 에러 — Animal 타입엔 bark 없음
(animal as? Dog)?.bark()   // ✅ 다운캐스팅하면 됨
가시성 vs 디스패치 분리:
호출 가능 여부(가시성) = 선언 타입(Animal) — 컴파일 타임. bark 안 보임
실제 어느 구현 실행(디스패치) = 런타임 타입(Dog) — vtable. eat()을 Dog이 override했으면 Dog 버전 실행
비유: 변수 타입(Animal)=리모컨 버튼(eat만 있음). 실제 기기(Dog)가 bark 할 줄 알아도 리모컨에 버튼이 없어 못 시킴.

③ 캐시 미스 — existential 큰 값이 느린 이유

캐시 미스 = CPU가 데이터를 캐시에서 못 찾아 느린 RAM에서 가져오는 것(~100사이클). 박싱/언박싱과는 다른 층위(메모리 접근 비용).

박싱 → 간접(포인터) 접근 → 캐시 미스 유발. 언박싱은 "박스에서 값 꺼내기"(연산), 캐시 미스는 "그 접근 시 캐시에 없어 RAM 가는 것"(비용) — 별개.

④ Array를 existential container에 담으면?

let shapes: any Collection = [a, b, c]

Container(40B):
  Value Buffer(24B): [Array 헤더 = buffer 포인터 8B]  ← Array는 8B라 inline 직접! box 없음
  Type Metadata Ptr(8B) → Array 메타데이터(크기·VWT)
  PWT Ptr(8B)          → (Array, Collection) witness table
        │ Array buffer 포인터
        ▼
   힙: [a, b, c]  ← 요소들 (Array가 관리, existential과 무관)
결론: existential heap box는 담는 struct가 24B 초과일 때만 생긴다. Array는 8B(buffer 포인터)라 inline 저장 → box 없음(요소가 힙에 있는 건 Array 자체 특성). "any면 무조건 박싱"은 틀림 — 값 크기에 따라 다름. ([any P]는 별개 — 각 원소가 container)

📚 참고 자료

Witness Table & Protocol Dispatch

Existential Container & Boxing

Static vs Dynamic Dispatch

🔁 복습 Q&A (2026-07-07) — vtable은 클래스당 하나 · 가시성 vs 디스패치

Q1. vtable은 인스턴스마다인가, 클래스마다인가?

클래스마다 하나입니다. 인스턴스가 갖는 건 vtable 자체가 아니라, 클래스 metadata를 가리키는 isa 포인터 1개. vtable은 그 metadata 안에 있고, 같은 클래스의 인스턴스 100개는 전부 같은 vtable 하나를 공유합니다.

class Base { func foo() {}  func bar() {} }
class Derived: Base { override func foo() {} }   // foo만 오버라이드

Base 의 vtable   : [ foo → Base.foo    , bar → Base.bar ]   ← 클래스당 하나
Derived 의 vtable: [ foo → Derived.foo , bar → Base.bar ]   ← foo 슬롯만 교체

base1, base2  ─isa→ Base vtable      (Base 인스턴스는 전부 공유)
d1, d2, d3    ─isa→ Derived vtable   (Derived 인스턴스는 전부 공유)

Q2. 가시성(visibility) vs 디스패치(dispatch) — 두 축이 따로 논다

class Parent { func run() { print("Parent") } }
class Child: Parent {
    override func run() { print("Child") }
    func onlyChild() {}
}
let x: Parent = Child()      // 선언 타입 Parent, 실제 Child
x.run()                      // "Child"  ← dynamic dispatch (vtable 따라감)
x.onlyChild()                // ❌ 컴파일 에러 — 가시성 문제
무엇을 결정기준
가시성무엇을 부를 수 있나선언 타입(Parent) — 컴파일 타임
디스패치부른 것 중 어느 구현이 실행되나실제 인스턴스(Child) — 런타임 vtable

Q3. "inline"의 두 가지 완전히 다른 의미