태그
요약프론트엔드Web
최종 편집
Dec 30, 2022 2:32 AM
발행일
October 15, 2021
한국신용데이터 프론트엔드 팀은 매 분기가 끝날 때마다 Dev Day를 함께 보낸다. 2021년부터 시작된 문화인 Dev Day는 "딴짓하도록 회사 차원에서 허용된 하루"인데, 이 날을 어떻게 보낼지는 당연히 팀마다 다르다. 프론트엔드 팀은 이 날 하루에 원래 하던 업무를 벗어던진 채 다음 3가지 활동을 한다.
- 이번 분기의 주요 활동과 성과 함께 돌아보고 회고하기
- 다음 분기 로드맵 공유 및 개인 목표 초안 작성하기
- 개인적으로 하고 싶었던 공부를 하거나, 재미있는 시도를 해보고 공유하기
2021년 3분기 Dev Day는 9월 28일에 있었고, 나는 평소에 키워드는 종종 들어봤으나 대체 뭔지는 도통 몰랐던 WebAssembly(이하 WASM)에 대해 조금이나마 알아보기로 했다.
찾아보니 WASM 스탠다드를 만드는 Bytecode Alliance의 글이 괜찮아 보여서 이를 짧게 요약했다(원문: Making JavaScript run fast on WebAssembly by Lin Clark). 정리하고 나니 대강의 개념 이해는 되어서, 당장 내 실무에 쓸 일은 없지만 관련 글이 나오면 읽을 수 있는 있겠다는 판단이 들었다. 일단은 이정도로 만족.
(10월 15일 업데이트) 웹어셈블리의 자세한 동작 원리가 아닌, 웹어셈블리 자체의 개념에 대해 이해할 수 있는 링크도 추가해둔다.
인트로
- JS를 브라우저에서 띄울 때는 브라우저의 JS 엔진이 잘 튜닝되어 있어서 실행이 빠른데, 요즘은 다른 환경에서도 JS를 많이 쓴다.
- 서버리스, 게이밍 콘솔, iOS 등
- WASM은 이러한 런타임에서 JS를 빠르게 돌릴 수 있게 해주는 기술이다.
작동 방식
- JS 코드는 JS 엔진이 있다면 인터프리터와 JIT 컴파일러 등을 통해 바이트코드로 바뀐다.
- JS 엔진이 없는 환경에는 JS 엔진을 코드와 함께 배포해야 하는데, JS 엔진을 WASM 모듈로 배포함으로써 여러 환경에서 포터블하게 만들 수 있다.
- JS 코드는 WASM 엔진 안에 격리된 JS 엔진 안에서 작동하게 된다.
- WASM 엔진이 사용하는 JS 엔진은 SpiderMonkey로, 파이어폭스도 이를 사용한다.
- precise stack scanning 기술을 이용해서 최적화한다.
- WASM은 그 자체로 머신코드를 만들어낼 수 없으므로 JS로 컴파일을 거쳐야 한다.
- 근데 JIT을 쓸 수 없으므로 WASM은 느린 게 정상이다.
- 그러면 WASM이 대체 어떻게 JS 실행을 “빠르게” 한다는 걸까?
어디에서 WASM을 쓰는가
- iOS (또는 JIT 못쓰는) 환경에서 JS 쓰기
- 게이밍 콘솔, unprivileged iOS app, 스마트 TV 등은 보안을 이유로 JIT을 못 쓴다.
- (→ JIT 컴파일링에 보안 이슈가 있는 게 당연하다는 듯 얘기하고 있는데 그 이유는 찾아봐도 잘 모르겠다)
- (10월 10일 업데이트: 긱뉴스에도 올렸는데 kunggom님이 이유를 알려주셨다. https://news.hada.io/comment?id=7233)
- JIT의 보안 이슈 관련해서는 예전에 여기에 소개되었던 MS Edge 팀의 블로그 글에 관련 내용이 언급된 바가 있습니다. 기본적으로 JIT 엔진은 복잡하기 때문에 공격 표면이 늘어날 뿐더러, JIT에서 성능 향상을 위해 적용하는 투기적 최적화(Speculative Optimization)와 같은 방법이 특정 패턴의 보안 문제를 반복적으로 발생시키는 경향이 있는 모양입니다. 이 때문에 웹 브라우저의 보안 결함 중 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처럼 더 최적화할 여지가 있을 것이다.
- 그런데 프로파일링 자체가 쉬운 게 아니라서 노력하는 중이다.