컴파일러 보다 인터프리터가 빠르다고요?
총 투표 수: 0 생성자: 현우 채 카테고리: Build, JS, Optimization 주차: 10주차

오늘의 목표! 위 사진을 이해시켜드리겠습니다.
-1. 더 빠른 인터프리터 Javascript !
- Javascript는 계산 위주의 작업에 약하다
- Javascript는 JIT 컴파일러를 사용한다
- JavaScript는 자주 쓰는 코드를 캐싱해서 빠르다
- JS는 인터프리터라 느리다
- 근데 왜 Node.js 서버가 유행이지..?
$$
뭔소리지..?
$$
이제 알아보도록 하자 😁
0. 발표 개요
오늘 이야기할 것
- 컴파일러 vs 인터프리터 (기초 개념 정리)
- 인터프리터는 진짜 느릴까?
- JS 엔진(V8)은 이 문제를 어떻게 풀었을까?
- 그래서 개발자는 무엇을 하면 좋을까?
1. 컴파일러 vs 인터프리터
컴파일 후 실행 파일을 생성하는가? 메모리에 바로 적재하는가?
1-0 용어 정리
| 컴파일 | 소스코드를 기게어 혹은 기계어와 유사한 low level까지 해석하는 과정 |
|---|---|
| Bytecode / | |
| IR (Intermediate Representation) | 가상머신이 이해할 수 있는 중간 레벨로 컴파일 한 코드 |
| Native code / 기계어 | CPU가 직접 해독하고 실행할 수 있는 비트 안뒤(0과 1)로 쓰인 컴퓨터 언어 |
1-1. 컴파일러 (C, C++)

프로그래밍 언어(C, C++)를 기계어로 빠르게 컴파일할 수 있도록 미리 번역해둔 프로그램
- 실행 전에 전부 변환
- 실행 속도 빠름
- 실행 파일 생성됨
컴파일 과정
- 전처리기: 컴파일하기 좋은 형태로 변환 (define)
- 컴파일러: 고급언어 → 저급 중간 표현(IR) or 어셈블리 코드로 변환
- 저급 중간 표현(IR): LLVM 등…
- 최적화까지 포함 (죽은 코드 제거, 루프 최적화 등)
- 어셈블러: 어셈블리 코드 → 기계어(obj 파일)
- 링커: 여러 obj 파일 + 라이브러리 → 하나의 실행 파일 (out, exe 등) / 실제 주소 배정
1-2. 인터프리터

고급언어 해석 후 바로 메모리에 적재
- 실행 파일 없음
- 유연함, 디버깅 쉬움
- ❌ 같은 코드도 매번 해석 → 느림
인터프리팅 과정
한 줄 한 줄 해석 → 메모리에 적재 (실행)
1-3. 비교해보자
| 컴파일러 | 인터프리터 |
|---|---|
| 실행 전에 전부 변환 | 한 번에 한 줄씩 변환 (런타임) |
| 실행 속도 빠름 | 같은 코드도 매번 해석 → 느림 |
| 실행 파일 생성됨 | 실행 파일 없음 (메모리에 바로 적재) |
| 실행 환경마다 컴파일 필요 | 실행 환경에 맞게 알아서 컴파일 |
| 실행 파일만 필요 | 런타임 엔진(인터프리터) 필요 (Node.js / JVM 등) |
2. 인터프리터는 진짜 느릴까?
2-1. JIT 컴파일이란

컴파일러 + 인터프리터 = 중간 단계까지 컴파일 하자
- 컴파일하기 쉬운 형태인 Bytecode (IR) 로 미리 변환
- 런타임에 바이트코드 → 기계어(native code) 번역
- 한 줄 한 줄 컴파일 → 결과적으로 사용되는 부분만 컴파일
- 바이트코드 → 기계어(native code) 캐시에 저장
- 이미 번역한 컴파일 과정을 반복하지 않기 위해 사용
2-2. Adaptive Compilation (Adaptive JIT)

실행초기에는 인터프리터로 실행하고, 자주 호출되는 메소드나 반복되는 코드를 검출하여 이런 코드만 컴파일하는 방식
런타임에 얻은 정보로 컴파일 하자!! 런타임 상황에 맞도록 최적화!
Adaptive JIT: JIT 인터프리터 + 런타임 최적화 컴파일러
코드가 사용되었을 때, 즉시 컴파일 X, 여러번 호출 된 후 지연시켜 컴파일. (Lazy Compilation)
프로그램이 실행될 때, 일부 부분만 많이 사용되는 특징이 있음 (80/20 법칙)
→ 런타임에서 얻을 수 있는 정보로 최적화 하기 때문에, 정적컴파일 보다 성능이 향상되는 경우도 있다고 함.
3. 현대 JS 엔진의 핵심: JIT 컴파일
JS 엔진의 진화: 인터프리터 + 컴파일러의 결합
JavaScript는 전통적인 인터프리터의 단점(느린 속도)을 극복하기 위해 JIT (Just-In-Time) 컴파일 방식을 사용합니다.


3.1 🌟 V8 엔진 (Chrome, Node.js)의 4단계 실행 과정:
- 파싱 (Parsing): JS → AST (추상 구문 트리) 변환 (코드 구조 분석)
- 렉싱 / 토크나이징 : 코드를 의미있는 조각으로 나눔 → 이때 (렉시컬) 스코프 결정
- Abstract Syntax Tree (AST) : 코드를 트리구조로
- 바이트코드 생성 & 실행 (Ignition 인터프리터):
- AST → 바이트코드 변환
- Ignition 인터프리터 : 바이트코드 실행 (빠른 초기 실행 담당)
- 프로파일링 (Profiling):
- 런타임 시 "자주 실행되는 코드" (Hot Code) 모니터링 (예: 반복문 내부의 함수)
- 최적화 컴파일 (TurboFan 컴파일러):
- Hot Code만 JIT 컴파일러인 **TurboFan (Adaptive JITC)**에게 보냅니다.
- TurboFan은 이 코드를 **가장 빠르고 최적화된 기계어 코드 (Native Code)**로 변환
- 다음부터 이 코드가 실행될 때는 번역 과정 없이 기계어 코드가 직접 실행
3.2 어떻게 속도가 빨라지는가?
function add(a, b) {
return a + b;
}
let sum = 0;
for (let i = 0; i < 100000; i++) {
sum += add(1, 2);
}
Adaptive JIT (적응형 JIT)란? - GPT
3.2-1. Ignition 인터프리터
; 함수 add(a, b)
LOAD_ARG a ; a를 레지스터에 로드
LOAD_ARG b ; b를 레지스터에 로드
CHECK_TYPE a ; a가 number인지?
CHECK_TYPE b ; b가 number인지?
; (아닐 수도 있으니까 매번 검사) - slow case
ADD_GENERIC a, b ; + 연산 수행
; (숫자 덧셈인지, 문자열 결합인지 런타임에 결정)
RETURN
SET sum = 0
SET i = 0
LOOP_START:
CHECK i < 100000 ; 반복 조건 검사
IF_FALSE GOTO END
CALL add(1, 2) ; 함수 호출 (비용 있음)
LOAD result
CHECK_TYPE sum ; sum 타입 확인
ADD_GENERIC sum, result
; 연산 의미를 매번 판단 (타입 별 체크)
INCREMENT i
GOTO LOOP_START
END:
3.2-2 TurboFan 컴파일러
; add 함수는 인라인 처리됨
; add(1, 2) → 3
SET sum = 0
SET i = 0
LOOP_START:
CMP i, 100000
JGE END
ADD sum, 3 ; 타입 체크 없음
; 함수 호출 없음
; 바로 CPU 덧셈
INC i
JMP LOOP_START
END:
3.2-3 만약에 이랬다면?
sum += add(1, "2");
3.3 Adaptive JIT 컴파일의 효과
| 구분 | 전통적인 인터프리터 | 현대적인 JIT (JS) |
|---|---|---|
| 코드 변환 | 실행 시 매번 바이트코드 변환 | 자주 쓰이는 코드는 영구적인 기계어로 변환 |
| 속도 | 느림 | 초기 실행은 느리지만, 반복 실행 시 매우 빠름 (컴파일러급 성능) |
| 오류 검출 | 해당 줄에 도달해야 발견 | (동일) 실행 중 발견되나, 최적화 중에도 오류 감지 가능 |
| 개발 편의성 | 높음 | 높음 (인터프리터의 유연성을 유지) |
4. 그래서 개발자는 무엇을 하면 좋을까?
1. Hotspot을 만들자
- 작은 함수
- 반복 호출 구조
2. 타입 안정성
- 같은 변수에 다른 타입 넣지 않기
- hidden class 깨지지 않게
3. 불필요한 추상화 주의
- 과도한 고차 함수
- 동적 구조 변경