해결책
큰 WebAssembly 모듈을 로드하는 일은 예전에는 직렬로 진행됐다. 브라우저가 파일 전체를 ArrayBuffer로 다운로드하고, 그것을 JavaScript 엔진에 넘긴 뒤에야 컴파일을 시작했다. 빠른 연결에서는 네트워크가 더 이상 병목이 아니었다. 디코딩과 컴파일의 CPU 시간이 병목이었고, 그 전부가 다운로드 후에 일어나며 메인 스레드는 멈춰 있었다.
뻔한 해결책들은 각각 대가가 있었다. 단일 최적화 컴파일러는 빠른 코드를 만들었지만 시작이 느렸다. JavaScript 엔진은 런타임에 지연 컴파일했는데, 이는 작업을 피할 수는 있어도 컴파일을 메인 스레드에 남겨 두고 파싱이 끝나기를 기다려야 했다. 그리고 파일이 도착하기 전에 컴파일할 방법은 없어 보였다. 옛 API는 완전한 ArrayBuffer만 받아들였기 때문이다.
Firefox 58은 양쪽 끝을 공략했다. 스트리밍 컴파일은 다운로드된 각 청크를 스트리밍 API를 통해 곧바로 엔진에 넣었고, wasm 모듈의 코드 섹션이 데이터보다 앞에 오기 때문에 수 메가바이트의 데이터가 아직 전송 중일 때도 컴파일을 시작할 수 있었다. 새로운 계층형 컴파일러는 최적화 컴파일러보다 10~15배 빠르게 코드를 만드는 베이스라인 계층을 추가했고, 그 출력은 약 2배만 느리게 실행됐기에 코드는 거의 즉시 실행되기 시작했다. 그런 다음 백그라운드 스레드가 옵티마이저로 모듈 전체를 다시 컴파일해 핫스왑했다. 두 컴파일러 모두 코어에 걸쳐 함수 단위로 작업을 나눴다.
컴파일 처리량이 이제 네트워크 처리량을 넘어섰기 때문에, 마지막 패킷이 도착하자마자 코드가 거의 준비되어 있었다. Mozilla는 이 변화를 WebAssembly가 JavaScript를 파싱하는 대신 이미지를 디코딩하는 것처럼 로드되게 만든 것이라고 표현했다. 메인 스레드 컴파일 일시 정지도, 전체 페이로드를 기다리는 일도 없어진 것이다.
이룬 성과
도착하는 대로 바이트를 빠른 베이스라인 계층으로 스트리밍하고, 그 코드를 즉시 실행하며, 백그라운드에서 최적화된 코드로 핫스왑한다
첫 패킷을 컴파일하면 다운로드 후 컴파일을 직렬화하는 대신 다운로드 시간을 CPU 작업 안에 숨길 수 있다.
10~15배 빠르게 컴파일하는 베이스라인 계층은 시작을 빠른 시작으로 바꾼다. 약 2배 느린 코드가 최적화가 진행되는 동안 실행된다.
wasm 함수는 독립적으로 컴파일되므로 작업이 한 스레드에 대기하지 않고 코어에 분산된다.
모듈의 코드를 데이터보다 앞에 두면 페이로드 바이트가 아직 도착하는 동안 비싼 부분을 스트리밍 컴파일할 수 있다.
각 변화는 결국 실행되는 기계어를 바꾸지 않으면서 시작 시간을 단축하므로 최고 성능은 그대로다.
병목 단계가 전체 입력을 기다린다면 첫 청크에서 시작하고 값싼 버전을 먼저 내보내라. 차단하는 대신 백그라운드에서 업그레이드하라
그 후
Firefox 58은 2018년 1월에 스트리밍 및 계층형 컴파일러를 출시했고, 발표 설명글은 912점의 Hacker News 스레드를 불러일으켰다. Mozilla의 테스트 페이지는 사용자가 자신의 기기에서 컴파일 속도를 측정할 수 있게 했으며, 팀은 같은 릴리스에 포함된 JavaScript 바이트코드 캐시를 확장해 컴파일된 기계어를 HTTP 캐시에 캐시할 기반을 마련했다고 말했다.
근거 확인하기
출처
- Making WebAssembly even faster: Firefox's new streaming and tiering compiler Mozilla Hacks · hacks.mozilla.org ·
- Hacker News discussion: Making WebAssembly even faster Hacker News · news.ycombinator.com ·