왜 동적 언어는 느릴까요
파이썬, 루비, 자바스크립트 같은 동적 언어를 써본 분들은 다들 한 번쯤 "왜 이렇게 느리지?" 하고 답답했던 경험이 있을 거예요. 같은 알고리즘인데 C나 Rust로 짜면 100배 빠르고, 자바로 짜도 10배는 빠르거든요. 이게 왜 그러냐면, 동적 언어는 변수의 타입을 미리 알 수 없기 때문이에요. a + b 한 줄을 실행하려고 해도 "a가 정수인가? 문자열인가? 객체라면 __add__ 메서드는 있나?" 같은 걸 매번 런타임에 확인해야 하거든요.
새로 등장한 Zef 언어 의 구현 문서가 이 오래된 문제를 어떻게 풀었는지 아주 자세히 풀어놓아서 화제예요. 단순히 "우리 언어 빠릅니다"가 아니라, 인터프리터를 처음부터 빠르게 만드는 구체적인 기법들을 단계별로 설명하고 있어서 컴파일러나 언어를 만들어보고 싶은 분들에게 정말 좋은 교재가 됩니다.
핵심은 "바이트코드 디스패치" 비용 줄이기
인터프리터의 동작은 의외로 단순해요. 소스 코드를 바이트코드(bytecode) 라는 작은 명령어 묶음으로 바꿔놓고, 큰 while문 안에서 "지금 명령이 ADD면 더해라, LOAD면 값 읽어라" 하는 식으로 돌리는 거죠. 이걸 보통 switch-case 디스패치 라고 부르는데요, 문제는 매 명령마다 분기 예측이 실패해서 CPU가 멈칫거린다는 거예요.
Zef는 여기서 threaded code(쓰레디드 코드) 라는 기법을 씁니다. 각 바이트코드 핸들러의 끝에서 다음 명령으로 "바로 점프"하게 만드는 방식이에요. 그러면 CPU의 분기 예측기가 "ADD 다음엔 LOAD가 자주 오더라" 같은 패턴을 학습해서 훨씬 빠르게 처리할 수 있죠. 비유하자면, 매번 안내데스크에 가서 "다음 어디로 가요?" 묻는 대신, 각 방에서 다음 방을 직접 알려주는 표지판을 붙여놓는 거예요.
인라인 캐싱이라는 마법
동적 언어가 느린 또 다른 이유는 속성 접근(attribute access) 이에요. obj.name 한 줄도 사실은 "obj의 클래스를 찾고, 그 클래스의 메서드 테이블을 뒤지고, name이라는 키가 있는지 검색하고..." 하는 복잡한 과정을 거치거든요. 이걸 매번 하면 죽음이죠.
Zef는 V8(크롬의 자바스크립트 엔진)이 쓰는 인라인 캐시(Inline Cache, IC) 기법을 단순화해서 적용했어요. 한 번 obj.name을 실행하면, "이 obj가 이 클래스 모양일 때 name은 메모리 주소 +16에 있더라"라는 결과를 그 자리에 캐시해두는 거예요. 다음번에 같은 모양의 객체가 오면 검색 없이 바로 +16 주소를 읽으면 되죠. 이게 동적 언어 성능 향상의 가장 큰 비밀 중 하나예요.
컴퓨티드 고투(Computed Goto)와 register-based VM
Zef 문서에서 또 인상 깊은 건 register-based VM(레지스터 기반 가상머신) 을 채택했다는 점이에요. 파이썬은 stack-based(스택 기반)인데, 이건 만들기 쉽지만 "a = b + c"를 하려면 b를 스택에 넣고, c를 스택에 넣고, 더해서 빼고, a에 저장하는 식으로 명령이 많아져요. 반면 register-based는 "r1 = r2 + r3" 한 줄로 끝나죠. 루아(Lua)가 이걸로 유명한데, 같은 일을 하면서도 명령 수가 30~50% 줄어드는 효과가 있어요.
GCC의 확장 기능인 computed goto (&&label 문법)를 활용하면 앞서 말한 threaded code를 C 레벨에서 깔끔하게 구현할 수 있는데, Zef도 이 방식을 적극 활용한다고 합니다.
다른 언어 구현과 비교해보면
이 분야에서 가장 유명한 건 역시 V8과 LuaJIT이에요. V8은 Ignition이라는 인터프리터와 TurboFan이라는 JIT을 결합해서 자바스크립트를 거의 네이티브 속도로 돌리고, LuaJIT은 마이크 펄(Mike Pall)이 거의 혼자 만든 트레이싱 JIT으로 동적 언어 성능의 한계를 보여줬죠. 파이썬도 3.11부터 "specializing interpreter"를 도입하면서 비슷한 방향으로 진화 중이고요.
Zef가 흥미로운 건, JIT 같은 거대한 인프라 없이 순수 인터프리터만으로 어디까지 빨라질 수 있는지를 보여준다는 점이에요. JIT은 빠르지만 메모리도 많이 먹고 시작도 느리거든요. 임베디드나 서버리스처럼 가볍게 돌려야 하는 환경에서는 "빠른 인터프리터"가 오히려 더 매력적일 수 있어요.
한국 개발자에게 무엇을 줄까
첫째, 사이드 프로젝트로 언어 만들기 에 관심 있는 분들에게는 정말 좋은 레퍼런스예요. 보통 "Crafting Interpreters" 책으로 시작하지만, 그 책은 기초까지만 다루거든요. Zef 문서는 그 다음 단계, 그러니까 "기초 인터프리터를 어떻게 진짜로 빠르게 만드는가"를 다룹니다.
둘째, 파이썬/장고 같은 동적 언어로 서비스를 운영하는 분들 에게도 도움이 돼요. 왜 어떤 코드는 느리고 어떤 코드는 빠른지, 어떤 패턴이 인라인 캐시를 깨먹는지 같은 직관을 얻을 수 있거든요. 같은 클래스 모양을 유지하는 게 왜 중요한지도 이제 이해되실 거예요.
셋째, Bun, Deno, Roc, Mojo 같은 새 런타임/언어가 쏟아지는 시대에 "왜 빠른가"를 평가할 수 있는 기준을 갖게 됩니다. 마케팅 문구에 휘둘리지 않고 "아, 이건 인라인 캐싱은 있는데 JIT은 없는 거구나" 같은 판단이 가능해지죠.
마무리
인터프리터 성능 최적화는 "수십 년간 쌓인 트릭의 집합"인데, Zef 문서는 그걸 한 곳에 모아 친절히 풀어준 보기 드문 자료예요. 언어 구현에 관심 있다면 꼭 한 번 읽어볼 만합니다.
여러분은 어떤 동적 언어를 주로 쓰시나요? 그리고 성능 때문에 곤란했던 경험이 있다면, 그게 인터프리터 자체 문제였는지 아니면 라이브러리 문제였는지 한번 돌이켜보면 어떨까요?
🔗 출처: Hacker News
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공