자바스크립트

[번역] 저스트-인-타임(JIT) 컴파일러 집중 코스

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 두 번째 글인 A crash course in just-in-time (JIT) compilers의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스 (현재글)
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

자바스크립트는 태생이 느렸지만, JIT라고 불리는 무언가 덕분에 빨라지게 되었다. 그런데 JIT는 어떻게 동작하는걸까?

자바스크립트는 브라우저에서 어떻게 실행될까

개발자로서 자바스크립트를 페이지에 추가하면, 당신은 목표와 문제를 갖게 된다.

목표 : 당신은 컴퓨터에게 무엇을 할지 말하고 싶다.

문제 : 당신과 컴퓨터는 다른 언어를 사용해서 이야기한다.

당신은 인간의 언어를 사용하고, 컴퓨터는 기계의 언어를 사용한다. 비록 당신이 자바스크립트나 다른 상위레벨 프로그래밍 언어들이 인간의 언어가 아니라고 생각할 지라도, 이들은 실제로 인간의 언어이다. 이들 언어는 기계가 아닌 인간이 이해하기 위해 디자인되었다.

그러므로 자바스크립트 엔진의 임무는 당신이 사용하는 인간 언어를 기계가 이해할 수 있는 무언가로 변경시키는 것이다.

나는 이 상황이 영화 Arrival 에서 인간과 외계인이 서로 대화하려고 하는 장면과 비슷하다고 생각한다.

02-01-alien03-500x286.png

이 영화에서 인간과 외계인은 단지 단어를 단어로 옮기는 식으로 번역하지 않는다. 두 집단은 세상에 대해 생각하는 방식이 다르다. 그리고 이는 인간과 기계 사이에서도 마찬가지이다. (여기에 대해서는 다음 글에서 설명하겠다.)

그러면 어떤 식으로 번역이 이루어질까?

프로그래밍에서 기계 언어로 번역하는 방식에는 일반적으로 2가지가 있다. 바로, 인터프리터와 컴파일러다.

인터프리터를 사용하면, 실시간으로 한줄, 한줄 번역을 진행한다.

02-02-interp02-500x291.png

반면, 컴파일러는 실시간으로 번역하지 않는다. 컴파일러는 미리 작동해서 번역 내용을 생성하고 그것을 적어둔다.

02-03-compile02-500x297.png

이들이 번역하는 방식에는 각각 장점과 단점이 있다.

인터프리터의 장점과 단점

인터프리터는 빠르게 준비해서 실행할 수 있다. 코드를 실행하기 위해 전체 코드를 컴파일하는 단계를 거칠 필요가 없다. 첫번째 줄을 번역하면 바로 실행할 수 있다. 

이 덕분에, 인터프리터는 자바스크립트 같은 언어에 잘 어울리는 것 같다. 웹개발자에게는 시작하자마자 빠르게 코드가 실행되도록 하는 것이 중요하다.

초기 브라우저들이 자바스크립트 인터프리터를 사용한 것은 이러한 이유 때문이다.

동일한 코드를 한번 이상 실행하게 되면 인터프리터의 단점이 발생하기 시작한다. 예를 들어 반복문 내부에 있다고 가정해보자. 그러면 동일한 번역 작업을 계속해서 반복해야만 할 것이다.

컴파일러의 장점과 단점

컴파일러는 정반대의 트레이드-오프를 갖는다.

초기 구동 시간은 조금더 오래 걸리는데, 왜냐하면 처음부터 모든 컴파일 과정을 진행해야 하기 때문이다. 하지만 이후에 코드가 반복해서 실행되면 더 빠른데, 왜냐하면 루프가 진행되는 동안 동일한 번역을 반복할 필요가 없기 때문이다. 

또다른 차이점은 컴파일러가 코드를 좀더 빠르게 실행하기 위해 전체 코드를 둘러보고 편집을 하는 시간이 더 소요된다는 점이다. 이런 편집을 최적화라고 부른다. 

인터프리터는 런타임에 동작하기 때문에, 번역하는 동안에 이러한 최적화를 위해 많은 시간을 소요할 수가 없다.

저스트-인-타임 컴파일러 : 두 세상의 최선

인터프리터의 비효율성 반복문에서 매번 같은 코드를 다시 번역해야 하는 문제 을 제거하기 위해 브라우저는 인터프리터에 컴파일러를 혼합하기 시작했다.

브라우저 마다 방식이 조금씩 다르긴 하지만, 기본 개념은 동일하다. 이들은 자바스크립트 엔진에 모니터(혹은 프로파일러)라고 불리는 부분을 추가했다. 이 모니터는 코드가 실행되는 것을 관찰하면서 해당 코드가 얼마나 많이 실행되고, 어떤 타입이 사용되는지를 기록한다.

먼저, 모니터는 인터프리터를 통해서 모든 것을 실행한다.

02-04-jit02-500x365.png

만약 같은 줄의 코드가 적은 횟수로만 실행되면, 그 코드는 따뜻하다(warm)라고 칭한다. 만약 많이 실행된다면 뜨겁다(hot)라고 칭한다.

기본(Baseline) 컴파일러

어떤 함수가 따뜻해지기(warm) 시작하면 JIT는 그 함수를 컴파일 하기 위해 전송한다. 그 이후에 JIT는 컴파일된 정보를 저장한다.

02-05-jit06-500x368.png

함수의 각 줄은 스텁”으로 컴파일된다. 스텁은 줄번호와 변수 타입에 의해 색인(Index)된다 (이 과정이 왜 중요한지는 나중에 설명하겠다). 만약 동일한 코드가 동일한 변수 타입에 의해 실행되는 것을 모니터가 발견하게 되면, 해당 코드의 컴파일된 버전을 내보낸다.

이 과정은 속도를 향상시켜 준다. 하지만 내가 말했듯이, 컴파일러는 더 많은 일을 할 수 있다. 컴파일러는 최적화를 하기 위해 시간을 들여서 가장 효율적인 방법을 찾아낼 수 있다.

기본 컴파일러가 이러한 최적화의 일부를 담당한다 (아래에서 예제를 하나 보여주겠다). 기본 컴파일러는 너무 많은 시간이 소요되는 것을 원치 않는데, 왜냐하면 실행 시간을 너무 오랫동안 멈추고 싶지 않기 때문이다.

하지만, 만약 코드가 정말로 뜨겁다면(hot) 코드가 아주 많은 횟수로 실행된다면 최적화를 더 하기 위해 추가적인 시간을 들일만한 가치가 있을 것이다.

최적화 컴파일러

코드의 한 부분이 아주 뜨거울(hot) 때, 모니터는 그 부분을 최적화 컴파일러에게 전송한다. 이 컴파일러는 또 다른 더욱 빠른 버전의 함수를 생성하며, 이 함수 역시 저장된다.

02-06-jit09-500x365.png

더 빠른 버전의 코드를 생성하기 위해, 최적화 컴파일러는 몇가지 가정을 해야한다.

예를 들어, 만약 특정한 생성자로부터 만들어지는 모든 객체들이 같은 모양을 갖는다고 가정할 수 있다면 즉 항상 같은 프라퍼티명을 가지며 이들 프라퍼티들이 같은 순서로 추가된다면 이를 이용해서 몇가지 최적화를 할 수 있을 것이다. 

최적화 컴파일러는 이러한 판단을 하기 위해 모니터가 코드의 실행을 지켜보며 수집한 정보들을 사용한다. 이전에 반복문을 순회하는 동안 항상 참이었던 값이 있다면, 그 값이 계속해서 참일 거라고 가정할 수 있을 것이다.

하지만 물론 자바스크립트에서는 어떤 것도 보장할 수 있다. 객체가 99번동안 항상 같은 모양을 하고 있더라도, 100번째에는 프라퍼티 하나가 빠져있을 수도 있다. 

그러므로 컴파일된 코드는 실행하기 전에 가정이 유효한지를 확인해야만 한다. 유효하다면, 컴파일러는 코드를 실행한다. 하지만 유효하지 않다면 JIT는 가정이 잘못되었다고 판단하고 최적화된 코드를 버린다.

02-07-jit11-500x361.png

그러면 실행은 다시 인터프리터 혹은 기본 컴파일된 버전으로 돌아간다. 이 과정은 역최적화(deoptimization) 혹은 구제(bailing out)라고 한다. 

최적화 컴파일러는 주로 코드를 빠르게 만들지만, 가끔 예상하지 못한 성능 문제를 야기하기도 한다. 만약 코드가 계속해서 최적화되고 역최적화된다면, 단순히 기본 컴파일된 버전을 실행시키는 것 보다 느려지게 될 것이다.

대부분의 브라우저들은 이러한 최적화/역최적화 싸이클이 발생했을 때 중지할 수 있는 제한을 갖고 있다. 예를들어 만약 JIT가 10번의 이상의 최적화를 시도하고 계속해서 그 코드를 버린다면, 더이상 최적화를 그만 시도하게 될 것이다.

최적화 예제 : 타입 특수화

최적화에는 다양한 종류가 있는데, 어떻게 최적화가 발생하는지를 보여주기 위해 한가지 형태를 살펴보도록 하겠다. 최적화 컴파일러의 가장 중요한 성과들 중 하나는 타입 특수화라고 불리는 것에서 얻어진다.

자바스크립트가 사용하는 동적 타입 시스템은 실행시점에 약간의 추가 작업이 요구된다. 예를 들어 다음의 코드를 살펴보자.

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

위의 반복문 내부에 있는 += 연산은 간단해 보일 수도 있다. 이 연산은 한번에 할 수 있을것 처럼 보이지만, 동적 타입 때문에 예상했던 것 보다 더 많은 단계가 필요하다.

arr이 100개의 정수를 갖는 배열이라고 가정해보자. 코드가 사용되기 시작하면 기본 컴파일러는 함수안에 있는 각각의 연산에 대한 스텁을 생성할 것이다. 그래서 sum += arr[i]에 대한 스텁이 생성되며, 이 스텁이 += 연산을 정수 연산으로써 처리하게 된다.

하지만 sumarr[i]이 정수라고 보장할 수는 없다. 자바스크립트에서는 타입이 동적이기 때문에, 루프를 다음번에 순회할 때에는 arr[i]가 문자열일 수도 있다. 정수를 더하는 것과 문자열을 연결하는 것은 아주 다른 두 개의 연산이므로, 둘은 아주 다른 기계 코드로 컴파일될 것이다.

JIT는 여러개의 기본 스텁을 컴파일함으로써 이런 상황을 처리한다. 만약 코드 조각이 동일한 구조라면 (즉, 항상 같은 타입들로 호출된다면) 하나의 스텁만 가질 것이다. 만약 코드 조각이 다형 구조라면 (코드 이곳 저곳에서 각각 다른 타입으로 호출된다면), 해당 연산이 실행되는 각각의 타입 조합 만큼 스텁을 갖게 될 것이다.

이 말은 JIT가 스텁을 선택하기 전에 많은 질문을 던져야 한다는 의미이다.

02-08-decision_tree01-500x257.png

각 라인별 코드들이 각자의 스텁 묶음을 기본 컴파일러 내부에 갖고 있기 때문에, JIT는 각 라인의 코드가 실행될 때마다 타입을 확인해야만 한다. 매번 루프를 순회할 때마다, JIT는 같은 질문을 해야만 한다.

02-09-jit_loop02-500x323.png

만약 JIT가 이렇게 반복해서 확인할 필요가 없다면, 코드 실행이 훨씬 빨라질 수 있을 것이다. 이것이 바로 최적화 컴파일러가 하는 일 중 하나이다. 

최적화 컴파일러에서는 전체 함수가 함께 컴파일된다. 타입 확인은 루프 이전에 발생할 수 있도록 이동된다.02-10-jit_loop02-500x318.png

어떤 JIT는 여기서 더 최적화하기도 한다. 예를 들어, 파이어폭스에는 오직 정수만을 갖는 배열들을 위한 특수한 분류가 있다. 만약 arr이 이러한 배열들 중 하나라면, JIT는 arr[i]가 정수인지 확인할 필요가 없게 된다. 이는 JIT가 루프에 진입하기 전에 모든 타입을 확인할 수 있다는 의미이다.

결론

지금까지 JIT에 대해 간략하게 살펴보았다. JIT는 코드가 실행되는 것을 관찰하여 자주 실행되는 코드가 최적화될 수 있도록 전송하여 자바스크립트를 더 빠르게 실행되도록 만든다. 이는 대부분의 자바스크립트 어플리케이션에의 성능을 몇배로 향상시켰다.

하지만 이러한 향상에도 불구하고, 자바스크립트의 성능은 예측하기 어려울 수 있다. 또한 코드를 빠르게 만들기 위해서 JIT는 실행시점에 몇가지 오버헤드를 발생시키는데, 이는 다음을 포함한다:

  • 최적화와 역최적화
  • 모니터의 기록 영역과 코드가 버려질 때 복구를 위한 정보가 사용하는 메모리
  • 기본 버전과 최적화 버전의 함수를 저장하기 위한 메모리

여기에 좀더 개선할 여지가 있다: 이러한 오버헤드는 제거될 수 있다면, 성능이 좀더 예측 가능해진다. 이것이 바로 웹어셈블리가 하는 일 중의 하나이다.

다음 글에서는 어셈블리에 대해, 그리고 컴파일러가 어떤 식으로 함께 동작하는지에 대해서 설명하도록 하겠다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s