페이지

2013년 7월 3일 수요일

asm.js는 무엇인가 그리고 무엇이 아닌가

* 이 글은 What asm.js is and what asm.js isn't을 번역한 내용입니다.

긴 글이 될 것이다. 한 줄 요약하자면 asm.js란 수년간 자연스러운 방식으로 개발되어온 자바스크립트의 특정 패턴을 형식화한 것이다. 새로운 VM이나 JIT 같은 것이 아니다.

asm.js는 자바스크립트의 부분집합이다. 주로 C나 C++ 같은 언어의 컴파일러 타겟으로 사용되며, 보다 쉽게 최적화되도록 하는 것을 목표로 정의되었다. 최근 온라인 상의 논쟁을 지켜보니 이 부분을 오해하는 사람들이 많은 것 같아서 글을 쓰게 되었다. asm.js에 대한 나의 견해와 함께 어느 정도 역사와 맥락을 설명하고자 한다.

먼저 언급해두고 싶은 것은, 어떤 것을 자바스크립트로 컴파일하는 게 전혀 새로운 일이 아니라는 점이다. 최소한 2006년에 이미 구글 웹 툴킷(GWT)을 통해 자바를 자바스크립트로 컴파일하는 게 가능했다. (GWT는 복잡한 클라이언트 앱을 작성하는 통합 툴킷으로 이 외에도 많은 기능이 있지만, 여기서는 컴파일러 쪽만 다루겠다.) 이후 C++이나 C# 같은 기존 언어들부터, 커피 스크립트, 타입 스크립트, 다트 등 새로운 언어들에 이르기까지 다양한 언어를 자바스크립트로 변환하는 다양한 컴파일러들이 등장했다.

컴파일된 코드(즉 컴파일러가 산출해낸 자바스크립트)는 좀 괴상하게 생겼다. 대개는 개발자가 직접 작성했을 법한 모양새가 아니다. 그리고 컴파일러마다 산출물에 나타나는 특정한 패턴이나 코드 스타일이 있다. 예컨대 다른 언어의 클래스를 자바스크립트의 클래스로 옮기면서 프로토타입 상속을 사용할 수도 있고 (이렇게 하면 좀더 '전형적인' 자바스크립트처럼 보일 것이다) 상속을 사용하지 않고 함수 호출로만 클래스를 구현할 수도 있고 (this를 일일이 인자로 전달하면서 말이다. '전형적인' 모습하곤 좀 거리가 멀겠지) 등등. 각 컴파일러의 고유한 동작 방식 때문에 산출물에도 특정한 패턴이 생기는 것이다.

서로 다른 자바스크립트 패턴은 엔진별로 실행속도가 천차만별이다. 당연한 일이다. 어떤 특정 벤치마크에서는 한 엔진이 다른 엔진들보다 빠를 수 있지만 일반론으로 말하자면 '가장 빠른' 자바스크립트 엔진이란 존재하지 않는다. 벤치마크마다 클래스 사용, 가비지 콜렉션, 정수 연산 등등 테스트 대상이 다르기 때문이다. 자바스크립트 엔진들을 비교해보면 어떤 엔진이 특정 부분에서는 다른 엔진보다 빨라도 다른 부분에서는 느릴 수 있다. 각각의 최적화가 다 별개의 작업이기 때문이다. 일례로 AWFY에서 개별 벤치마크 페이지를 보면 이해가 될 것이다. (링크를 타고 가서 'breakdown'을 클릭해보라.)

컴파일된 코드 역시 마찬가지다. 컴파일된 코드의 특정 패턴들은 자바스크립트 엔진별로 실행속도가 다르게 나온다. 예를 들어 GWT로 생성한 코드가 어떤 때는 파이어폭스보다 크롬에서 엄청 빠르다는 것을 최근에 알게 되었다. 링크 타고 가보면 버그 티켓을 볼 수 있을 것이다. 최근에 이 간극을 줄이는 작업을 했다. 이런 일은 하나도 이상할 게 없다. 특정 패턴의 코드에 대해서는 어떤 스크립트 엔진이 다른 엔진보다 빠르다. 다른 엔진이 이걸 따라잡기 위해 수정한다. 이런 식으로 일이 돌아가는 것이다.

GWT로 컴파일하는 자바 외에 (자바스크립트로 변환되는) 또다른 주요 언어는 C++이다. 최근 수년간, C++를 자바스크립트로 변환해주는 컴파일러를 적어도 두 개는 댈 수 있는데, 엠스크립튼맨드릴이다. 완전 별도의 프로젝트이고 실제로도 다른 점이 많다. 엠스크립튼은 오픈소스이고 자바스크립트만을 타겟으로 하는 데 비해, 맨드릴은 비공개 소스이고 플래시 등 다른 언어들도 타겟으로 하고 있다. - 그러나 맨드릴도 최근 몇년간은 동일하게 C++코드를 컴파일한 자바스크립트 패턴에 집중하고 있다. 싱글톤 타입 배열로 메모리를 가리키거나, C++의 정수처럼 동작하는 값을 얻어내기 위해(자바스크립트에는 double 값밖에 없으므로) |0 같은 비트 연산자를 사용하는 것 등이 이런 패턴에 해당된다.

말하자면 엠스크립튼과 맨드릴이 유용한 자바스크립트 패턴을 '발견했다'고 할 수 있다. 크록포드가 JSON을 발견한 것처럼 말이다. 물론 형고정 배열(typed array)이나 비트 연산자나 JSON 문법 모두 기존에 있던 것들이지만, 이것들을 아주 유용하게 사용할 수 있는 특정한 방식에 주목한 것은 의미있는 일이다. 지금은 데이터 교환 형식으로 JSON을 사용하는 건 너무나 지당한 일이고 마치 원래부터 이런 방식으로 사용하려고 고안된 것 같지만, 사실은 그렇지 않았다. C++를 자바스크립트로 변환하는 컴파일러를 만들 때, |0이나 싱글톤 타입 배열을 쓰는 것도 이제는 그 정도로 자연스러운 일이다.

엠스크립튼/맨드릴 패턴 류의 코드들은 수년간 자바스크립트 엔진이 발전해온 방향의 수혜자들이다. 예를 들어 자바스크립트 엔진들은 타입이 변경되지 않는 코드를 최적화하는 쪽으로 발달해왔는데, 엠스크립튼/맨드릴 패턴으로 생성한 코드들은 암묵적으로 정적 타입을 사용한다. 타입이 고정된 C++에서 유래한 코드이기 때문에 사실 타입이 변경돼서는 안되는 거지. 마찬가지로 엠스크립튼/맨드릴 패턴에서 중요하게 사용되는 비트 연산자 역시 썬스파이더 같은 주요 벤치마크의 크립토 테스트에서 사용되던 것들이다. 따라서 이미 최적화가 잘 되어있다.

달리 말하면, 엠스크립튼/맨드릴 패턴의 코드들은 자바스크립트 엔진이 이미 잘하고 있는 것에 초점을 맞추었기 때문에 빠른 속도를 성취할 수 있었다. 물론 이것은 우연의 일치가 아니다. 엠스크립튼과 맨드릴 모두 브라우저 테스트 결과에 기반하여 최대한 빨리 실행되는 코드를 생성하도록 방향을 잡았다. 그리하여 이 컴파일러들의 산출물이 웹에서 점점 더 많이 사용되었고, 브라우저들은 보다 구체적인 방식으로 이 코드들을 최적화해주었다. 구글은 옥테인 벤치마크(널리 알려진 V8 벤치마크의 후신)에 맨드릴 코드 벤치마크를 추가했다. 구글과 파이어폭스 모두 맨드릴과 엠스크립튼을 최적화하는 데 꽤 공을 들이고 있다. (이들의 버그 트래커를 잠깐만 검색해봐도 알 수 있다.) 즉 엠스크립튼/맨드릴 패턴은 자바스크립트 엔진들이 최적화 목표로 삼는 또 하나의 자바스크립트 패턴이 된 것이다. 썬스파이더, 옥테인, 크라켄 등등 유명한 벤치마크들과 어깨를 나란히 하면서 말이다. 이 모든 것이 아주 자연스러운 과정이었다.

이쯤에서 asm.js가 등장한다. 엠스크립튼/맨드릴 코드의 실행속도가 빠르긴 해도 네이티브 코드와는 역시 차이가 있었다. 간극이 크지는 않았다. 대부분의 경우에 네이티브 코드에 비해 3배 정도 느린, 말하자면 자바랑 거의 비슷한 수준이었다. 하지만 (고성능 게임과 같은) 일부 케이스에서는 여전히 문제가 되었다. 이 때문에 모질라는 이 간극을 줄일 방법을 연구하는 리서치 프로젝트를 시작했다. asm.js는 엠스크립튼/맨드릴 패턴을 타입 시스템으로서 공식적으로 정의할 목적으로 시작되었고 오픈소스로 개발되었다. 이 공식화 과정을 통해 우리는 모든 예외 케이스들을 생각해내고, 엠스크립튼/맨드릴 코드에서 (이 패턴이 목표로 하는 것과는 달리 실제로는) 타입이 런타임에 변경될 여지가 있는, 따라서 속도를 저하시킬 수 있는 곳들을 찾아냈다. asm.js의 목표는 이런 숨겨진 함정들을 다 찾아내 제거하는 것이었다.

첫번째 벤치마크 세트의 결과는 전망이 좋은 편이다. 보다 큰 벤치마크에서도 네이티브 코드와의 속도 차이가 2배 정도로 줄어들었다. 파이어폭스에서 이러한 속도 개선을 이루어내기까지 1명의 엔지니어가 겨우 3개월 작업했다. 새로운 VM이나 새로운 JIT를 개발한 게 아니라, 파이어폭스의 기존 자바스크립트 엔진에 추가적인 최적화 작업을 일부 더한 것뿐이기 때문이다. 얼마 지나지 않아 구글 역시 asm.js 코드에 상당한 수준의 속도향상을 보여 주었다. IO 키노트에서도 주목을 받았고 AWFY에서도 볼 수 있다. (사실 지난 며칠간만 해도 양쪽 브라우저 모두 괄목할 만한 속도향상을 이루어냈다. 우리가 떠드는 지금도 개선되고 있다!) 다시 말하지만 두 브라우저에서 이렇게 빠른 속도향상이 가능했던 것은 새로운 VM이나 JIT 없이 기존의 자바스크립트 VM에 최적화 작업이 추가되었기 때문이다. 따라서 asm.js를 "새로운 VM"이라고 말하는 것은 어불성설이다.

사람들이 asm.js를 새로운 '웹 기술'이라고 말하는 것도 보았다. 퉁쳐서 말하는 거라면야 이런 식의 표현도 가능하긴 하겠지만, 이건 'WebGL은 웹기술이다'라고 말할 때와는 완전 다른 의미이다. 사실 우리가 보통 '웹기술'이라고 말할 때는 WebGL같은 걸 가리킨다. 표준화된 기술이 존재하고, 브라우저가 이것을 지원하기로 결정하면, 이것을 구현하기 위한 작업이 필요하다. 반면 asm.js는 자바스크립트로 컴파일된 코드의 특정 패턴에 불과하며 자바스크립트는 이미 표준화되어있고 지원도 되고 있다. GWT 산출물과 마찬가지로 asm.js는 표준화할 필요가 없다. 브라우저의 '지원'도 필요없고, 브라우저가 asm.js를 실행하기 위해 어떤 최적화를 수행하든 그것 역시 표준화할 필요가 없다. 브라우저들은 모두 이미 자바스크립트를 지원하고 있으며 자바스크립트의 속도로 경쟁하고 있다. 따라서 asm.js도, GWT 산출물도, 커피스크립트 산출물도, 모두 브라우저에서 실행 가능하고 다양한 방식으로 최적화된다.

'지원한다'는 말을 다시 생각해보자. 앞서, 브라우저가 asm.js를 '지원한다'고 퉁쳐서 말할 수 있다고 했지만, 이 말이 무슨 뜻인지 확실히 할 필요가 있다. WebGL 같은 걸 말할 때, 예컨대 'IE10은 WebGL을 지원하지 않는다'고 말할 때는 의미가 확실하다. 하지만 '어떤 브라우저가 asm.js를 지원한다'고 말할 때의 의미는 아주 다르다. 내가 생각할 때 사람들이 이런 표현을 사용할 때는 '어떤 브라우저가 asm.js를 최적화하기 위해 별도의 작업을 했다'는 의미인 것 같다. 하지만 '이 브라우저는 GWT 산출물을 지원한다'고 말하는 사람은 하나도 못 봤다. 아무래도 '지원한다'는 표현을 쓰는 것 자체가, asm.js가 자바스크립트의 특정 패턴 이상이라는 잘못된 개념에서 비롯된 것 같다. 사실은 그렇지 않다.

그럼 도대체 왜 사람들은 아직도 asm.js가 자바스크립트의 특정 패턴만이 아니고 뭔가가 더 있을 거라고 생각하는 걸까? 확실치는 않지만 몇 가지 가능성을 짚어보고 그에 대해 해명해보겠다.

1. asm.js 코드는 기본적으로 저수준의 VM을 정의한다. 즉 '메모리'를 가리키는 싱글톤 배열이 존재하고 모든 작업이 여기서 이루어진다는 얘기인데, 이것은 사실이지만, 엠스크립튼과 맨드릴, 그리고 다른 컴파일러의 산출물 역시 마찬가지이기도 하다. 따라서 어떤 의미로는 맞는 말이지만, 그냥 일반적인 의미에서 그렇다는 얘기다. 자바스크립트로 (또는 튜링 연산이 가능한 어떤 언어로든) VM을 구현할 수 있다는 얘기지 asm.js와는 무관하다.

2. asm.js가 이루어낸 초기의 속도개선이 너무나 놀라운 수준이어서, 보통 일이 진전될 때의 점진적인 단계가 아니라 현재의 자바스크립트 속도에서 엄청난 도약을 한 것처럼 느껴졌던 것 같다. 그리고 이러한 커다란 도약은 보통 어떤 '새로운' 것, 크고 완전히 별개의 어떤 것을 필요로 한다. 하지만 앞서 말했듯이 이 발전은 실제로는 아주 조금씩 이루어졌다. 엠스크립튼/맨드릴 코드와 관련된 최적화 작업에는 수년이 걸렸고, 크랭크샤프트나 아이언멍키와 같이 정적단일할당(SSA) 방식으로 최적화하는 JIT를 작성하는 데도 수년이 걸렸다. (물론 겹치는 시기가 있었지만) 이러한 장기 프로젝트가 여기에 쉽게 최적화될 수 있는 코드 패턴을 만났을 때 정말 반짝반짝 빛을 발한 덕에 asm.js의 속도개선이 가능했다. 사실 asm.js 코드의 속도를 개선한 최근의 최적화 작업 대부분은, 뭔가를 직접적으로 최적화하는 게 아니라, 브라우저가 코드를 완전히 최적화하는 판단을 내리도록 변경한 것이었다. 즉 코드가 크랭크샤프트나 아이언멍키 등의 최적화 JIT에 실제로 닿을 수 있도록 해준 것이다. 최적화 JIT의 저력은 한참 전부터 존재했으나, 경험론(heuristics) 때문에 힘을 못 쓰고 있었던 것이다.

3. 비슷한 얘기로, asm.js 코드를 특정한 방식으로 최적화해주지 않는 브라우저에서는 asm.js가 '쓸 수 없을 정도로 느릴 것'이라고 말하는 사람들도 있다. (오늘만 해도 해커 뉴스에서 그런 댓글을 보았다.) 우선 이건 틀린 말이다. 이런 벤치마크들만 보더라도 대부분의 경우 파이어폭스와 크롬이 본질적으로 동일한 성능을 보여준다는 것이 명백하다. 차이가 있다 하더라도 눈에 띄게 줄어들고 있다. 말이 틀린 건 둘째치고, 구글, 마이크로소프트, 애플의 자바스크립트 VM 엔지니어들에게 실례되는 말이기도 하다. asm.js 코드가 파이어폭스 외의 브라우저에서 '쓸 수 없을 정도로 느리다'고 말하는 것은, 다른 VM의 개발자들은 동료 개발자들에 의해 이미 가능성이 입증된 수준의 성능에 도달하지 못할 것이라는 의미를 담고 있다. 그들이 얼마나 유능한 개발자들인지, 또 그 재능이 얼마나 많이 입증돼왔는지를 생각하면 정말 어처구니 없는 소리다.

만약 저들이 동일한 속도 개선을 '할 수 없어서'가 아니라 '하기 싫어서' 안 할 것이고 어쨌든 그래서 '쓸 수 없을 정도로 느릴 것'이라는 뜻이라면, 그 역시 명백히 틀린 말이다. 자바스크립트의 특정 유형이 현재 사용되고-에픽 시타델 같이 노는 물이 다른 애들까지 포함해서 말이다- 그와 관련된 벤치마크까지 존재하는데, 이에 대한 최적화를 하지 않겠다고 결정할 브라우저가 어디 있겠는가? 모든 브라우저가 모든 분야에서 빨라지려 하고 있고 이 분야는 경쟁이 엄청나다. 앞서 IO 키노트 얘기도 했지만 구글은 이미 asm.js 코드를 최적화하고 있으며 마이크로소프트에서 역시 긍정적인 신호를 보이고 있다.

4. 또다른 이유로 생각해볼 수 있는 것은 파이어폭스의 asm.js 최적화 모듈 이름이 오딘멍키라고 불린다는 사실이다. (이 블로그 포스트에서 언급되었다.) 파이어폭스의 자바스크립트 엔진인 스파이더몽키는 자신의 JIT들에 트레이스멍키, 재거멍키, 아이언멍키 등 멍키 류의 이름을 붙여왔다. (반면에 새로운 JIT인 베이스라인은 멍키 이름을 쓰지 않았다. 보시다시피 아무 일관성이 없다.) 그러니 오딘멍키라는 이름은 새로운 JIT구나, 그렇다면 결국 asm.js 최적화에 새로운 JIT가 필요한 거구나, 가 된 것 같다. 하지만 저 블로그 포스트에서도 언급했다시피 오딘멍키는 새로운 JIT가 아니라 자바스크립트 엔진에 추가된 모듈에 불과하다. 오딘멍키가 하는 일은 이렇다. asm.js 코드가 있는지 탐지한다. 일반적인 파서가 반환하는 파스 트리를 가지고 와서 타입 체크를 한다. 이 정보를 기존의 아이언멍키 최적화 컴파일러에 넘겨준다. 아이언멍키의 코드 최적화와 코드 생성 기능이 그대로 사용되며 asm.js를 위해 새로 만들어진 JIT는 전혀 없다. 앞서 말했듯이 이런 상황이었기 때문에 1명의 엔지니어가 3개월만에 (스펙 관련된 작업도 하면서 동시에) 오딘멍키를 만들 수 있었던 것이다. 새로운 JIT를 만들었다면 훨씬 더 많은 시간과 노력이 필요했을 것이다.

5. 또 오딘멍키가 코드를 타입체크할 것인지 판단할 때 "use asm"이라는 힌트를 사용하는 것도 생각해볼 수 있다. 실제로도 이건 좀 괴상한 측면이 있고, 어떤 사람들한테는 그르친 일처럼 느껴질 것이다. 나는 진짜 그 감정 이해한다. 사실 asm.js 설계 작업을 할 때 나는 반대했던 쪽이었다. 그런데 이렇게 안 하면 경험에 기반한(heuristic) 판단을 해야 한다. 이 코드 블럭에 asm.js에서 쓸 수 없는 것이 포함돼있지는 않은가? (throw문이 없다든지...) 싱글톤 타입 배열이 포함돼있는가? 뭐 이런 식으로. 판단 결과가 나오면 오딘멍키가 타입 체크를 시작하는 것이다. 내 생각에는 이렇게 해도 실질적으로는 비슷한 결과가 나올 것 같았다. 그렇긴 하지만 경험론이란 때때로 틀릴 때가 있고 또 종종 오버헤드를 발생시킨다. 그리고 이 최적화 작업이라는 게 임의의 코드에 '운좋게 실행되는' 게 아니라, C++를 자바스크립트로 변환하려는 개발자의 예측과 의도 하에 실행되는 것인데, 그 의도를 표기하는 일이 안될 게 뭐란 말인가. 이것이 명시적인 힌트를 사용할 것인가 말 것인가를 둘러싼 치열한 논쟁거리였다.

중요한 것은 "use asm"이라는 힌트가 JS의 구문론에 아무런 영향을 미치지 않는다는 사실이다. (만약 그런 경우가 있다면 -새로운 최적화는 가끔 특이한 예외상황에서 버그를 유발하곤 하니까- 다른 버그와 마찬가지로 고쳐져야 한다.) 즉 자바스크립트 엔진 입장에서는 무시하면 그만이다. 그리고 자바스크립트 엔진이 정말 이게 전혀 쓸모없다고 판단했다면, 우리는 그냥 코드 산출을 중단하면 그만이고.

6. asm.js는 스펙을 가지고 있고 타입 시스템을 정의한다. GWT가 산출하는 패턴 같은 것에 비하면 훨씬 '정형화'되어 보일 것이다. 더 나아가 실제로 스펙을 가진 것들, 즉 표준화가 완료되지 않은 IndexedDB나 WebGL 등등의 웹기술보다도 더 정형화되어 있다. 아마도 이 때문에 어떤 사람들에게는 asm.js가 자바스크립트 패턴 정도를 넘어서 WebGL과 더 비슷하게 보이는 것 같다. 하지만 스펙이란 게 항상 표준화를 목적으로 존재하는 것은 아니다. 앞서 언급했듯이 스펙과 타입 시스템을 작성한 이유 중 하나는, 이를 통해 모든 세부사항을 다 쥐어짜내서 타입이 변경될 가능성과 최적화를 저해할 여지를 제거하기 위해서였다. 내 생각에 굳이 스펙을 작성한 데는 네 가지 정도의 이유가 있다.

* 스펙이 있으면 커뮤니케이션하기가 좀더 쉬워진다. 앞서 말했듯이 GWT 산출물은 종종 파이어폭스보다 크롬에서 빨리 돌아가는데 (아마 지금도 그럴 것이다) 난 사실 이유를 잘 모르겠다. (아마 여러 가지 이유가 있을 것이다.) GWT와 크롬은 같은 회사 프로젝트니까 그 개발자들이 사적으로 만나서 얘기를 나눈다해도 놀랄 일이 아니고 잘못도 아니다. GWT와 크로미움처럼, 엠스크립튼과 파이어폭스 역시 단일 브라우저 벤더가 주축이 되어 개발하는 오픈소스 프로젝트들이고, 비슷한 상황이 여기에도 존재한다. 사적인 논의의 한계점을 비켜가기 위해 스펙을 작성하면 좋을 것 같다고 생각했다. 즉 스펙이 있으면 '이러한 이유(와 온갖 기술적 세부사항들) 때문에 이 코드가 파이어폭스에서 빠르게 돌아가는 겁니다'라고 말할 수 있게 된다. 만약 다른 브라우저 벤더들이 그 코드를 똑같이 빠르게 실행시키고 싶다면 관련된 모든 문서를 다 볼 수 있다. 또 맨드릴 같은 컴파일러들이 이 속도개선의 혜택을 활용하고 싶다면, 그들도 역시 모든 정보를 얻어갈 수 있다. 스펙을 작성하고 공개함으로써 공적으로 투명하게 운영되는 것이다.

* 타입 시스템을 정의함으로써 보다 합리적으로 사전(ahead of time) 컴파일 할 수 있는 가능성이 열렸다. 사실 엠스크립튼과 맨드릴의 산출물 패턴은 옛날에 이미 사전 컴파일이 가능했어야 했다. (사전 컴파일되는 C++로부터 생성되는 코드라는 점을 유념하시라) 하지만 그러지 못했던 이유는 타입이 실제로는 변경될 가능성이 몇 군데 있었기 때문이다. 우리가 일을 제대로 했다면, asm.js에는 더 이상 타입이 변경될 가능성이 없다. 따라서 사전 컴파일이 가능하다. 타입 시스템 덕에 이것이 가능한 수준에 그치지 않고 실제로 말이 되는 얘기가 되었다.

사전 컴파일은 오버헤드와 최적화 경험론의 위험요소를 줄이는 데 대단히 유용하다. 앞서도 말했지만, 경험론이 제대로 된 판단을 내리지 못하면 코드가 최적화 JIT에 아예 닿지도 못할 수가 있다. 더군다나 경험론을 위한 데이터(함수 또는 루프의 실행시간, 변수의 타입 정보 등등)를 수집하는 과정에서 오버헤드가 추가된다. (전체 최적화 전의 준비운동 단계에서, 그리고 이후에 역-최적화 및 재컴파일이 필요하게 되면 그 단계에서 한번 더.) 만약 코드를 사전에 컴파일할 수 있다면, 이 두 가지 문제가 모두 간단히 해결된다. 그냥 전체를 최적화하면 되고, 바로 하면 되는 것이다. (하지만 여기에도 걱정되는 면이 있다. 많은 양의 코드를 한꺼번에 최적화하는 데 상당한 시간이 소요될 수 있기 때문이다. 파이어폭스에서는 이를 일부 완화하기 위해 멀티 코어로 컴파일을 실행하거나, 컴파일 속도가 느린 특정 함수는 베이스라인 JIT로 폴백하는 안 등을 고려하고 있다.)

사전 컴파일을 잘 활용하면, '버벅이' 문제도 해결할 수 있다. (새로운 무기나 효과를 쓰는 새로운 레벨이 시작되어) 새로운 코드가 실행될 때, 일부 코드가 문제성으로 진단되어 최적화가 진행되면서 게임이 막 빨라져야 하는 바로 그 시점에 게임이 멈추거나 잠깐 느려지는 현상 말이다. 버벅이가 문제가 되는 이유는 뭔가 흥미로운 일이 생기는 바로 그 순간, 즉 최악의 타이밍에 발생하기 때문이다. 게임 개발자들이 이 문제로 골치썩는 얘기를 많이 들었다. 사전 컴파일은 모든 코드를 사전에 완전히 최적화하기 때문에 버벅이 문제를 비켜간다.

또 사전 컴파일을 통해 성능 예측이 가능해진다. 모든 코드가 완전히 최적화될 거라는 걸 알기 때문에, 한번도 테스트해본 적 없는 새로운 벤치마크에서의 성능에 대해서도 타당한 수준으로 확신을 가질 수 있다. 무엇을 언제 최적화할지 경험론에 의존하지 않기 때문이다. 파이어폭스의 사전 컴파일 기능을 에픽 시타델이나 Lua를 자바스크립트로 컴파일하는 Lua VM 등의 새로운 코드베이스나 벤치마크에 최초로 실행시킬 때마다 훌륭한 성능을 성취함으로써 이 부분은 이미 반복적으로 입증이 되었다. 자바스크립트 벤치마킹과 관련된 개인적인 경험상 과거에는 이런 일이 거의 없었다.

사전 컴파일에 이런 장점들이 있지만, 이것은 구현상의 세부사안이고 여러 접근방법 중 하나일 뿐이다. 자바스크립트 엔진들이 이런 류의 코드를 더 잘 실행시키기 위한 방법들을 계속해서 탐구하고 있기 때문에, 앞으로 우리는 이 영역에서 더 많은 실험적 시도들을 보게 될 것이다.

* 스펙과 타입 시스템을 작성한 이유 중 마지막은 asm.js가 리서치 프로젝트로 시작되었고 어느 시점엔가는 논문을 낼 수도 있기 때문이다. (다른 이유들에 비하면 훨씬 덜 중요하긴 하지만, 다른 이유들도 썼으니 완성하는 의미로 썼다.)

여기까지가 asm.js는 왜 스펙과 타입 시스템을 작성했는지에 대한 설명이고, 여섯번째 이유에 대한 답이었다. 사람들이 asm.js를 새로운 VM 비슷한 것으로 오해하는 이유로 남은 두 가지는 다음과 같다.

7. Math.imul 때문이다. Math.imul은 asm.js 설계과정에서 나온 것으로, 실제로 ES6 표준화 과정에 제안되었고 현재 파이어폭스와 크롬에는 구현되었다. 어떤 사람들은 asm.js가 Math.imul에 의존하고 있다고 생각하는 것 같다. 즉 자바스크립트의 새로운 언어적 특질에 의존하고 있다는 뜻이 된다. Math.imul이 유용하긴 해도 이건 전적으로 선택 사항이다. 벤치마크에 미치는 영향은 지극히 미미하며, (엠스크립튼도 했듯이) 폴리필도 엄청 쉽다. 어쩌면 그냥 폴리필 코드를 쓰고 표준에는 제안하지 않는 게 혼란의 여지를 줄이고 훨씬 간단했을 지도 모르겠다. 하지만 Math.imul은 정말 간단하게 정의할 수 있다. 그리고 자바스크립트에 자연스러운 방식으로는 구현할 수 없는 유일한 정수 연산이기도 하다. (여기서 주목할 만한 사실은 자바스크립트에 integer라는 타입이 없는데도 다른 것들은 자연스러운 방식으로 표현할 수 있다는 점이다.) 표준에 제안 안 하는 게 이상하게 느껴졌다.

만약 자바스크립트 커뮤니티가 Math.imul에 반대했다면 당연히 asm.js에서도 사용하지 않았을 거라는 점을 강조하고 싶다. asm.js는 자바스크립트의 부분집합이며 표준에 부합하지 않는 것은 사용할 수 없다. 물론 두말 할 것도 없이 자바스크립트에 새로운 표준의 도입이 논의되는 것은 늘상 있는 일이다. C++를 컴파일한 코드에서 유용하게 쓸 수 있는 것이라면 자바스크립트 커뮤니티와 표준안에서 다뤄볼 만한 가치가 있고 말이다.

8. 마지막으로, asm.js와 구글의 PNaCL을 보자. 둘 다 C++ 코드가 웹상에서 네이티브에 가까운 속도로 안전하게 실행될 수 있도록 하는 것을 목표로 한다. 그러니 사람들이 이 둘을 비교하는 블로그 포스트를 써왔다 해도 놀랄 일이 아니다. 그 외에도 LLVM을 특정한 방식으로 활용한다든가 하는 유사점들이 있다. 하지만 이 둘이 마치 경쟁하는 프로덕트인 것처럼, 즉 한쪽의 승리가 다른 쪽의 패배인 것처럼 비교하는 것은, 내 생각엔 부적절하다. PNaCl(과 PNaCl의 기반이 되는 PPAPI)은 초안 수준의 웹 기술로, 웹을 이루는 구성요소가 되기까지는 수많은 벤더들의 지원 속에서 표준화가 이루어져야 한다. 반면 asm.js는 자바스크립트의 패턴에 불과하며 모든 모던 브라우저에서 이미 지원되고 있다. asm.js 코드는 브라우저의 속도 경쟁에 힘입어 지금 이 순간에도 최적화되고 있다. 다른 수많은 자바스크립트 패턴과 마찬가지로 말이다. 따라서 asm.js는 PNaCl 등의 진영에 어떤 일이 생기든 상관없이 앞으로도 한동안 속도 우위를 유지할 것이다. PNaCl이 브라우저 벤더들의 자바스크립트 속도 경쟁을 중단시킬 수 있는 현실적인 시나리오는 어떻게 해서도 떠오르지 않는다. 플래시, 자바, 실버라이트, 유니티 등등 다른 플러그인 기술들도 그렇게 하지 못했다.

이 모든 것은 PNaCl이 얼마나 훌륭한가와는 아무 상관이 없다. (나는 엔지니어링 기술의 인상적인 위업이라 생각하고, 관련된 개발자들에게 어마어마한 존경심을 갖고 있다는 점을 언급해두고 싶다.) 동작하는 영역이 다를 뿐이다. 브라우저 시장에서 자바스크립트의 속도경쟁은 현재 대단히 중요하다. 당신이 개발하는 브라우저가 다른 브라우저보다 어떤 면에서 느리다면, 당신은 당연히 그걸 개선해야 한다. 이런 상황이 벤치마크들 사이에서도 일어나고 (asm.js 코드가 두 브라우저에서 큰 속도개선을 이루어낸 점 이미 얘기했다) 에픽 시타델 데모(애초에 크롬에서는 전혀 실행되지 않았었는데 구글은 재빠르게 문제를 해결했다.) 같은 것에서도 볼 수 있다. 이런 일은 웹상에서 앞으로도 계속될 것이고 asm.js의 성능을 돌진시킬 것이다. 앞서 말했듯이 PNaCl에서 생기는 일과는 아무 상관 없이 말이다.

정말 그 정도로 간단한 일인가?

asm.js는 자바스크립트의 한 패턴일 뿐 그 이상의 무엇이 전혀 아니며, asm.js의 속도개선은 자바스크립트 속도 경쟁으로 인해 브라우저 벤더들이 다양한 패턴을 최적화한 결과에 빚지고 있다고 말했다. 사실 상황은 이것보다 좀더 복잡하다. 어떤 패턴이라도 최적화되어야만 한다든가, 어떤 벤더가 자바스크립트의 임의의 부분집합에 대해 힌트에 기반한 한정화된 최적화를 해도 괜찮다고 주장하려는 것은 아니다.

이해를 돕기 위해 극단적인 가설로 예를 들어보겠다. 어떤 브라우저가 자바스크립트의 작은 부분집합에 대해 최적화하기로 결정했다. 자바스크립트 배열을 메모리로 사용하고, 정수 또는 문자열을 여기에 저장한다.

function WTF(print) {
  'use WTF';
  var mem = [];
  function run(arg) {
    mem[20] = arg;
    var a = 100;
    mem[5] = a;
    a = 'hello';
    mem[11] = a;
    print(mem[5]);
    mem[5] = mem[11];
    a = arg;
    return a;
  }
  return run;
}

이것을 WTF.js라고 부르자. 'use WTF'이 존재하면 브라우저가 mem 이라는 싱글톤 배열이 있는지 등등을 확인한 후 WTF.js 코드를 엄청 최적화해주는 것이다. mem은 클로저 내에 존재하고 이스케이프하지 않기 때문에 우리는 정적으로 이것을 식별해낼 수 있다. 또한 인덱스가 중간에 빌 가능성이 있으니 해시테이블로 구현되어야 할 것 같다. 또 정수와 문자열만을 담게 되어 있으니까, 초-특화된 커스텀 데이터 구조가 있다면 유용할 것 같다 등등. 다시 말하지만 이건 극단적이고 우스꽝스럽게 만들어진 예시일 뿐이다. 하지만 이런 패턴이 어떤 특이한 언어의 컴파일 타겟으로는 유용하게 쓰일 수도 있음을 이해할 수 있을 것이다.

WTF.js의 문제는 이것이 자바스크립트로서는 폭망 수준이라는 데 있다. 구체적으로 말하자면, 최신 자바스크립트 엔진들의 최적화 방법론 기본 규칙을 위반한다. 일단 변수들의 타입이 바뀐다. (지역변수도 그렇고 mem의 원소들도 그렇고) 자바스크립트 엔진 개발자들이 몇 년에 걸쳐 하지 말라고 말해온 게 정확히 이런 것이다. 그리고 무엇의 부분집합이라는 건지 근본이 없다. 바로 전 문단에서 가설로라도 어떤 정당화를 해보려고 했지만 아무 것도 생각해낼 수가 없었다. 이 코드는 개선의 여지가 너무나 명백하다. 예컨대 mem 배열이 0에서부터 순차적인 인덱스를 사용하여 구멍이 뚫리지 않도록 한다든지 -자바스크립트 엔진 개발자들이 진짜 옛날부터 말해왔던 것- 등등. WTF.js는 진짜 WTF 수준이다.

asm.js에서는 이런 일이 일어나지 않았다. 앞서 상술한 것처럼 자바스크립트의 세계에서 수년간 계속되어 온 자연스러운 과정을 최신판으로 발전시킨 것이다. C++를 자바스크립트로 변환하는 다수의 컴파일러들이 독자적으로 등장했고, 이후 자신들이 목표로하는 자바스크립트의 특정 부분집합에 집중하기 시작했으며, 이 패턴이 웹에서 점점 더 널리 사용됨에 따라 자바스크립트 엔진들이 이 부분집합을 최적화하기에 이르렀다. |0이 좀 이상하게 보일지는 몰라도 asm.js를 위해 발명된 것은 아니다. asm.js 한참 이전에 여러 번 따로따로 "발견"되었고, 유용성이 드러나 자바스크립트 엔진들이 최적화해주었다. 누구 한 사람에 의해 주도된 것이 아니라 유기적으로 진행된 과정었다.

우리는 asm.js의 초기버전을 설계하는 동안 이 규칙을 따랐다. 최적화가 더 잘 될 수 있는 코드 패턴이 있는데, 희한하기도 하고 때로는 기괴했으며, 무엇보다도 특수한 최적화를 거치지 않으면 성능이 형편없는데 그런 최적화는 아직 존재하지 않았다. 제안된 패턴의 특성을 이런 식으로 이해했을 때 나는 강하게 반대했다. 엠스크립튼과 맨드릴만 해도 이미 꽤 잘 돌아가고 있지 않은가. 특수한 최적화를 해주면 훨씬 빠르게 실행되지만 그렇게 할 수 없는 지금으로서는 엄청나게 느린 새로운 패턴을 설계하는 건 좋지 않은 생각이다. 부적절하고 WTF같은 느낌이다.

그래서 asm.js를 설계할 때, 우리는 아무런 특수한 최적화 없이 자바스크립트 엔진 위에서 테스트했다. 주로 크롬과 파이어폭스에서 WIP 최적화를 끈 상태로. asm.js가 엠스크립튼의 산출물 모드 중 하나이기 때문에, 비교대상으로서 좋은 기반을 가진 셈이었다. 스위치를 끄고 asm.js를 기존의 엠스크립튼 산출물 패턴으로 되돌렸을 때 더 빨라지는가 느려지는가? 우리가 보았을 때 (더 참신한 아이디어들도 많았지만 이런 것들을 다 쳐내고 나자) 대체로 스위치를 껐을 때의 영향은 미미했다. 가끔은 더 좋아지고 가끔은 더 나빠졌지만, 대체로 자바스크립트 엔진이 이미 가지고 있는 꽤 비슷한 (꽤 훌륭한) 성능 수준을 유지했다. asm.js는 기존의 패턴과 상당히 닮았기 때문에 말이 되는 얘기다. (아마 성능이 개선되었다면 asm.js가 런타임 타입 변경을 극도로 조심한 결과가 긍정적으로 나타난 경우이고, 성능이 저하되었다면 자바스크립트 엔진의 경험론이 운 나쁜 쪽으로 사용된 경우일 것이라고 짐작해본다.)

몇달 전 엠스크립튼의 기본 코드 생성 모드가 asm.js로 변경되었다. 다른 중대한 변경사항들과 마찬가지로 메일링 리스트와 irc에서 논의가 이루어졌고, 다른 브라우저에서 이 변경으로 인한 속도저하가 보고되지 않았다는 점을 기쁘게 생각한다. 우리가 그 부분을 제대로 해냈다는 또다른 증거니까. 실은 (대규모 프로젝트에서 중대한 변경 이후 발생하는 일반적인 소소한 버그들 말고) 주요 리그레션 테스트 결과 파이어폭스에서 초기화시점에 속도저하가 발생된다는 보고를 받았었다! (사전 컴파일이 가끔 둔해지면서 생긴 문제로, 그 버그 리포트 이후에는 뭐 금방 해결돼서 속도가 크게 개선되었다.)

WTF.js 같은 기괴한 놈에 대한 최적화를 개발하고 탑재하는 건 분명 이기적인 행위다. 어떤 벤더 하나가 독단적으로 이득을 취하는 동안 다른 벤더의 브라우저들에는 예기치 못한 불필요한 성능상의 손실을 야기하기 때문이다. asm.js는 이런 얘기들과 거리가 멀다. 자연스럽게 나타난 자바스크립트 패턴, 즉 이미 자바스크립트 엔진들이 최적화한 엠스크립튼/맨드릴 패턴 위에 구축되었고, 기존 패턴과 비교했을 때 다른 브라우저에 성능상의 손실을 끼치지 않도록 설계되었으며, 두 브라우저의 벤치마크가 보여주고 있듯이 웹상에 비약적인 속도개선의 기회를 이끌어냈기 때문이다.

댓글 없음:

댓글 쓰기