최근 C++, Rust, Go의 컴파일 과정을 살펴보고 있다.
처음에는 이름이 너무 많아 가장 막막한 부분이 다음과 같았다:
- GCC
- Clang
- LLVM
- AST
- IR
- SSA
- MIR
- GIMPLE
- RTL
- 링커
- 로더
각 용어를 따로 보면 어느 정도는 이해할 수 있다. 하지만 이들을 한 그림에 놓고 보면 쉽게 정리가 되지 않는다.
나중에 보니, 문제는 용어가 너무 많은 것이 아니라 실제 프로그램을 출발점으로 삼지 않았다는 데 있었다.
컴파일러가 단순히 ‘소스 코드를 어셈블리로 옮기는’ 과정을 수행하는 것은 아니다. 더 정확히 말하면, 프로그램의 추상화 수준을 계속 낮추는 과정이다.
각 중간 표현 계층은 특정 종류의 문제를 더 쉽게 처리하도록 마련된 것이다.
아주 작은 C++ 프로그램 하나를 통해 주류 컴파일 체인을 연결해 보자.
이 코드에는 몇 가지 의도적인 요소가 남아 있다.
작은 프로그램 하나만으로 주류 컴파일 흐름을 거의 모두 다뤄볼 수 있다.
1. 소스 코드는 컴파일러가 실제로 원하는 형태가 아니다
프로그래머가 보는 코드는 다음과 같다.
사람이라면 다음 코드와 대략 같다는 것을 쉽게 이해한다.
하지만 컴파일러는 처음부터 그렇게 해석할 수 없다.
소스 코드는 그저 문자 스트림일 뿐이다.
i n t 空格 x 空格 = 空格 I N C ...
컴파일러는 먼저 문자를 더 안정적인 구조로 처리해야 한다.
그래서 현대적인 컴파일 파이프라인은 소스 코드에서 바로 어셈블리로 넘어가지 않는다. 소스 코드는 사람이 읽기 좋지만, 기계가 분석하기에는 적합하지 않다.
2. 전처리: C/C++의 가장 초기 텍스트 처리 계층
C/C++의 첫 단계는 보통 전처리다.
clang++ -E main.cpp -o main.ii
전처리기는 주로 다음을 처리한다.
#include
#define
#ifdef
#pragma
이 프로그램에서 가장 잘 드러나는 부분은 다음과 같다.
전처리 후 핵심 코드는 다음과 같이 바뀐다.
여기서 중요한 관점은 다음과 같다.
전처리는 의미 분석이 아니다.
타입을 이해하지 않는다.
add가 함수인지도 모른다.
INC(add(1, 2))가 타당한지도 모른다.
그저 규칙에 따라 텍스트를 확장할 뿐이다.
이것이 C/C++ 매크로가 문제를 일으키기 쉬운 이유다. 매크로는 함수가 아니므로, 함수의 타입 검사 및 스코프 규칙을 따르지 않는다.
예를 들면 다음과 같다.
다음처럼 작성하면
다음처럼 확장된다.
다음이 아니다.
C++에서 만나는 이상한 문제 중에는 실제로 컴파일 단계에 들어간 것도 아닌 대부분, 전처리 단계에서 이미 문제를 만들어 둔 것뿐인 경우가 많다.
3. 어휘 분석: 문자를 토큰으로 자르기
전처리 후 컴파일러는 어휘 분석을 시작한다.
소스 코드 조각은 다음과 같다.
이 코드는 다음과 같이 나뉜다.
이 단계의 역할은 단순히 ‘단어 분리’뿐이다.
int가 키워드라는 것을 안다.
x가 식별자라는 것을 안다.
1이 숫자 리터럴이라는 것을 안다.
하지만 아직 다음은 모른다.
x 是局部变量
add 是函数
printf 是外部函数
1 能不能传给 add
이것들은 모두 어휘 분석이 담당하는 일이 아니다.
Clang으로 토큰을 확인할 수 있다.
clang++ -Xclang -dump-tokens -fsyntax-only main.cpp
컴파일 오류에서 잘못된 문자, 닫히지 않은 문자열, 잘못된 숫자 리터럴 형식 등이 나타나면 보통 이 계층 부근에서 처리가 멈춘다.
4. 구문 분석: 토큰에서 AST로
토큰은 여전히 일직선적이다.
컴파일러는 프로그램의 구조를 알아야 한다.
예를 들면 다음과 같다.
구문 분석 후에는 다음과 같은 트리로 바뀌며, 단순화하면 대략 이렇다.
이것이 AST, 추상 구문 트리다.
AST의 목적이 수준을 높이려는 것이 아니라, 소스 코드의 구조를 안정적으로 표현하는 데 있다.
소스 코드는 텍스트다.
AST는 구조다.
이 단계가 지나면 컴파일러는 비로소 다음을 알게 된다.
这是一个变量声明
变量名是 x
初始化表达式是一个加法
加法左边是函数调用
加法右边是整数 1
하지만 AST만으로 프로그램이 올바른지는 증명할 수 없다.
예를 들어 다음과 같다.
foo가 아예 존재하지 않더라도 먼저 AST를 만들 수 있다.
문법상에는 문제가 없다.
의미层面에서야 문제가 된다.
Clang으로 AST를 확인할 수 있다.
clang++ -Xclang -ast-dump -fsyntax-only main.cpp
실제 C++ AST는 매우 깁니다. 템플릿, 오버로드, 암시적 변환, 생성자, 소멸자가 모두 들어 있기 때문이다.
이것이 C++ 프런트엔드가 복잡한 이유다. 토큰을 분리하기 어렵거나 구문 트리를 그리기 어려운 탓이 아니라, 의미가 지나치게 복잡하기 때문이다.
5. 의미 분석: 컴파일러가 비로소 프로그램을 이해하기 시작하는 단계
의미 분석은 이런 문제들을 처리한다.
add 是否存在
add 参数数量是否正确
add 参数类型是否匹配
printf 是否声明过
int x 的初始化是否合法
函数返回值是否符合声明
이 코드에서는 다음과 같다.
전처리 후에는 다음과 같다.
의미 분석은 다음을 확인한다.
add 的类型是 int(int, int)
add(1, 2) 返回 int
返回值可以和 1 做加法
结果可以赋给 int x
다음처럼 작성하면
문법은 여전히 성립한다.
AST도 만들 수 있다.
하지만 의미 분석 단계에서 인자의 타입이 일치하지 않는다는 오류가 발생한다.
개발자들이 흔히 말하는 ‘컴파일 오류’는 다음과 같이 더 나눌 수 있다.
语法错误:代码结构不合法
语义错误:结构合法,但意思不合法
链接错误:单个文件能编译,但最终符号找不到
이 세 종류의 오류는 완전히 서로 다른 단계에서 발생한다.
Rust의 Borrow Checker도 이 큰 범주에 속한다.
예를 들어 다음과 같다.
let r;
{
let x = 1;
r = &x;
}
println!("{}", r);
이것은 문법 오류가 아니다.
구조는 완전히 유효하다.
진짜 문제는 의미 수준에 있다. x는 이미 스코프를 벗어났으므로 r은 이를 참조할 수 없다.
따라서 Rust의 소유권, 대여, 수명은 문법 기능이 아니라 더 강한 의미 분석으로 이해해야 한다.
이것도 Rust 컴파일러가 Go 컴파일러보다 훨씬 복잡한 이유 중 하나다.
6. AST는 최적화에 적합하지 않으므로 IR이 필요하다
많은 사람이 컴파일 흐름을 처음 배울 때 다음과 같이 생각한다.
源码
↓
AST
↓
汇编
이 모델은 너무 대략적이다.
현대 컴파일러는 최적화를 위해 AST에 오래 머물지 않는다.
이유는 단순하다.
AST는 소스 코드에 너무 가깝다.
예를 들어 다음과 같다.
AST는 소스 코드의 많은 구조를 그대로 유지한다.
하지만 최적화기가 실제로 관심 있어 하는 것은 다음과 같다.
这个值从哪里来
这个值被谁使用
这个计算有没有副作用
这个分支是否可达
这个变量能不能放进寄存器
这个函数能不能内联
AST는 이런 질문에 잘 맞지 않는다.
그래서 컴파일러는 AST를 IR로 낮춘다.
IR은 Intermediate Representation, 즉 중간 표현이다.
예를 들어 이 코드는
다음과 같은 3주소 코드 형태로 낮아진다고 이해할 수 있다.
t1 = call add, 1, 2
t2 = t1 + 1
x = t2
call printf, "%d\n", x
return 0
이 형태가 AST보다 최적화에 적합하다.
표현식 트리가 단순한 명령어들로 분해된다.
데이터 의존 관계도 더 명확해진다.
LLVM IR은 대략 다음처럼 보인다. 단순화하면 다음과 같다.
여기서 눈여겨볼 점이 몇 가지 있다.
add는 C++에서 다음과 같이 컴파일될 수 있다.
_Z3addii
이것이 C++의 name mangling이다.
C++는 함수 오버로드를 지원하므로, 링커가 add라는 이름 하나만으로 식별할 수는 없기 때문이다.
예를 들어 다음과 같다.
두 함수 모두 소스 코드에서는 add라는 이름이지만, 링크할 때는 구분해야 한다.
그리고 printf는 다음과 같이 선언되어 있기 때문에
C++ 이름 변경이 적용되지 않아 그대로 유지된다.
printf
이것이 extern "C"의 실제 의미 중 하나다. 심볼 이름을 제어해 C ABI와 연동할 수 있게 하는 것이다.
7. 최적화: 컴파일러가 프로그램 재작성을 시작할 때
최적화를 하지 않으면 컴파일러는 프로그램 구조를 비교적 충실하게 유지한다.
최적화를 켜면 다음과 같다.
clang++ -O2 -S -emit-llvm main.cpp -o main.ll
이 코드는 다음과 같이 최적화될 가능성이 높다.
define i32 @main() {
entry:
call i32 (ptr, ...) @printf(ptr @.str, i32 4)
ret i32 0
}
add(1, 2)는 사라진다.
INC가 확장된 + 1도 사라진다.
x도 사라진다.
마지막에는 다음만 남는다.
printf("%d\n", 4)
컴파일러가 코드를 엉뚱하게 바꾼 것이 아니다.
컴파일러가 이러한 재작성이 관찰 가능한 동작을 바꾸지 않는다는 사실을 증명했기 때문이다.
여기서 몇 가지 전형적인 최적화가 일어난다.
函数内联:add(1, 2) 被展开
常量折叠:1 + 2 + 1 变成 4
死代码删除:中间变量 x 不再需要
Java JIT, Go SSA, Rust LLVM 최적화를 보면 이러한 최적화 이름이 자주 반복되는 것을 알 수 있다.
이유는 단순하다. 프로그램 최적화라는 기본 문제는 서로相通하기 때문이다.
8. SSA: 현대 최적화기는 왜 ‘변수를 한 번만 대입’하는 형태를 좋아하는가
SSA는 현대 컴파일러 최적화를 이해하는 핵심이다.
SSA는 Static Single Assignment의 약자다.
정적 프로그램에서 각 변수가 한 번만 대입된다는 뜻이다.
다음 예를 보자.
일반적인 코드에서는 x에 두 번 대입한다.
제어 흐름이 복잡해지면 최적화기는 특정 위치의 x가 어디에서 왔는지 판단하기 어렵다.
SSA는 이를 다음과 같은 형태로 바꾼다.
x1 = 1
if cond goto then else merge
then:
x2 = 2
goto merge
merge:
x3 = phi(x1, x2)
return x3
여기의
phi(x1, x2)
는 다음을 뜻한다.
如果从未进入 if 的路径来,x3 = x1
如果从 then 分支来,x3 = x2
SSA의 가치는 ‘변수가 변한다’는 사실을 명시적인 데이터 흐름 관계로 바꾼다는 데 있다.
이것은 최적화에 매우 중요하다.
예를 들어 다음과 같다.
常量传播
死代码删除
公共子表达式消除
循环不变代码外提
值编号
逃逸分析
이 모든 최적화가 SSA의 혜택을 받는다.
LLVM IR은 SSA 형식이다.
Go 컴파일러 내부에서도 SSA를 사용한다.
HotSpot C2도 SSA에 가까운 Sea of Nodes IR을 사용한다.
많은 데이터베이스 최적화기는 SSA라고 부르지 않지만, 비슷한 데이터 의존 관계를 유지한다.
특정 컴파일러의 작은 팁이 아니라, 현대 최적화 시스템의 기본적인 방법이다.
9. 백엔드: 플랫폼 독립 IR에서 머신 종속 IR로
LLVM IR도 여전히 플랫폼 독립적이다.
예를 들어 다음과 같다.
%sum = add i32 %a, %b
이 문장에서는 다음이 지정되어 있지 않다.
x86 用哪条指令
ARM 用哪条指令
结果放哪个寄存器
参数从哪里来
调用约定是什么
이것들은 모두 백엔드의 문제다.
백엔드는 다음을 처리한다.
指令选择
寄存器分配
指令调度
调用约定
栈帧布局
目标平台 ABI
같은 덧셈이라도 x86-64에서는 다음과 같을 수 있다.
mov eax, edi
add eax, esi
ret
ARM64에서는 다음과 같을 수 있다.
add w0, w0, w1
ret
IR은 같지만 대상 머신이 다르면 최종 명령어도 달라진다.
이것이 LLVM의 가치다.
언어 구현자는 LLVM IR만 생성하면 LLVM의 백엔드 역량을 재사용할 수 있다.
그렇지 않으면 새로운 언어마다 다음을 직접 지원해야 한다.
x86-64
ARM64
RISC-V
Windows ABI
Linux ABI
macOS ABI
寄存器分配
指令选择
调试信息
异常处理
이건 거의 불가능하다.
10. 오브젝트 파일: 머신 코드도 아직 최종 프로그램은 아니다
백엔드가 어셈블리를 생성하면 어셈블러가 오브젝트 파일을 만든다.
clang++ -c main.cpp -o main.o
main.o에는 머신 코드가 들어 있지만, 아직 완전한 프로그램은 아니다.
다음 명령으로 심볼을 볼 수 있다.
nm main.o
다음과 같은 내용을 볼 수 있다.
0000000000000000 T _Z3addii
0000000000000010 T main
U printf
대략 의미는 다음과 같다.
_Z3addii 已定义
main 已定义
printf 未定义
U printf는 오류가 아니다.
현재 오브젝트 파일에 printf의 구현이 없다는 뜻이며, 링크 단계에서 다른 곳을 찾아야 한다.
이것이 많은 사람이 처음 undefined reference를 만났을 때 오해하기 쉬운 부분이다.
문법 오류가 아니다.
타입 오류도 아니다.
링크 단계의 심볼 해석에 실패한 것이다.
11. 링킹: 흩어진 바이너리를 하나의 프로그램으로 합치기
링커가 하는 일을 대략 다음과 같이 이해할 수 있다.
main.o
+
libc
+
启动代码
+
运行时库
↓
可执行文件
다음 문제를 해결해야 한다.
符号解析
地址分配
重定位
静态库选择
动态库记录
入口点设置
C++를 작성한다면 다음도 관련된다.
name mangling
ODR
模板实例化
静态初始化
异常表
RTTI
虚表
예를 들어 다음과 같다.
undefined reference to `foo'
링커가 foo의 구현을 찾지 못했다는 뜻이다.
multiple definition of `foo'
여러 오브젝트 파일이 모두 같은 강한 심볼을 제공했다는 뜻이다.
undefined reference to `_Z3addii'
C++ 심볼 이름이 맞지 않는다는 뜻이며, 선언과 정의가 일치하지 않거나 C/C++ ABI가 섞여 문제가 생겼을 수 있다.
링커는 컴파일러 프런트엔드와는 별개인 또 다른 큰 시스템이다.
많은 대규모 C++ 프로젝트의 빌드 문제는 사실 컴파일러가 아니라 링커 문제다.
12. 로딩: 프로그램 실행 전, 운영체제가 이어받는 단계
다음 명령을 실행하면
./a.out
CPU가 곧바로 main부터 실행하는 것 또한 아니다.
Linux에서는 대략 다음 과정을 거친다.
内核读取 ELF
映射程序段到虚拟内存
加载动态链接器
加载共享库
处理重定位
初始化运行时
调用 main
프로그램의 메모리 레이아웃은 대략 다음과 같다.
.text 代码段
.rodata 只读数据
.data 已初始化全局变量
.bss 未初始化全局变量
heap 堆
stack 栈
따라서 엄밀히 말하면 전체 흐름은 다음이 아니다.
源码 → 可执行文件
다음과 같다.
源码 → 目标文件 → 可执行文件 → 进程
컴파일러는 ‘프로그램을 어떻게 생성할지’를 해결한다.
링커는 ‘프로그램을 어떻게 합칠지’를 해결한다.
로더는 ‘프로그램을 어떻게 프로세스로 만들지’를 해결한다.
13. Clang, LLVM, GCC는 한 그림에 어떻게 배치해야 할까
이제 이 이름들을 다시 보면 훨씬 명확해진다.
Clang + LLVM의 흐름은 다음과 같다.
C++ Source
↓
Clang Frontend
↓
Clang AST
↓
LLVM IR
↓
LLVM Optimizer
↓
LLVM Backend
↓
Object File
↓
Linker
↓
Executable
GCC는 다른 흐름이다.
두 흐름이 해결하는 문제는 같은 종류지만, 내부 표현은 다르다.
Clang은 LLVM의 별칭이 아니다.
LLVM도 Clang의 별칭이 아니다.
더 정확히 말하면 다음과 같다.
Clang 是 C/C++/Objective-C 前端。
LLVM 是中端和后端基础设施。
GCC 是另一套完整编译器系统。
다음과 같이 기억하면 된다.
Clang 负责读懂 C++。
LLVM 负责优化和生成机器码。
GCC 自己既有前端,也有中端和后端。
14. Rust는 왜 HIR과 MIR을 가지며, 왜 LLVM을 사용하는가
Rust의 흐름은 대략 다음과 같다.
Rust Source
↓
AST
↓
HIR
↓
THIR
↓
MIR
↓
Borrow Check
↓
LLVM IR
↓
LLVM Backend
↓
Machine Code
Rust의 이 계층들은 복잡해 보이려고 만든 것이 아니다.
각 계층에는 명확한 역할이 있다.
AST는 소스 코드에 가깝다.
HIR은 일부 문법 설탕과 표면 구조를 낮춰 프로그램 형태를 더 안정적으로 만든다.
THIR은 타입 검사 이후의 표현식 분석에 더 적합하다.
MIR은 Rust에서 아주 중요한 계층으로, 제어 흐름, move, borrow, drop, 수명 등의 의미를 표현하기에 적합하다.
LLVM IR은 범용 최적화와 머신 코드 생성 단계로 넘어가는 역할을 맡는다.
여기서 다음 구분을 주의해야 한다.
MIR 是 Rust 语义层的 IR。
LLVM IR 是机器代码生成层的 IR。
Borrow Checker는 LLVM IR에 직접 의존할 수 없다.
LLVM IR은 이미 너무 저수준이어서 Rust의 ownership, borrow, drop 의미는 그 계층에서 주된 분석 대상으로 삼기에 적합하지 않다.
AST에 직접 의존할 수도 없다.
AST는 문법 표면에 너무 가깝고 문법 설탕이 많아, 엄격한 데이터 흐름 및 제어 흐름 분석에 적합하지 않기 때문이다.
그래서 Rust에는 MIR이 필요하다.
이것이 중간 표현의 진정한 가치다. 추상화 계층을 하나 더 늘리는 것이 아니라, 특정 분석에 적합한 프로그램 형태를 제공한다.
15. Go는 왜 훨씬 단순해 보이는가
Go의 컴파일 흐름은 대략 다음과 같다.
Go Source
↓
Parser
↓
AST
↓
Type Check
↓
SSA
↓
Go Backend
↓
Machine Code
Go는 기본적으로 LLVM을 거치지 않는다.
자체 SSA와 백엔드를 가지고 있다.
이는 Go의 설계 목표와 관련이 있다.
Go 언어는 의도적으로 단순하게 유지한다.
没有模板元编程
没有复杂宏系统
没有 Rust 那种 ownership 检查
没有 C++ 那种重载和隐式规则
따라서 Go 컴파일러는 더 직접적인 방식으로 동작할 수 있다.
이것이 Go 컴파일 속도가 빠른 중요한 이유 중 하나다.
물론 ‘단순하다’는 Go 컴파일러에 기술적 난제가 없다는 뜻은 아니다.
Go의 탈출 분석, 인라인, SSA 최적화, 스택 성장, GC 관련 메타데이터 생성에는 상당한 엔지니어링 디테일이 있다.
다만 C++나 Rust와 비교하면 Go 프런트엔드의 의미 복잡도는 확실히 훨씬 낮다.
16. 현대 컴파일러는 왜 새로운 IR을 계속 만들어내는가
여기까지 왔다면 처음 질문에 대체로 답할 수 있다.
왜 GCC에는 GIMPLE과 RTL이 있는가?
왜 LLVM에는 LLVM IR, SelectionDAG, Machine IR이 있는가?
왜 Rust에는 HIR, THIR, MIR이 있는가?
왜 Go에는 SSA가 있는가?
모든 단계에 잘 맞는 단 하나의 표현이 없기 때문이다.
AST는 소스 코드 구조를 표현하기에 적합하다.
HIR은 표면 문법을 제거하기에 적합하다.
MIR은 언어 의미와 제어 흐름을 표현하기에 적합하다.
LLVM IR은 플랫폼 독립 최적화에 적합하다.
Machine IR은 레지스터 할당과 명령어 스케줄링에 적합하다.
RTL은 GCC 백엔드가 머신에 가까운 연산을 기술하기에 적합하다.
같은 프로그램도 단계에 따라 다르게 보인다.
사람에게는 비즈니스 로직이다.
프런트엔드에는 구문 트리다.
타입 시스템에는 제약 조건의 집합이다.
최적화기에는 데이터 흐름 그래프다.
백엔드에는 명령어 선택과 레지스터 할당 문제다.
링커에는 심볼과 재배치 기록이다.
로더에는 ELF 섹션과 동적 라이브러리 의존성이 있다.
컴파일러의 복잡성은 바로 이러한 관점 사이의 전환에서 나온다.
17. 실제로 기억해야 할 주류 컴파일 흐름
세부 사항에 빠지지 않고 주류 경로만 남긴다면 다음 흐름으로 기억할 수 있다.
여러 주류 컴파일러에 대응시키면 다음과 같다.
AST, IR, SSA를 각각 따로 외우는 것보다 이 그림이 더 유용하다.
이 그림은 같은 산업용 파이프라인에서 각 시스템이 어느 위치에 있는지 알려 준다.
18. 이 지식을 배우면 엔지니어링에 어떻게 도움이 되는가
이것은 순수한 이론이 아니다.
매크로 문제가 생기면 전처리 결과를 확인한다는 것을 안다.
clang++ -E main.cpp
구문 및 의미 문제가 생기면 프런트엔드에서 발생했다는 것을 안다.
clang++ -Xclang -ast-dump -fsyntax-only main.cpp
최적화 전후의 차이를 보고 싶으면 LLVM IR을 확인한다는 것을 안다.
clang++ -O0 -S -emit-llvm main.cpp -o main_O0.ll
clang++ -O2 -S -emit-llvm main.cpp -o main_O2.ll
심볼을 찾지 못하면 오브젝트 파일을 확인한다는 것을 안다.
nm main.o
ELF 구조를 보고 싶다면 다음과 같이 한다.
readelf -a a.out
어셈블리를 보고 싶다면 다음과 같이 한다.
objdump -d a.out
이 도구들은 기술적 자랑을 위한 것이 아니다.
각각 컴파일 흐름의 서로 다른 단계에 대응한다.
어느 단계에서 문제가 생겼는지 특정할 수 있다면, 이미 절반은 해결한 셈이다.
19. 컴파일러의 진정한 가치는 어디에 있는가
컴파일러의 가장 큰 가치는 몇 가지 용어가 아니라, 복잡한 시스템을 바라보는 방식에 있다.
복잡한 입력은 바로 실행되지 않는다.
먼저 어떤 중간 표현으로 바뀐다.
그런 다음 분석되고, 제약 조건이 적용되며, 최적화되고, 추상화 수준이 낮아진 뒤에야 실행 계층으로 들어간다.
이 패턴은 컴파일러에만 있는 것이 아니다.
데이터베이스는 SQL을 논리 계획과 물리 계획으로 변환한다.
JVM은 바이트코드를 내부 IR로 변환한 뒤 JIT 최적화를 맡긴다.
React는 UI 업데이트를 Fiber 구조로 조직한다.
Kubernetes Scheduler는 스케줄링 요청을 리소스 제약과 점수 모델로 변환한다.
이 시스템들은 겉으로 전혀 달라 보이지만, 근본에 깔린 사고방식은 꽤 유사하다.
输入不是直接执行的。
先建立表示。
再基于表示做分析和优化。
最后执行。
컴파일러는 이 사고방식의 가장 고전적이고 완성도 높은 버전일 뿐이다.
C++, Rust, Go, GCC, LLVM으로 이어지는 이 흐름을 이해할 수 있다면, 다시 JVM, 데이터베이스, 프런트엔드 프레임워크, 스케줄링 시스템을 살펴보았을 때 많은 설계가 더 이상 고립된 명사처럼 보이지 않을 것이다.