번역 - V8 Javascript 엔진의 단계 별 작동 방식

자바스크립트 세부사항 스터디

Posted by jopemachine on September 24, 2022 Original Posted by Carson Updated on October 03, 2022

V8 Javascript 엔진의 단계 별 작동 방식

하이 레벨에서 볼 때 V8 JavaScript 엔진 실행은 5단계로 구성됩니다.

  1. Host 환경 초기화

  2. 자바스크립트 코드 컴파일

  3. 바이트 코드 생성

  4. 바이트 코드 실행 및 인터프리팅

  5. 몇몇 바이트 코드의 최적화

Host 환경 초기화

기술적으로 이건 V8의 일이 아닙니다.

호스트 환경 및 V8 엔진 초기화는 렌더러 프로세스에서 수행됩니다.

브라우저는 여러 렌더러 프로세스들을 가집니다.

대체로, 각 브라우저 탭들은 각각의 렌더러 프로세스를 가지며, 각각 V8 인스턴스를 초기화 합니다.

만약 당신이 렌더러 프로세스와 브라우저가 어떻게 소통하는지에 더 관심이 있다면, 이 포스팅을 참고하세요.

호스트 환경이란 무엇일까요? 우리 환경에서 호스트 환경은 브라우저에 해당합니다.

그러므로, 우리는 이 글에서 “브라우저”, 그리고 “호스트 환경” 이란 용어를 동일한 의미로 사용할 것 입니다.

그러나, 브라우저는 자바스크립트가 실행되는 호스트 환경들 중 하나일 뿐이라는 점을 명심하세요.

또 다른 유명한 호스트 환경은 Node.js 입니다.

그래서 호스트 환경이 무엇입니까?

호스트 환경은 자바스크립트 엔진에 필요한 아래 사항들을 모두 제공합니다.

  1. 콜 스택
  2. 콜백 큐
  3. 이벤트 루프
  4. 웹 API와 웹 DOM

웹 페이지에서 유저 상호작용은 여러 종류의 이벤트들을 트리거링 합니다. 브라우저는 각 이벤트들의 콜백 함수를 콜백 큐에 추가합니다.

무한 루프처럼 작동하는 것처럼 보이는 이벤트 루프는 큐에서 지속적으로 콜백 함수를 가져옵니다.

그리고 콜백 함수의 자바스크립트는 컴파일 되고 실행됩니다. 몇몇 중간 데이터는 콜 스택에 저장되고, 배열이나 객체와 같은 데이터들은 힙에 저장됩니다.

왜 브라우저는 데이터 저장에 서로 다른 두 공간을 사용할까요?

  • 속도에 대한 트레이드 오프: 콜 스택은 빠른 속도를 위해 메모리 내에 연속적인 공간을 요구합니다. 그러나 메모리 내에 연속적인 공간을 드뭅니다. 이 문제를 해결하기 위해, 브라우저 디자이너들은 콜 스택 크기를 최대 크기로 제한 합니다. 대체로 브라우저는 제한된 크기의 콜 스택에 정수나 기본 데이터 타입의 데이터를 저장합니다.

  • 공간에 대한 트레이드 오프: 객체과 같은 비싼 데이터들은 연속적인 데이터 공간을 요구하지 않습니다. 반면 힙에서의 데이터 처리는 상대적으로 느려지게 됩니다.

제 의견으론, 콜 스택과 이벤트 루프는 자바스크립트가 어떻게 작동하는지에 관한 두 가지 중요한 메커니즘 입니다.

  • 여기에 어떻게 콜 스택이 작동하는지 설명하는 포스팅이 있습니다. 당신이 이 메커니즘에 관해 더 많이 배우고 싶다면, 이 포스트의 끝에 더 많은 읽을 거리가 있습니다.

  • 이벤트 루프에 관해선, Jake Archibald의 이 포스팅이 가장 좋은 상호작용 가능한 예제입니다.

V8 엔진은 호스트 환경에 의존하며, 호스트 환경에 힘을 부여합니다.

V8과 호스트 환경의 관계는 마치 당신의 컴퓨터의 OS과 응용 소프트웨어의 관계와 비슷합니다.

소프트웨어는 실행되는 OS에 의존합니다.

반면에, 그들은 당신의 시스템에 더 많은 작업들을 할 수 있도록 힘을 부여합니다.

예를 들어 포토샵을 들어보겠습니다.

포토샵은 Windows, macOS에서 실행될 필요가 있습니다.

반면 당신의 OS는 포토샵이 할 수 있는 아름다운 포스터를 만드는 작업을 할 수 없습니다.

V8 엔진도 마찬가지로, 호스트 환경에 추가적인 여러 기능들을 제공해줍니다.

  • 갹체와 함수의 생성 같은 ECMAScript 표준에 의존하는 자바스크립트의 핵심 기능들

  • 가비지컬렉션 메커니즘

  • 코루틴 기능

  • 그리고 더 많은 기능들…

호스트 환경과 V8이 준비되면, V8 엔진은 다음 단계를 시작합니다.

자바스크립트 코드 컴파일

이 단계에서 V8 엔진은 자바스크립트 코드를 AST로 변환하며, 스코프들을 생성합니다.

V8 엔진은 단순히 자바스크립트 언어만 사용하지 않습니다. 이 스크립트는 처리되기 전 구조화 되어야 합니다.

AST는 V8이 이해하기 쉬운 트리 구조입니다.

그 반면 글로벌 스코프, 그리고 호스트 환경의 콜 스택에 저장된 상단의 더 많은 스코프들을 포함한 스코프들은 이 단계에서 생성됩니다.

스코프는 그 자체로 다른 포스팅에서 설명할 가치가 있습니다. 여기선 건너뛰어도 좋습니다.

AST가 어떻게 생겼을까요?

아래와 같은 간단한 자바스크립트와 AST 포맷을 확인해봅시다.

1
const medium = 'good ideas';

각 자바스크립트 라인들은 위 예제와 같이 이 단계에서 AST로 변환됩니다.

바이트 코드 생성

이 단계에서 V8 엔진은 AST로 스코프와 바이트 코드 출력을 생성합니다.

바이트 코드는 어떻게 생겼을까요?

아래의 동일한 예제를 이번엔 V8의 개발자 쉘인 D8로 봅시다.

D8을 macOS에 설치하기 위해 아래 명령을 터미널에 입력하세요.

1
brew install v8

우리의 예제를 v8.js에 저장하고 터미널에서 아래 명령을 실행하세요.

1
d8 --print-bytecode v8.js

D8은 이전 스텝에서 생성된 AST와 스코프들에 기반하여 바이트 코드들을 출력합니다.

1
2
3
4
5
6
7
8
9
10
11
[generated bytecode for function:  (0x0ee70820ffed <SharedFunctionInfo>)]
Parameter count 1
Register count 1
Frame size 8
  0xee708210076 @    0 : 12 00             LdaConstant [0]
  0xee708210078 @    2 : 1d 02             StaCurrentContextSlot [2]
  0xee70821007a @    4 : 0d                LdaUndefined
  0xee70821007b @    5 : aa                Return
Constant pool (size = 1)
Handler Table (size = 0)
Source Position Table (size = 0)

Parameter count 1는 한 개의 파라미터가 있다는 것을 나타내며, 우리의 예제에서 medium이 이 파라미터에 해당합니다.

그리고, 아래엔 인터프리터가 실행할 4줄의 바이트 코드가 나타나 있습니다.

바이트 코드 인터프리팅 및 실행

바이트 코드들은 명령어의 집합입니다.

이 단계에서 인터프리터는 각 바이트 코드들을 위 부터 아래 순서로 실행할 것 입니다.

이전의 예제에서 우리는 4개의 바이트 코드들을 보았습니다.

1
2
3
4
LdaConstant [0]
StaCurrentContextSlot [2]
LdaUndefined
Return

각 라인의 바이트 코드들은 조립식 레고와 같습니다.

당신의 코드들이 얼마나 멋진지에 관계 없이, 이것들은 무대 뒤에선 기본적인 블록들로 조립됩니다.

각 바이트 코드들의 세부사항을 설명하는 것은 이 포스팅의 범위를 벗어납니다. 만약 관심이 있다면, 여기에 V8에서 사용되는 전체 바이트 코드들의 리스트가 있습니다.

기계어 컴파일 및 실행

이 단계는 이전 단계와 병렬적으로 실행됩니다.

바이트코드를 실행할 때 V8은 코드를 계속 주시하고 있으며, 이들을 최적화 할 기회를 찾고 있습니다.

몇몇 자주 사용되는 바이트 코드의 패턴이 발견되면, V8은 이들을 hot 이라고 마킹 합니다. “Hot”한 코드들은 더 효율적인 기계어로 변환되어 실행됩니다.

만약 최적화가 실패한다면 어떻게 될까요?

최적화는 코드들을 de-optimize 하여 인터프리터가 원래의 바이트 코드들을 실행하도록 만듭니다.

바이트 코드 vs 기계어

그러나 어째서 V8은 더 빠른 기계어를 직접적으로 사용하지 않는 걸까요?

어쨰서 전체 과정을 느리게 만드는 중간 언어인 바이트 코드를 사용하는 것일까요?

흥미롭게도, 이게 정확히 초기 V8 팀이 자바스크립트를 구현한 방식이었습니다.

초기 V8은 아래와 같이 동작합니다.

  1. V8은 스크립트를 AST, 스코프로 컴파일합니다.

  2. 컴파일러는 AST와 스코프를 기계어로 컴파일합니다.

  3. V8은 몇몇 자주 사용되는 기계어를 hot으로 마킹합니다.

  4. 또 다른 컴파일러는 hot 코드들을 최적화 된 기계어로 변환합니다.

  5. 만약 최적화가 실패하면 컴파일로는 process를 de-optimize해 실행합니다.

V8의 구조는 더 복잡해졌지만, 기본적인 아이디어는 동일합니다.

그러나 V8 팀은 바이트 코드를 도입하여 엔진을 진화시켰습니다. 왜일까요?

왜냐하면 기계어 코드를 사용하는 것은 여러 가지 문제점들을 동반하기 때문입니다.

1. 기계어 코드는 굉장히 많은 양의 메모리를 요구합니다.

V8 엔진은 컴파일된 기계어를 페이지가 로드 될 때 재사용하기 위해 메모리에 저장합니다.

기계어로 컴파일 할 때 10KB의 자바스크립트 코드는 20M 분량의 기계어 코드로 부풀려집니다. 이것은 2000배의 메모리 손실을 의미합니다.

이 경우 바이트 코드는 어느 정도의 메모리를 사용할까요? 80KB 정도입니다.

바이트 코드는 여전히 자바스크립트 원본 코드에 비해 무겁지만 기계어 코드에 비하면 훨씬 더 가볍습니다.

오늘 날 1MB를 넘어가는 자바스크립트 코드들은 흔하게 사용됩니다. 기계어 코드로 2GB 메모리를 사용하는 것은 좋은 생각이 아닙니다.

용량 최적화 덕분에 브라우저는 컴파일 된 바이트 코드들을 캐시할 수 있으며, 이전의 스탭들을 건너뛰어 코드들을 직접 실행할 수 있습니다.

2. 기계어 코드가 항상 바이트 코드보다 빠른 것은 아닙니다.

기계어 코드들은 실행될 때 빛처럼 빠른 반면, 컴파일 될 때 더 긴 시간을 요구합니다.

바이트 코드는 좀 더 실행이 느리지만 더 빠르게 컴파일 될 수 있습니다. 인터프리터는 실행 전 바이트 코드를 인터프리팅 할 필요가 있기 때문입니다.

우리가 이 두 케이스를 처음부터 끝까지 측정해 본다면 어떤 것이 더 빠를까요?

답은 경우에 따라 다르다는 것입니다

강력한 인터프리터와 똑똑한 바이트 코드 최적화 컴파일러를 개발하는 중간에서 균형을 잡아야 합니다.

V8에서 유명한 최적화 컴파일러는 TurboFan 으로 바이트 코드에서 높은 수준으로 최적화 된 기계어 코드를 생성해 냅니다.

3. 기계어는 개발 과정의 복잡함을 증가시킵니다.

서로 다른 CPU들은 서로 다른 구조를 가집니다. 각각의 CPU들은 서로 다른 기계어들을 이해합니다. 시장엔 아래와 같은 많은 종류의 프로세서가 있습니다.

  • ARM
  • ARM 64
  • X64
  • S397
  • 그리고 더 많은 것들…

만약 브라우저가 기계어 코드만 사용한다면, 각각의 많은 케이스들을 따로 처리하기 위해 많은 노력이 요구될 것입니다. 개발자로서, 우리는 이게 좋지 못한 일임을 직관적으로 알고 있습니다.

우리는 추상화가 필요합니다.

바이트 코드들은 자바스크립트 코드와 CPU 사이의 추상화입니다. 중간에 바이트 코드를 도입함으로써, V8 팀은 기계어 컴파일의 작업 양을 줄입니다. 그러면서, 이것은 V8이 다른 플랫폼으로 더 쉽게 포팅 가능하도록 도와줍니다.

결론

위의 모든 것들을 종합적으로 생각해 볼 때, 우리는 이제 어떻게 크롬 V8이 작동하는지 하이 레벨에서 살펴볼 수 있습니다.

원문

해당 게시물은 원작자의 허락을 받고 번역되었습니다. 이 글의 모든 저작권은 원작자에게 있습니다.

This article is a translated version of below article. All rights goes back to him.