JavaScript 코어와 V8: 엔진 아키텍처 및 성능 심층 분석
Grace Collins
Solutions Engineer · Leapcell

소개
끊임없이 진화하는 웹 개발과 그 이상의 환경에서 JavaScript는 인터랙티브한 경험을 위한 주요 언어로 자리 잡고 있습니다. 모든 JavaScript 코드 라인 뒤에는 사람이 읽을 수 있는 명령을 기계 실행 가능한 명령으로 번역하기 위해 끊임없이 노력하는 정교한 엔진이 있습니다. 수많은 JavaScript 엔진 중에서 JavaScriptCore와 V8은 가장 영향력 있고 널리 채택된 두 엔진으로 두각을 나타냅니다. 이들의 기본 아키텍처와 성능 특성을 이해하는 것은 단순한 학술적 연습이 아닙니다. 최적화되고 효율적인 코드를 작성하려는 개발자와 중요한 기술 스택 결정을 내리는 설계자에게 귀중한 통찰력을 제공합니다. 이 탐구는 이러한 강력한 엔진의 계층을 벗겨내어 고유한 철학과 JavaScript 실행에 대한 영향을 밝힐 것입니다.
JavaScript 엔진의 핵심 개념
JavaScriptCore와 V8의 특정 사항을 자세히 알아보기 전에 대부분의 최신 JavaScript 엔진에 공통적인 몇 가지 기본 개념을 파악하는 것이 중요합니다.
- 파서(Parser): 첫 번째 단계로, 파서는 JavaScript 소스 코드를 읽고 이를 추상 구문 트리(AST)로 변환합니다. AST는 구문 세부 사항이 없는 코드 구조의 트리와 같은 표현입니다.
- 인터프리터(Interpreter): 인터프리터는 AST를 한 줄씩 직접 실행합니다. 더 간단하고 시작 시 더 빠르지만, 동일한 코드를 반복해서 해석하기 때문에 장기 실행 코드에서는 일반적으로 더 느립니다.
- 바이트코드 생성기(Bytecode Generator): 일부 엔진은 파싱 후 AST를 바이트코드라고 하는 중간 표현으로 변환합니다. 바이트코드는 원시 AST보다 더 작고 효율적으로 실행됩니다.
- JIT 컴파일러(Just-In-Time Compiler): 성능 마법이 일어나는 곳입니다. JIT 컴파일러는 런타임 중에 자주 실행되는 바이트코드(또는 경우에 따라 AST 자체)를 동적으로 매우 최적화된 기계 코드로 컴파일합니다.
- 베이스라인 컴파일러(Baseline Compiler): 따뜻한(warm) 함수에 대해 괜찮은 기계 코드를 빠르게 생성하는 빠르고 낮은 수준의 컴파일러입니다. 주요 목표는 컴파일 속도이지 최고 성능이 아닙니다.
- 최적화 컴파일러(Optimizing Compiler): 뜨거운(hot) 함수(여러 번 실행되는 함수)를 분석하고 종종 추측적 최적화를 사용하여 매우 최적화된 기계 코드를 생성하는 느리고 높은 수준의 컴파일러입니다.
- 가비지 컬렉터(GC): JavaScript는 가비지 컬렉션 언어이므로 개발자는 메모리를 수동으로 관리할 필요가 없습니다. GC는 더 이상 도달할 수 없는 메모리를 자동으로 회수합니다. 다양한 엔진은 다양한 GC 알고리즘(예: 마크-스윕, 세대별)을 사용합니다.
Safari 및 WebKit의 기반 JavaScriptCore
JavaScriptCore(JSC)는 Apple의 Safari 웹 브라우저 및 기타 WebKit 기반 애플리케이션에서 주로 사용되는 JavaScript 엔진입니다. 아키텍처는 시간과 함께 크게 발전하여 시작 성능과 최고 실행 속도의 균형을 맞추기 위해 다단계 컴파일 파이프라인을 채택했습니다.
JSC의 핵심 아키텍처 구성 요소는 다음과 같습니다.
- 렉서 및 파서: JavaScript 코드를 AST로 초기 파싱을 처리합니다.
- 바이트코드 생성기: AST에서 중간 바이트코드 표현을 생성합니다.
- 인터프리터(LLInt - Low-Level Interpreter): 바이트코드를 실행합니다. 이것은 빠른 시작을 제공합니다.
- JIT 계층: JSC는 여러 계층의 JIT 컴파일을 사용합니다.
- 베이스라인 JIT: "따뜻한" 코드의 첫 번째 계층입니다. 바이트코드를 비교적 빠르게 기계 코드로 컴파일하여 해석보다 상당한 속도 향상을 제공합니다.
- DFG JIT(Data Flow Graph JIT): 더 고급 최적화 컴파일러입니다. 코드 사용을 프로파일링하고 데이터 흐름 그래프를 구축하여 유형 전문화, 인라이닝, 죽은 코드 제거와 같은 고급 최적화를 적용합니다. 컴파일이 더 느리지만 "뜨거운" 함수에 대해 훨씬 더 빠른 기계 코드를 생성합니다.
- FTL JIT(Faster Than Light JIT): 가장 높고 공격적인 최적화 계층입니다. LLVM(Low-Level Virtual Machine)을 백엔드로 사용하여 전통적으로 정적 컴파일러에서 볼 수 있는 정교한 기계 코드 생성 및 최적화를 가능하게 합니다. 이 계층은 최대 성능을 위해 "매우 뜨거운" 함수를 대상으로 합니다.
최적화 예시(JSC의 경우 개념적):
숫자를 더하는 루프를 상상해 보세요.
function sumArray(arr) { let total = 0; for (let i = 0; i < arr.length; i++) { total += arr[i]; } return total; } // 반복적으로 호출되어 "뜨거워짐" for (let j = 0; j < 100000; j++) { sumArray([1, 2, 3, 4, 5]); }
처음에는 sumArray
가 해석되거나 베이스라인 JIT에 의해 실행될 것입니다. 일관되게 숫자 배열을 받는다면 DFG JIT는 루프 내부의 유형 검사를 제거하는 number[]
에 대해 전문화할 수 있습니다. FTL JIT가 호출되면 LLVM의 기능을 활용하여 작업 벡터화 또는 루프 풀림과 같은 특정 배열 크기에 대해 추가로 최적화할 수 있습니다.
JavaScriptCore의 성능 특성:
- 메모리 효율성 우수: JSC는 Safari가 널리 사용되는 모바일 장치에 중요한 상대적으로 보수적인 메모리 사용으로 알려져 있습니다.
- 뛰어난 최고 성능: FTL JIT가 LLVM을 활용하여 JSC는 많이 최적화된 코드에 대해 매우 높은 최고 성능을 달성할 수 있습니다.
- 좋은 시작 성능: LLInt 및 베이스라인 JIT는 반응성 있는 사용자 경험을 보장합니다.
- 전력 효율성 집중: 주로 Apple 장치를 대상으로 하므로 전력 소비는 설계의 핵심 고려 사항입니다.
Chrome 및 Node.js의 기반 V8
V8은 Google의 오픈 소스 JavaScript 및 WebAssembly 엔진으로, 놀라운 성능과 Chrome, Node.js 및 Electron을 구동하는 역할로 유명합니다. V8은 공격적이고 고도로 동적인 전략으로 최적화에 접근합니다.
V8의 정교한 파이프라인은 다음과 같습니다.
- Ignition(인터프리터): V8은 초기 실행을 위해 완전한 JIT 컴파일을 포기하고 Ignition을 도입했습니다. 바이트코드를 생성하고 실행합니다. Ignition은 매우 효율적이며 이전 V8 버전에 비해 메모리 사용량을 줄이고 시작 시간을 개선합니다.
- TurboFan(최적화 컴파일러): 이것은 V8의 주요 최적화 컴파일러입니다. TurboFan은 Ignition의 바이트코드(함수가 "뜨겁다"는 프로파일링 정보가 표시된 후)를 가져와 매우 최적화된 기계 코드로 컴파일합니다. 다음과 같은 광범위한 최적화를 수행합니다.
- 인라이닝: 함수 호출을 함수의 본문으로 대체합니다.
- 유형 전문화: Ignition의 유형 피드백을 사용하여 관찰된 유형에 특화된 코드를 생성합니다.
- 숨겨진 클래스(또는 맵): V8은 숨겨진 클래스를 사용하여 객체 레이아웃을 효율적으로 표현하고 빠른 속성 액세스 및 다형적 연산을 허용합니다.
- 추측적 최적화 및 역최적화: TurboFan은 관찰된 유형을 기반으로 가정을 합니다. 가정이 위반되면(예: 함수가 갑자기 다른 유형을 수신하는 경우) TurboFan은 해당 코드를 역최적화하여 인터프리터 또는 덜 최적화된 버전으로 되돌리고 다시 컴파일합니다.
최적화 예시(V8의 경우):
동일한 sumArray
함수를 고려해 보세요.
function sumArray(arr) { let total = 0; for (let i = 0; i < arr.length; i++) { total += arr[i]; } return total; } // 반복적으로 호출되어 "뜨거워짐" for (let j = 0; j < 100000; j++) { sumArray([1, 2, 3, 4, 5]); }
sumArray
가 자주 호출되면 Ignition은 유형 피드백(예: arr
는 항상 숫자 배열임)을 제공합니다. 그런 다음 TurboFan은 예를 들어 다음과 같은 최적화된 sumArray
버전을 컴파일합니다.
arr[i]
가 항상 숫자일 것이라고 확신합니다.- 배열 크기가 작으면 예측 가능하면 루프를 풀 수 있습니다.
- 비용이 많이 드는 런타임 유형 검사를 피합니다.
나중에 sumArray(['a', 'b'])
가 호출되면 TurboFan은 해당 sumArray
컴파일 코드 경로를 역최적화하고, Ignition을 통해 실행하고, 새로운 유형 피드백을 수집하고, 새로운 유형 패턴이 안정화되면 다시 컴파일할 수 있습니다.
V8의 성능 특성:
- 탁월한 최고 성능: TurboFan의 공격적인 최적화와 동적 역최적화가 결합되어 V8은 뜨거운 코드에 대해 매우 높은 실행 속도를 달성할 수 있습니다.
- Ignition을 통한 빠른 시작: Ignition은 초기 구문 분석 및 실행을 빠르게 제공하여 성능과 메모리의 균형을 맞춥니다.
- 공격적인 메모리 사용: 역사적으로 V8은 절대적인 메모리 효율성보다 속도를 우선시했지만, 이를 개선하기 위한 지속적인 노력이 이루어지고 있습니다.
- 처리량 최적화: 서버 측 환경(Node.js) 및 복잡한 클라이언트 측 애플리케이션(Chrome)에 대해 설계되었으며, 지속적인 고성능이 중요합니다.
아키텍처 및 성능 차이점
기능 | JavaScriptCore(JSC) | V8 |
---|---|---|
인터프리터 | LLInt (저수준 인터프리터) | Ignition (바이트코드 인터프리터) |
JIT 컴파일러 | 베이스라인 JIT, DFG JIT, FTL JIT (LLVM 사용) | TurboFan (최적화 컴파일러) |
계층적 컴파일 | 더 명확한 계층(3개의 JIT), 최상단에 LLVM 위치 | 이중 계층: Ignition(인터프리터) 및 TurboFan(JIT) |
최적화 초점 | 균형 잡힌 접근 방식, 뛰어난 메모리/전력 효율성, 우수한 최고 성능. | 공격적인 처리량 지향, 매우 높은 최고 성능. |
역최적화 | 역최적화 사용 빈도 낮음. | 추측적 최적화 및 역최적화에 크게 의존. |
백엔드 | 베이스라인/DFG용 맞춤형 백엔드, FTL용 LLVM | TurboFan용 맞춤형 백엔드 |
메모리 사용량 | 일반적으로 더 메모리 효율적 | 더 많은 메모리 사용 가능, 속도 우선 |
가비지 컬렉터 | 마크-스윕, 세대별 | 세대별 (Orinoco 컬렉터) |
주요 환경 | Safari, WebKit 기반 앱, iOS 환경 | Chrome, Node.js, Electron |
비교 요약:
LLVM을 활용하는 FTL JIT로 정점을 찍는 다단계 JIT 파이프라인을 갖춘 JSC는 균형 잡힌 접근 방식을 목표로 합니다. 좋은 시작 성능, 모바일에 적합한 뛰어난 메모리 효율성, 그리고 결국 뜨거운 코드에 대한 매우 높은 최고 성능을 달성하려고 합니다. LLVM에 대한 의존성은 매우 성숙하고 강력한 컴파일러 인프라에 접근할 수 있게 해줍니다.
V8은 Ignition과 TurboFan을 사용하여 보다 공격적인 이중 컴파일러 접근 방식을 취합니다. 원시 실행 속도와 복잡한 애플리케이션의 처리량을 우선시합니다. 추측적 최적화와 강력한 역최적화 메커니즘을 통해 지속적으로 매우 성능이 뛰어난 기계 코드를 생성하여 Chrome의 까다로운 웹 애플리케이션 및 서버 측 Node.js 애플리케이션과 같은 시나리오에 강력한 기능을 제공합니다.
둘 중 하나를 선택하는 것은 종종 환경에 달려 있습니다. Apple의 생태계의 경우 JSC는 기본적이고 최적화된 선택입니다. 크로스 플랫폼 데스크톱 애플리케이션(Electron) 또는 서버 측 JavaScript(Node.js)의 경우 V8의 성능 특성이 지배적인 엔진입니다.
결론
JavaScriptCore와 V8은 모두 JavaScript 실행 영역에서 공학의 정점을 나타내며, 각 엔진은 언어의 최대 성능을 추출하기 위해 세심하게 만들어졌습니다. JavaScriptCore는 다단계 시스템과 LLVM을 활용하여 모바일 및 데스크톱에 적합한 매우 높은 최고 성능을 달성하는 균형 잡힌 접근 방식과 메모리 효율성에서 탁월한 반면, V8은 공격적이고 추측적인 최적화와 동적 역최적화를 통해 이중 컴파일러 접근 방식으로 차별화되어 현대 웹과 서버 측 JavaScript를 구동하는 원시 처리량과 최고 성능을 제공합니다. 궁극적으로 두 엔진 모두 JavaScript의 가능성을 지속적으로 확장하며 언어의 놀라운 다재다능함과 광범위한 채택을 이끌고 있습니다.