인터프리터 성능 병목으로 개발 속도가 바닥칠 때, 900ms 지연 시간을 0으로 만드는 캐싱 아키텍처

인터프리터 성능 병목으로 개발 속도가 바닥칠 때, 900ms 지연 시간을 0으로 만드는 캐싱 아키텍처

Alex Kim·2026년 1월 22일·3

3D 기하 언어 Geoscript 개발 중 겪은 성능 병목을 Cross-Run Persistence 전략으로 해결하여 지연 시간을 900ms에서 0으로 단축한 최적화 과정을 공유합니다.

솔직히 말해, 인터프리터를 직접 구현한다고 하면 대부분 토이 프로젝트 수준에서 그칩니다. 하지만 3D 기하(Geometry)를 다루는 언어라면 이야기가 다릅니다. 버텍스 하나만 늘어도 Latency(지연 시간)가 기하급수적으로 튑니다. 개발자가 코드 한 줄 고치고 '실행' 버튼 누를 때마다 1초씩 멍때려야 한다면, 그 언어는 죽은 거나 다름없습니다.

오늘은 3D 기하 언어 'Geoscript'를 개발하면서 겪은 성능 최적화 과정을 공유합니다. 뻔한 상수 폴딩(Constant Folding) 이야기하려는 게 아닙니다. 라이브 코딩 환경에서 Latency를 극적으로 줄인 Cross-Run Persistence(실행 간 캐시 지속) 전략에 대한 이야기입니다.

1. 기본기: 순수 함수의 이점을 쥐어짜라

Geoscript의 가장 큰 특징은 외부 DB 연결이나 사용자 입력이 없는 '순수 함수(Pure Function)' 기반이라는 점입니다. RNG(난수 생성기)조차 시드(Seed)가 고정되어 있어, 실행할 때마다 완벽하게 동일한 결과를 뱉습니다.

이 특성을 이용하면 상수 폴딩을 극한까지 밀어붙일 수 있습니다.

일반적인 상수 폴딩: 1 + 12로 바꿈.

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에서 사용되는 무거운 연산을 예로 들어보겠습니다.

  1. 랜덤한 점들을 생성 (randv)
  2. alpha_wrap으로 점들을 감싸는 메쉬 생성 (매우 무거운 연산, 약 900ms 소요)
  3. simplify로 메쉬 단순화
  4. 렌더링

개발자가 3번 단계인 simplify의 파라미터를 튜닝하고 있다고 가정해 봅시다. 기존 방식대로라면 코드를 수정하고 실행할 때마다 앞단의 alpha_wrap을 다시 계산해야 합니다. 매번 900ms의 Latency가 발생합니다.

하지만 캐시를 지속(Persist)시킨다면? 인터프리터는 alpha_wrap 부분의 코드가 바뀌지 않았음을 해시를 통해 감지합니다. 즉시 이전 실행의 메모리에서 결과값을 가져옵니다.

  • Before: 코드 수정 -> 실행 -> alpha_wrap(900ms) -> simplify -> 렌더링
  • After: 코드 수정 -> 실행 -> 캐시 히트(0ms) -> simplify -> 렌더링

결과적으로 개발자는 simplify 함수의 결과만 즉각적으로 확인하게 됩니다. 체감 Latency는 거의 제로에 수렴합니다.

결론: 아키텍처가 경험을 만든다

많은 주니어 개발자들이 성능 최적화라고 하면 알고리즘 복잡도(Big-O)만 따집니다. 하지만 시스템 전체의 워크플로우를 이해하면, O(n)을 줄이는 것보다 아예 n을 실행하지 않는 방법이 보입니다.

여러분이 만드는 시스템이 '반복적인 작업'을 수행한다면, 무조건 다시 계산하고 있는 건 아닌지 의심해 보십시오. 가장 빠른 코드는 실행되지 않는 코드입니다.

Alex Kim
Alex KimAI 인프라 리드

모델의 정확도보다 추론 비용 절감을 위해 밤새 CUDA 커널을 깎는 엔지니어. 'AI는 마법이 아니라 전기세와 하드웨어의 싸움'이라고 믿습니다. 화려한 데모 영상 뒤에 숨겨진 병목 현상을 찾아내 박살 낼 때 가장 큰 희열을 느낍니다.

Alex Kim님의 다른 글

댓글 0

첫 번째 댓글을 남겨보세요!