[번역] Understanding V8’s Bytecode

원문 : https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775

V8은 Google의 오픈소스 JavaScript 엔진입니다. Chrome, Node.js 및 기타 여러 애플리케이션들은 V8을 사용합니다. 이 글에서는 V8의 바이트 코드 형식에 대해 설명합니다. 기본 개념을 알고 있으면 실제로 읽기 쉽습니다.

중국어로 쓰여진 글이며, justjavac가 번역한 글입니다.

V8이 JavaScript 코드를 컴파일 할 때 parser는 추상 구문 트리(abstract syntax tree)를 생성합니다. 구문 트리는 JavaScript 코드의 구문 구조를 트리로 표현한 것입니다. 인터프리터 Ingnition은 구문 트리에서 바이트 코드를 생성합니다. 최적화 컴파일러인 TurboFan은 최종적으로 바이트 코드를 가져와서 최적화된 머신 코드를 생성합니다.

두 가지 실행 모드가 있는 이유를 알고 싶다면 JSConfEU에서 비디오를 확인하세요.

바이트 코드는 머신 코드의 추상화입니다. 바이트 코드를 실제 CPU와 동일한 계산 모델로 설계 한 경우 바이트 코드를 머신 코드로 더 쉽게 컴파일 할 수 있습니다. 이것이 인터프리터가 종종 레지스터 혹은 스택 머신인 이유입니다. Ignition은 누산기(accumulator) 레지스터가 있는 레지스터 머신입니다.

V8의 바이트 코드는 함께 구성될 때 JavaScript 기능을 가능하게 하는 작은 빌딩 블록으로 생각할 수 있습니다. V8에는 수 백개의 바이트 코드가 있습니다. Add , TypeOf 또는 LdaNamedProperty 같은 프로퍼티 로드하는 연산자들이 바이트 코드에 있습니다. 또한 V8은 CreateObjectLiteral 혹은 SuspendGenerator 와 같은 매우 특정한 바이트 코드도 있습니다. 헤더 파일 bytecodes.h 는 바이트 코드 전체 목록을 정의합니다.

각각의 바이트 코드는 입력 및 출력을 레지스터 피연산자로 지정합니다. Ignition은 r0, r1, r2... 와 누산기 레지스터를 사용합니다. 거의 모든 바이트 코드는 누산기 레지스터를 사용합니다. 바이트 코드가 지정하지 않는 다는 점을 제외하면 일반 레지스터와 동일합니다. 예를 들어 Add r1r1 레지스터의 값을 누산기 레지스터의 값에 더합니다. 이것은 바이트 코드를 더 짧게 유지하고 메모리를 절약합니다.

많은 바이트 코드는 Lda 또는 Sta 로 시작합니다. LdaStaa 는 누산기를 나타냅니다. 예를 들어, LdaSmi [42] 는 Small Integer(Smi) 42 를 누산기 레지스터에 로드합니다. Star r0 는 현재 누산기의 값을 레지스터 r0에 저장합니다.

지금까지는 기본 내용이였고, 실제 바이트 코드의 기능에 대해 살펴보겠습니다.

function incrementX(obj) {
  return 1 + obj.x;
}
incrementX({x: 42});  // V8’s compiler is lazy, 
                      // if you don’t run a function, it won’t interpret it.

JavaScript의 V8 바이트 코드를 보기 위해서, D8 또는 --print-bytecode 플래그를 Node.js(8.3 또는 더 위의 버전)와 함께 호출함으로써 출력할 수 있습니다. Chrome의 경우 커맨드 라인에서 --js-flags="--print-bytecode" 를 사용하여 Chrome을 시작하세요. Run Chromium with flags를 참고하세요.

$ node --print-bytecode incrementX.js
...
[generating bytecode for function: incrementX]
Parameter count 2
Frame size 8
  12 E> 0x2ddf8802cf6e @    StackCheck
  19 S> 0x2ddf8802cf6f @    LdaSmi [1]
        0x2ddf8802cf71 @    Star r0
  34 E> 0x2ddf8802cf73 @    LdaNamedProperty a0, [0], [4]
  28 E> 0x2ddf8802cf77 @    Add r0, [6]
  36 S> 0x2ddf8802cf7a @    Return
Constant pool (size = 1)
0x2ddf8802cf21: [FixedArray] in OldSpace
 - map = 0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)>
 - length: 1
           0: 0x2ddf8db91611 <String[1]: x>
Handler Table (size = 16)

대부분의 출력을 무시하고 바이트 코드에만 집중할 수 있습니다. 각 바이트 코드의 의미는 다음과 같습니다.

LdaSmi [1]

LdaSmi [1] 은 누산기에 상수 값 1을 로드합니다.

Star r0

다음으로 Star r0 는 현재 누산기에 있는 값 1을 레지스터 r0 에 저장합니다.

LdaNamedProperty a0, [0], [4]

LdaNamedPropertya0 로 불리는 속성을 누산기에 로드합니다. aiincrementX() 의 i 번째 인수를 가리킵니다. 이 예제에서는 incrementX() 의 첫 번째 인수인 a0 에서 지정된 속성을 찾습니다. 이름은 상수 0 으로 결정됩니다. LdaNamedProperty0을 사용하여 별도의 테이블에서 이름을 찾습니다.

- length: 1
           0: 0x2ddf8db91611 <String[1]: x>

여기서 0x 에 매핑됩니다. 따라서 이 바이트 코드는 obj.x 를 로드합니다.

값이 4인 피연산자는 무엇일까요? incrementX()피드백 벡터 로 불리는 인덱스입니다. 피드백 벡터는 성능 최적화에 사용되는 런타임 정보를 가지고 있습니다.

이제 레지스터는 다음과 같습니다.

Add r0, [6]

마지막 명령어는 r0 를 누산기에 추가하여, 결과적으로 43이 됩니다. 6 은 피드벡 백터의 또 다른 인덱스입니다.

Return

Return 은 누산기의 값을 반환합니다. incrementX() 의 마지막입니다. incrementX() 의 호출자는 누산기에서 43 을 가지고 추가 작업을 수행할 수 있습니다.

언뜻 보기에 V8 의 바이트 코드는 모든 추가 정보가 출력되어 암호처럼 보일 수 있습니다. 그러나 Ignition 이 누산기 레지스터가 있는 레지스터 시스템이라는 걸 알고나면 대부분의 바이트 코드가 수행하는 작업을 파악할 수 있습니다.

Last updated