[번역] The race for speed part 3: JavaScript compiler strategies

Posted by in Research

원문 출처 : http://creativejs.com/2013/06/the-race-for-speed-part-3-javascript-compiler-strategies/

 

 

JavaScript는 많은 이유로 큰 인기를 누리고 있다. 폭 넓은 사용 범위를 가지고 있고, 개발자의 관점에서 볼 때 아주 빠르고 유연하다.  언어의 모든 것이 객체여서 즉각적으로 구조를 만드는 것이 아주 쉽고 어떠한 데이터 타입도 필요 없다. 데이터 타입을 모두 추론하기 때문이다. 그러나 분명 이러한 다양성은 컴파일을 어렵게 만든다.

 

 

 

Hidden classes


비록 자바스크립트를 이용해서 객체와 계층 구조를 만드는 것은 아주 쉬운 일이지만, 컴파일러가 이러한 복잡한 구조를 다루기 위해서는 긴 시간이 필요하다. C 언어는 보통 프로퍼티나 변수를 저장하고 가져올 때 hashtable이나 사전식 데이터 구조를 이용한다. 이 둘은 이름을 식별하는 고유 문자열을 검색하여 프로퍼티의 값을 조회할 수 있는 유사배열 구조를 가지고 있다. 문제는 아주 큰 hashtable에서 프로퍼티를 찾을 때 속도가 느려진다는 점이다.

속도를 높이기 위해서 V8과 SpiderMonkey는 hidden class를 이용한다. Google은 이것을 maps라고 부르고 Mozilla는 shapes라고 부르는데 이 둘은 거의 같다. 이러한 구조는 일반적인 사전식 조회보다 훨씬 더 빠르게 프로퍼티를 찾아서 처리한다.

 

 

Type inference


자바스크립트의 동적 타이핑(Dynamic typing)은 같은 프로퍼티가 어떤 곳에서는 Number 타입이었다가, 다른 곳에서는 String 타입이 되는 것을 허용한다. 불운하게도 이런 다양성은 컴파일러가 더 많은 타입 검사 조건을 만들어야 함을 의미하고, 조건에 따라 타입을 구분해야하는 코드는 타입이 정해진 코드보다  훨씬 더 크고 느리다.

이 문제의 해결책을 타입 추론(type inference)이라고 부르며, 최근의 JavaScript 컴파일러는 이 방식을 이용하고 있다. 컴파일러는 코드를 점검한 후에 프로퍼티의 데이터 타입을 가정한다. 추론이 옳다면 빠르게 기계어 코드를 만드는 typed JIT을 실행한다. 기대했던 것과 타입이 다르다면 더 느린 조건문을 수행할 un-typed JIT으로 코드를 넘긴다.

 

 

Inline caches


최근 자바스크립트 컴파일러가 채택하고 있는 가장 일반적인 최적화 기법이 인라인 캐싱(Inline caching)이다. 이것은 새로운 기술이 아니다.(30년 전에 Smalltalk 컴파일러가 처음 구현하였다.) 그러나 아주 유용하다.

인라인 캐싱을 위해서, type inference와 hidden class이라는 두 가지 기술이 필요히디. 컴파일러가 새로운 객체를 만나면 그 객체의 hidden class를 추론한 데이터 타입에 따라 캐시한다. 같은 구조를 코드 내의 어디에서 다시 만나면 캐시해 놓은 버전을 가져와서 빠르게 비교한다. 비교 결과 일치하는 것으로 판단되면, 이전에 기계어 코드로 최적화 해 놓은 것을 재사용한다. 그러나 구조나 타입이 다르거나 변했다면, 일반 코드로 대체한다.

최근의 컴파일러는 이런 경우에 다형성 인라인 캐싱(polymorphic inlince caching)을 수행하기도 한다. – 즉 다시 말해서, 같은 구조에 대해서 각각의 데이터 타입 별로 추가적인 코드를 생성한다.

 

 

인라인 확장. 일명 인라이닝(inline expansion. aka “inlining”)

함수 호출은 계산적으로 비싸다. 이는 함수 호출이 어떤 종류의 조회를 요구하는데, 조회가 느린 작업이기 때문이다. 인라인 확장의 배경에 깔린 아이디어는 호출된 함수의 몸체를 가져와서, 그것을 호출한 코드에 가져다 놓는 것이다. 이렇게 하면 분기를 피할 수 있고 더 빠르게 코드를 처리할 수 있다. 하지만 추가적으로 메모리가 소비된다.

 

루프 불변 코드 이동. 일명 hoisting (loop-invariant code motion. aka “hoisting”)

루프는 최적화의 최우선 대상이다. 불필요한 루프 내의 계산을 제거하면 크게 성능을 향상 시킬 수 있다. 이러한 가장 일반적인 예는 배열을 순회하는 for 루프다. 배열을 순회하는 동안에 배열의 길이를 매번 계산하는 것은 불필요한 일이다. 길이를 미리 계산해서 변수에 넣어둠으로써 루프 밖으로 호이스팅 할 수 있다.

 

상수 폴딩(constant folding)

프로그램 전체에 걸쳐 변하지 않는 상수나 변수의 값을 미리 계산하는 방법.

 

공통 부분식 제거(common-subexpression elimination)

상수 폴딩과 유사하게, 이 최적화는 같은 값으로 평가되는 표현식에 대한 코드를 스캔하여 작동한다. 그러한 표현식은 미리 계산된 값을 가지고 있는 변수로 교체할 수 있다.

 

죽은 코드 제거(dead-code elimination)

죽은 코드는 사용하지 않거나 도달할 수 없는 코드를 말한다. 프로그램에 한 번도 호출하지 않는 함수가 있다면 최적화를 해도 소용이 없다. 사실 이러한 함수는 제거해도 안전하고, 또한 제거함으로써 프로그램의 전체 크기를 줄일 수 있다.

 

 

 

 

 

이러한 최적화 기법은 단지 JavaScript 코드를 네이티브 C 코드 만큼 빠르게 구현하고자 하는 야심찬 목표를 향한 시작일 뿐이다. 이게 가능할까? 이 시리즈의 마지막 포스트에서 빠른 자바스크립트의 최첨단에 있는 어떤 프로젝트에 대하여 알아보자.