
솔직히 말해, 인터프리터를 직접 구현한다고 하면 대부분 토이 프로젝트 수준에서 그칩니다. 하지만 3D 기하(Geometry)를 다루는 언어라면 이야기가 다릅니다. 버텍스 하나만 늘어도 Latency(지연 시간)가 기하급수적으로 튑니다. 개발자가 코드 한 줄 고치고 '실행' 버튼 누를 때마다 1초씩 멍때려야 한다면, 그 언어는 죽은 거나 다름없습니다.
오늘은 3D 기하 언어 'Geoscript'를 개발하면서 겪은 성능 최적화 과정을 공유합니다. 뻔한 상수 폴딩(Constant Folding) 이야기하려는 게 아닙니다. 라이브 코딩 환경에서 Latency를 극적으로 줄인 Cross-Run Persistence(실행 간 캐시 지속) 전략에 대한 이야기입니다.
1. 기본기: 순수 함수의 이점을 쥐어짜라
Geoscript의 가장 큰 특징은 외부 DB 연결이나 사용자 입력이 없는 '순수 함수(Pure Function)' 기반이라는 점입니다. RNG(난수 생성기)조차 시드(Seed)가 고정되어 있어, 실행할 때마다 완벽하게 동일한 결과를 뱉습니다.
이 특성을 이용하면 상수 폴딩을 극한까지 밀어붙일 수 있습니다.
일반적인 상수 폴딩:
1 + 1을2로 바꿈.Geoscript의 상수 폴딩: 복잡한 메쉬 생성 함수 전체를 미리 계산된 리터럴로 바꿈.
예를 들어, 루프 안에서 icosphere(radius=10)를 호출한다고 칩시다. 이 함수가 외부 변수에 의존하지 않는다면, 컴파일러(혹은 인터프리터 전처리) 단계에서 아예 메쉬 데이터로 치환해 버립니다. 런타임에 구를 1000번 생성하는 오버헤드를 0으로 만드는 겁니다.
2. 준비 작업: CSE와 구조적 해싱
상수 폴딩 다음 단계는 공통 하위 표현식 제거(Common Subexpression Elimination, CSE)입니다. 코드 여기저기서 똑같은 계산을 반복하는 걸 막아야 합니다.
이를 구현하려면 AST(추상 구문 트리)의 두 노드가 '동일하다'는 것을 증명해야 합니다. 저는 트리 기반 구조 해싱(Tree-based Structural Hashing)을 사용했습니다.
- AST의 각 노드를 순회하며 u128 해시를 생성.
- 동일한 해시가 발견되면, 재계산 대신 캐시된 값을 사용.
- 스코프와 문맥(Context)까지 고려해야 해서 구현이 까다롭지만, 한 번 해두면 표현식 내부(Intra-expression) 최적화가 가능해집니다.
여기까지는 교과서적인 내용입니다. 진짜 문제는 이 다음입니다.
3. 발상의 전환: 왜 캐시를 매번 초기화하는가?
Shadertoy나 Geotoy 같은 툴은 '라이브 코딩' 환경입니다. 개발자는 코드를 조금 수정하고, 실행하고, 결과를 확인하는 루프를 수없이 반복합니다.
여기서 의문이 들었습니다. "코드의 90%는 그대로인데, 왜 실행할 때마다 모든 캐시를 날려버리는가?"
이전 실행(Run)에서 생성된 상수 표현식 캐시를 다음 실행까지 지속(Persist)시키지 못할 이유가 없습니다. 이것이 바로 'Cross-Run Expr Cache Persistence'입니다.
[실전 사례: 900ms의 병목 해결]
실제 Geotoy에서 사용되는 무거운 연산을 예로 들어보겠습니다.
- 랜덤한 점들을 생성 (
randv) alpha_wrap으로 점들을 감싸는 메쉬 생성 (매우 무거운 연산, 약 900ms 소요)simplify로 메쉬 단순화- 렌더링
개발자가 3번 단계인 simplify의 파라미터를 튜닝하고 있다고 가정해 봅시다. 기존 방식대로라면 코드를 수정하고 실행할 때마다 앞단의 alpha_wrap을 다시 계산해야 합니다. 매번 900ms의 Latency가 발생합니다.
하지만 캐시를 지속(Persist)시킨다면? 인터프리터는 alpha_wrap 부분의 코드가 바뀌지 않았음을 해시를 통해 감지합니다. 즉시 이전 실행의 메모리에서 결과값을 가져옵니다.
- Before: 코드 수정 -> 실행 ->
alpha_wrap(900ms) ->simplify-> 렌더링 - After: 코드 수정 -> 실행 -> 캐시 히트(0ms) ->
simplify-> 렌더링
결과적으로 개발자는 simplify 함수의 결과만 즉각적으로 확인하게 됩니다. 체감 Latency는 거의 제로에 수렴합니다.
결론: 아키텍처가 경험을 만든다
많은 주니어 개발자들이 성능 최적화라고 하면 알고리즘 복잡도(Big-O)만 따집니다. 하지만 시스템 전체의 워크플로우를 이해하면, O(n)을 줄이는 것보다 아예 n을 실행하지 않는 방법이 보입니다.
여러분이 만드는 시스템이 '반복적인 작업'을 수행한다면, 무조건 다시 계산하고 있는 건 아닌지 의심해 보십시오. 가장 빠른 코드는 실행되지 않는 코드입니다.


