Tech News

제목WebAssembly는 어떻게 JavaScript를 빠르게 실행할 수 있는가2021-10-12 09:39
작성자 Level 10

https://bytecodealliance.org/articles/making-javascript-run-fast-on-webassembly 

### 인트로


- JS를 브라우저에서 띄울 때는 브라우저의 JS 엔진이 잘 튜닝되어 있어서 실행이 빠른데, 요즘은 다른 환경에서도 JS를 많이 쓴다. (서버리스, 게이밍 콘솔, iOS 등)

- WASM은 이러한 런타임에서 JS를 빠르게 돌릴 수 있게 해주는 기술이다.


### 작동 방식


- JS 코드는 JS 엔진이 있다면 인터프리터와 JIT 컴파일러 등을 통해 바이트코드로 바뀐다.

- JS 엔진이 없는 환경에는 JS 엔진을 코드와 함께 배포해야 하는데, JS 엔진을 WASM 모듈로 배포함으로써 여러 환경에서 포터블하게 만들 수 있다.

- JS 코드는 WASM 엔진 안에 격리된 JS 엔진 안에서 작동하게 된다.

- WASM 엔진이 사용하는 JS 엔진은 SpiderMonkey로, 파이어폭스도 이를 사용한다.

- WASM은 그 자체로 머신코드를 만들어낼 수 없으므로 JS로 컴파일을 거쳐야 한다.

- 근데 JIT을 쓸 수 없으므로 WASM은 느린 게 정상이다. 그러면 WASM이 대체 어떻게 JS 실행을 “빠르게” 한다는 걸까?


### 어디에서 WASM을 쓰는가


iOS (또는 JIT 못쓰는) 환경에서 JS 쓰기

- 게이밍 콘솔, unprivileged iOS app, 스마트 TV 등은 보안을 이유로 JIT을 못 쓴다.

(→ JIT 컴파일링에 보안 이슈가 있는 게 당연하다는 듯 얘기하고 있는데 그 이유는 찾아봐도 잘 모르겠습니다.)

- 따라서 이런 데서는 인터프리터를 써야 하는데, 사실 원래 이런 플랫폼에서 도는 앱들은 굉장히 오래 돌고 코드 양이 많기 때문에 인터프리터를 써서 느려지는 걸 피하는 게 맞다.

- 인터프리터의 성능 저하 이슈를 피하면서도 JS를 쓰려면 어떻게 해야 할까?


서버리스에서 JS 쓰기

- 서버리스 환경은 JIT은 존재하지만 콜드 스타트 타임이 길어서 레이턴시가 길어지는 게 문제다. (엔진 로드에만 최소 5 ms)

- 콜드 스타트 타임을 숨기는 최적화 기법들이 있지만, 네트워크 레이어가 좋아질수록(e.g., QUIC) 큰 의미가 없어지고, 또 여러 서버리스 함수를 동시에 실행해도 최적화 기법이 별 쓸모가 없어진다.

- 인스턴스 재사용으로 콜드 스타트 타임을 피할 수도 있지만, 이는 요청 사이의 상태가 공유된다는 뜻이고 보안 위험이 된다.

- 이런 것 때문에, 실무에서는 베스트 프랙티스를 따르지 않고 한 서버리스 함수에 많은 내용을 집어넣는 일도 많아지고 있다.

- 즉 콜드 스타트 문제만 해결되면, 이를 피하기 위한 여러 기법을 쓸 필요도 없고 많은 문제가 해결된다.

- WASM은 JS를 감싸서 격리하고 있고, WASM 자체의 코드는 짧고 단순해서 감시하기도 쉽고, 보안 리스크도 줄어든다.


### JS 엔진은 어디에 시간을 많이 쓰는가


초기화 페이즈

- (engine 초기화) 서버리스에 해당. 자기 자신을 준비해야 하고, 빌트인 함수들을 환경에 추가해야 한다. 이게 서버리스의 콜드 스타트가 느린 이유 중 하나다.

- (application 초기화) 함수를 바이트코드로 파싱, 변수에 메모리 할당, 변수에 값 할당


런타임 페이즈

- 이때부터의 throughput은 여러 조건의 영향을 받는다.

- which language features are used

- whether the code behaves predictably from the JS engine’s point of view

- what sort of data structures are used

- whether the code runs long enough to benefit from the JS engine’s optimizing compiler


JS 엔진을 빠르게 한다는 건 초기화와 런타임 페이즈 두 개를 빠르게 한다는 것이다. 정확히는, 초기화에 걸리는 시간을 줄이고 런타임에는 쓰루풋, 즉 코드의 처리 속도를 늘린다.


### 초기화 시간 줄이기


- WASM은 Wizer 라는 pre-initializer 를 사용하여 초기화 시간을 줄인다. (작은 앱 기준으로 JS isolate 대비 JS on WASM은 대략 13배 빠르다)

- 코드를 배포하기 전에 빌드하는 단계에서, pre-initializer는 모든 JS 코드를 한번 초기화 단계까지 실행해본다.

- 이렇게 하면 JS 엔진의 리니어 메모리에 JS 코드들이 바이트코드로 저장되어 있는 상태이고, 메모리 할당도 끝나있다.

- 이걸 그대로 복사해서 WASM의 데이터 섹션에 붙인다.

- JS 엔진이 instantiate될 때는 데이터 섹션의 모든 데이터에 접근할 수 있다. 특정 메모리가 필요하면 데이터 섹션에서 복사해오면 된다. 그래서 스타트 시간이 필요가 없고, 그래서 pre-initialization이라고 부른다.

- 현재는 JS 엔진과 같은 모듈에 데이터 섹션을 붙여두지만, 미래에는 module linking을 이용하여 데이터 섹션을 별도의 모듈로 만들어, 여러 어플리케이션이 JS 엔진을 공유할 수 있게 할 계획이다.

- 그리고 사실 이 pre-초기화 테크닉은 JS 엔진에 국한될 필요가 없고 파이썬, 루비, 루아 등 어떤 런타임에도 쓸 수 있는 컨셉이다.


### 쓰루풋 늘리기


- JS 코드가 짧은 시간동안만 실행된다면 어차피 JIT을 거치지 않기 때문에 WASM의 쓰루풋도 브라우저와 같을 것이다. 그러나 길게 실행되는 코드는 JIT의 개입 여부가 야기하는 쓰루풋 차이가 크다.

- WASM은 JIT을 못 쓰니, 대신 AOT(ahead-of-time) 컴파일을 하되 JIT에서 가져올 수 있는 기법은 가져오는 방식을 취했다.

- JIT의 최적화 기법 중 하나가 인라인 캐싱이다. 과거에 실행된 코드 조각을 유지해뒀다가 재사용하는 것.

- WASM에서는 JS에서 자주 사용하는 패턴을 stub으로 만들어놓았다. 예를 들어 오브젝트 프로퍼티에 접근하기.

- 원래 오브젝트 프로퍼티 접근을 제대로 하려면 shape와 offset 정보가 필요한데, 이것들은 AOT로 알 수 없다.

- 그러나 shape와 offset을 파라미터로 해서 프로퍼티에 접근하는 stub은 미리 만들어둘 수 있다. 이 stub 코드는 여러 군데서 재사용 가능하다.

- WASM은 이러한 common patterns를 다 stub으로 만들어둔다. 이건 JS 코드가 실제로 어떻게 생겼냐랑은 상관없다. 이를 통해 JS 엔진이 만들 머신코드가 줄고, 초기화 시간도 줄고, 캐시 로컬리티도 좋아지게 할 수 있다.

- 이러한 stub을 2kb만 준비해놔도, 실제 JS 코드의 95% 정도는 커버할 수 있음이 확인되었다.

- 이런 기법은 ahead-of-time, 즉 코드 내용을 모르는 채(프로파일링 없이) 최적화하는 것이므로, 프로파일링을 더 한다면 JIT처럼 더 최적화할 여지가 있을 것이다.

- 그런데 프로파일링 자체가 쉬운 게 아니라서 노력하는 중이다.

댓글
자동등록방지
(자동등록방지 숫자를 입력해 주세요)