Section 7 — Protocol Witness Table 동작 흐름
클래스의 vtable과 비슷하지만, 한 가지 큰 차이가 있다:
클래스의 vtable은 객체 안에 isa 포인터로 박혀 있고,
PWT는 (타입, 프로토콜) 쌍마다 별도로 만들어져 existential 또는 generic context에서 전달된다.
sequenceDiagram
participant Caller
participant Existential as any P container
participant PWT as P-witness-table-for-S
participant Concrete as S.method impl
Caller->>Existential: p.method()
Existential->>PWT: lookup index for "method"
PWT->>Concrete: call function pointer
Concrete-->>Caller: result
반면 generic 호출 `func f<T: P>(_ t: T)`은 컴파일러가
호출 사이트마다 PWT를 인자처럼 넘긴다. 그리고 최적화가 켜지면
specialization이 일어나 T가 고정된 별도 함수로 복제되고, 디스패치 자체가 사라진다.
이게 generic이 종종 zero-cost로 불리는 이유다.
Q&A — 학습 중 막혔던 질문 모음 (2026-06-04)
본문을 읽다가 막힌 단어/개념을 GPT와 Q&A로 풀어낸 기록.
같은 질문이 다시 나올 때 빠르게 다시 보기 위해 누적한다.
Q1. "다이아몬드 문제 회피"가 뭐야?
다중 상속에서 같은 조상의 멤버가 두 경로로 내려와 충돌하는 문제. 모양이 마름모(◇)라 다이아몬드 문제다.
클래스 다중 상속을 허용하는 C++은 virtual 상속으로 우회해야 하고,
Java/Kotlin/Swift class는 다중 상속 자체를 금지해 회피한다.
Swift 프로토콜은 "요구사항만 정의 + 기본 구현 충돌 시 컴파일 에러"로 채택 타입이 직접 해결하게 강제한다.
Q2. "제네릭과 결합하면 zero-cost 추상화가 가능하다"의 의미
추상화는 보통 간접 호출과 박싱 비용이 따라온다. 그런데 <T: P> generic 함수는
컴파일러가 specialization을 수행해 T별 전용 함수로 복제하므로 PWT 조회와 박싱이 사라진다.
손으로 직접 짠 구체 타입 코드와 동일한 어셈블리가 나온다 — 이게 zero-cost.
Q3. 박싱(Boxing)이 뭐야?
작은 값을 일정한 모양의 "상자" (existential container) 에 담아 넘기는 것.
타입마다 크기가 다른데 any P 슬롯은 고정 크기여야 해서 박스가 필요하다.
Swift는 buffer가 보통 3-word(24 byte). 그보다 작으면 inline, 크면 heap에 따로 두고 buffer엔 포인터만.
Q4. "레지스터/스택에 위치"가 뭔말?
CPU 메모리 계층. 레지스터(CPU 내부, 0 사이클) → L1/L2/L3 캐시 → 스택(함수 호출마다 자동 할당) → 힙(명시적 malloc).
앞으로 갈수록 빠름. let n = 42 같은 스택 변수는 거의 공짜, 힙은 malloc/free 비용 발생.
Q5. PWT 조회 후 "함수 호출"이 뭔말?
PWT는 함수 포인터 배열이다. 슬롯에서 함수 주소를 꺼내 그 주소로 call(점프)하는 것 = 함수 호출.
구체 타입은 컴파일 시점에 호출 주소가 어셈블리에 박혀 있어서 조회 없이 바로 call.
Q6. (오답노트) Swift는 모듈 단위로 컴파일된다
모듈 = 함께 컴파일되는 코드 단위 (SPM, Framework, Xcode target, main app). 모듈 빌드 시
.swiftmodule(시그니처)과 바이너리(.dylib/.o, 본문)가 나뉜다.
모듈 경계를 넘는 호출은 specialization, inlining, dead code elimination 같은 최적화가 막힐 수 있다.
Q7. 함수 시그니처가 뭐야?
함수의 "겉모습" — 이름 + 파라미터 타입 + 반환 타입. 본문은 제외.
.swiftmodule엔 시그니처만 들어 있고, 본문은 라이브러리 바이너리에 따로 컴파일되어 있다.
Q8. "라이브러리 바이너리에 이미 컴파일되어 있다"
라이브러리 빌드 시점에 generic 본문은 "T 모르는 일반화된 형태"의 기계어로 굳어 버린다.
App을 컴파일할 때 "T = Circle"로 다시 짜고 싶어도 본문 소스가 없으니 specialization 불가능.
@inlinable은 본문 소스를 .swiftmodule에 같이 박아 넣어 specialization을 가능하게 한다.
Q9. @inlinable은 함수에 붙이는 거 아니야?
맞다. 함수/메서드/연산자/computed property에 붙인다. 타입 자체에는 못 붙인다.
"Array, Dictionary가 @inlinable 처리되어 있다"는 말은 그 타입의 거의 모든 메서드 각각에 @inlinable이 붙어 있다는 뜻.
Q10. "박스 열기 + 포인터 조회 + jump" 자세히
any Shape의 s.area() 호출 단계:
- 박스(existential container) 열기 — buffer에서 실제 값 위치 확보
- PWT 포인터 로드 — container에서 PWT 주소를 레지스터로
- 함수 포인터 조회 — PWT[area 슬롯]에서 함수 주소를 가져옴
- 그 주소로
CALL — 점프해서 실행
구체 타입은 CALL Circle.area 한 줄로 끝. 핫 루프에서 차이 누적되면 체감된다.
Q11. "박싱이 generic 쪽이 덜 일어난다"
Specialization 실패한 경우의 얘기. Generic 함수는 ABI가 "값은 T 크기 그대로 + PWT를 별도 인자로 전달"이라
박싱 없이 PWT dispatch만 일어난다. any는 호출 전부터 박싱이 일어난다.
Q12. (오답노트 정정) any도 inline이면 zero-cost?
아니다. 부분적으로만 맞다. 작은 값(≤ 3 word)은 inline boxing이라 박싱 비용은 ≈ 0이 맞다.
하지만 dispatch 비용은 크기와 무관하게 항상 PWT 조회 + 간접 호출이 일어난다.
Zero-cost가 되려면 generic + specialization 조합이 필요하다.
Q13. 메모리 블록이 뭐야?
연속된 메모리 주소 범위. "방 한 칸"처럼 생각하면 된다.
inline = 같은 블록 안 / heap = 분리된 다른 블록을 포인터로 가리킴.
Q14. "Array 본체는 힙"이 뭔말?
Swift Array는 struct(값 타입)지만 내부에 힙 포인터 하나만 가진다. 진짜 원소들이 담긴 storage는 힙에.
그래서 "껍데기는 스택, 본체(저장소)는 힙"이다. String, Dictionary, Set도 같은 구조.
Q15. Generic + 모듈 경계에서 박싱 안 하는 이유
Generic 함수의 호출 규약(ABI)이 any와 다르다. any P는 호출자가 값을
existential container로 박싱해 넘기지만, <T: P>는 T의 실제 크기 그대로 값을 전달하고
PWT를 숨겨진 별도 인자로 함께 넘긴다. 박스라는 고정 모양이 필요 없어서 박싱이 일어나지 않는다.
Specialization 실패해도 PWT dispatch만 일어나고 박싱은 없다.
요약 한 줄
"any는 박싱 + PWT, <T: P>는 PWT만 (성공 시엔 그것도 사라짐).
구체 타입은 다 없음."