WebAssembly는 웹을 위한 새로운 바이너리 포맷입니다. WebAssembly가 새로운 안정화 버전 브라우저에 탑재되기 시작했습니다. WebAssembly의 주요 목표는 속도입니다. 이 글을 통해 WebAssembly가 어떻게 실행속도를 개선했는지 설명하려고 합니다.
“속도”는 상대적인 이야기입니다. JavaScript 같은 다이나믹 랭귀지와 비교하면 WebAssembly의 실행속도가 빠릅니다. 왜냐하면 WebAssembly는 속도를 최적화하기 쉬운 정적 타입 시스템을 사용하기 때문입니다. WebAssembly는 네이티브 코드만큼 빠른 속도를 목표로 하고 있습니다. asm.js도 이런 목표에 매우 근접한 결과를 냈습니다. 하지만 WebAssembly는 네이티브 코드와의 실행속도 격차를 더욱 줄이고 있습니다. 그래서 이번 글은 왜 WebAssembly가 asm.js 보다 빠른지에 대해 집중하려고 합니다.
시작하기 전에 일러둘 주의사항이 있습니다. 실행속도(Performance)는 측정하기 어려우며, 많은 변수가 존재합니다. 또, 새로운 기술의 경우 언제나 “최적화를 거치면 개선되는 케이스”가 존재합니다. 그래서 지금 시점의 모든 WebAssembly 벤치마크가 빠른 실행속도를 나타내지는 않습니다. 이 글은 왜 WebAssembly가 빨라야 하는지 설명합니다. 아직 WebAssembly가 빠르지 않을 수도 있습니다. 하지만 우리는 문제를 해결할 것입니다.
그런 사실을 염두에 두고, 왜 WebAssembly가 빠른지 알아봅시다.
1. 기동시간 (Startup)
WebAssembly는 다운로드 크기를 줄이고 파싱 속도를 높이기 위해 설계됐습니다. 그래서 대용량 어플리케이션도 빠르게 시작시킬 수 있습니다.
사실 gzip 압축되고 최소화(minified)된 JavaScript 코드의 다운로드 크기를 더 줄이는 것은 만만한 일이 아닙니다. 이미 네이티브 코드만큼 충분히 컴팩트하기 때문입니다. WebAssembly 바이너리 포맷은 다운로드 크기를 염두에 두고 주의깊게 설계함으로써 이를 더욱더 개선하려고 합니다 (LEB128s 같은 지표가 있습니다.). 통상 (gzip 압축된 코드 크기와 비교할 때) 크기면에서 10–20% 이득이 있습니다(크기가 작습니다).
WebAssembly는 파싱 속도를 대대적으로 개선합니다. WebAssembly는 JavaScript에 비교할 때, 차원이 다르게 빠른 속도로 파싱됩니다. 이런 속도는 결과적으로 바이너리 포맷이기 때문입니다. 더구나 빠른 파싱 속도를 얻기 위해 설계된 바이너리 포맷이기 때문입니다. 또 WebAssembly는 함수를 파싱하는 일을 (또 최적화하는 일을) 아주 쉽게 병렬로 처리할 수 있습니다. 멀티코어 머신에서 사용할 경우 매우 효율적입니다.
기동시간에는 코드를 다운로드하는 시간과 파싱하는 시간 이외에 다른 요소들, 즉 VM이 코드를 최적화하는 시간과 실행에 필요한 추가 데이터를 다운로드하는 시간 등도 포함됩니다. 하지만 코드 다운로드 시간과 파싱 시간은 생략할 수 없는 시간입니다. 그래서 우선적으로 개선해야 하는 시간입니다. 나머지 요소들은 브라우저 차원에서 또는 앱 차원에서 최적화하거나 타협할 수 있습니다 (예를 들어, 초기 프레임에서 baseline 컴파일러 나 WebAssembly 인터프리터를 사용한다면, 전체 코드를 완벽하게 최적화하지 않아도 될 것입니다.).
2. CPU 기능 (CPU features)
asm.js의 실행속도를 빠르게 만들었던 트릭이 하나 있습니다. JavaScript가 모든 숫자를 실수형(double)으로 처리하는데 비해, asm.js는 숫자연산 직후에 bitwise-and 연산을 덧붙였습니다. 그러면 논리적으로 볼 때 실수 연산이 간단한 정수 연산으로 바뀌게 됩니다. CPU는 실수 연산보다 정수 연산에 뛰어납니다. 그래서 VM 입장에서는 asm.js를 쓸 때 CPU 성능을 최대한 끌어낼 수 있었던 것입니다.
하지만 asm.js는 JavaScript로 표현할 수 있는 코드만 사용할 수 있는 한계가 있었습니다. WebAssembly에는 그런 제약이 없습니다. 그리고 WebAssembly를 사용하면 다음과 같은 CPU 기능들을 추가로 사용할 수 있습니다.
- 64비트 정수형. 64비트 정수형 연산은 최대 4배 빠릅니다. 이를 통해 해싱(hashing) 알고리즘과 암호화 알고리즘 등의 속도를 높일 수 있습니다.
- 오프셋 로드(load)와 저장 기능. 이 기능은 여로모로 도움이됩니다. 기본적으로 메모리 객체를 다루는 모든 일에 도움이 됩니다. (C 언어의 구조체처럼) 오프셋이 고정된 메모리 객체의 필드에 접근할 때 유용합니다.
- 얼라인 되지 않은 로드와 저장(unaligned loads and stores) 기능. 이 기능 덕분에 asm.js에서 필요했던 마스크(mask) 작업을 피할 수 있습니다 (asm.js는 호환성 유지를 위해 Typed Array에 대해 마스크 작업을 해야 했습니다). 이 기능은 거의 모든 로드 작업과 저장 작업에 유용합니다.
- popcount, copysign 등 다양한 CPU 명령들. 이 명령들은 특별한 상황에서 도움이 됩니다 (예를 들어 popcount 명령은 cryptanalysis에 도움이 됩니다).
위의 기능들을 이용한 벤치마크 결과는 어떨까요? asm.js와 비교했을 때 평균 5% 정도의 속도 개선을 얻을 수 있습니다. 앞으로 SIMD 같은 추가적인 CPU 기능을 사용하게되면 실행속도를 더욱더 개선할 수 있을 것입니다.
3. 툴체인 개선 (Toolchain Improvements)
WebAssembly는 원래 컴파일러의 타겟 코드입니다. 그래서 2개의 부분으로 나뉩니다. 하나는 WebAssembly 코드를 생성하는 컴파일러 부분이고 (툴체인 부분), 또 하나는 WebAssembly 코드를 실행시키는 VM 부분입니다 (브라우저 부분). 좋은 성능을 내려면 양쪽 모두의 성능이 좋아야 합니다.
이는 asm.js의 경우도 마찮가지였습니다. Emscripten 프로젝트가 상당히 많은 툴체인 최적화를 이루어냈습니다. asm.js의 경우 LLVM의 최적화 도구와 Emscripten의 최적화 도구를 사용합니다. WebAssembly는 그 기반을 그대로 사용합니다. 그와 동시에 현저한 개선을 추가했습니다. asm.js와 WebAssembly 모두 컴파일러 입장에서 일반적이지 않은 특별한 타겟 코드입니다. 우리는 asm.js에서 얻었던 경험들을 WebAssembly에 적용해서 더 좋은 결과를 만들어낼 수 있습니다. 예를 들면 다음과 같은 것들입니다.
- 우리는 Emscripten asm.js optimizer를 개선해서 Binaryen WebAssembly optimizer를 만들었습니다. 이 최적화 도구는 속도 개선을 목표로 설계됐습니다. 높아진 속도 덕분에 더 복잡한 최적화 작업을 수행할 수 있습니다. 예를 들어, 함수 중복 제거 같은 최적화 작업을 디폴트로 수행할 수 있습니다. 함수 중복 제거 작업을 거치면 대용량 C++ 코드의 컴파일 결과물의 크기를 대략 5% 정도 줄일 수 있습니다.
- 크기 축소가 어렵고 복잡한 제어흐름에 대한 보다 효과적인 최적화와 Relooper 알고리즘을 통해 컴파일 결과로 만들어내는 인터프리터-타입 루프(interpreter-type loops)의 효율을 개선합니다.
- Binaryen optimizer는 다양한 실험을 염두에 두고 설계됐습니다. 그래서 superoptimization 적용 실험을 통해 소폭의 효율 개선을 이뤄냈습니다. 이 개선은 우리가 시도하기만 했다면 asm.js에서도 실현 가능했었을 것입니다.
전반적으로, 이런 툴체인 개선 덕분에 asm.js 대신 WebAssembly를 선택하면 더 나은 결과를 얻을 수 있습니다 (7% and 5% on Box2D, respectively).
4. 예측 가능한 실행 성능 (Predictably Good Performance)
asm.js는 기본적으로 네이티브 코드의 실행 속도를 낼 수 있습니다. 하지만 어떤 브라우저에서도 네이티브 코드와 동등한 실행 속도를 실현한 적이 없었습니다. 브라우저마다 최적화 시도 방향이 달랐고 그래서 서로 다른 결과를 보였습니다. 시간이 지나면서 최적화 방향이 수렴하기 시작했습니다. 하지만 asm.js 자체가 표준이 아니라는 것이 근본적인 문제였습니다. asm.js는 JavaScript에 기반한 비공식 스펙이었습니다. 일개 벤더가 만든 스펙이었고, 다른 벤더들이 관심을 보였을 뿐이었습니다.
반면 WebAssembly는 주요 브라우저 벤더들이 공동으로 설계한 기술입니다. JavaScript의 경우, 실행 속도를 높이려면 아주 창의적인 방법을 사용해야 합니다. asm.js의 경우 간단한 방법으로 실행 속도를 높일 수 있지만 모든 브라우저가 지원하지 않습니다. WebAssembly의 경우는 실행 속도 개선 방법에 관해 보다 많은 합의가 이루어져 있습니다. 아직 VM 마다 차이점이 많지만 (tier 컴파일 방법, AOT vs. JIT, 등), 모든 브라우저에서 기대할 수 있는 실행성능의 기준선에 대한 합의가 존재합니다.
이 글은 Alon Zakai 이 쓴 Why WebAssembly is Faster Than asm.js 의 한국어 번역본입니다.
작성자: ingeeKim
"누구에게나 평등하고 자유로운 웹"에 공감하는 직장인.
댓글이 없습니다.