GN⁺: 트리 쉐이킹, 원예학적으로 오도된 알고리즘 (2023)
(wingolog.org)WebAssembly의 한계와 Tree-shaking의 중요성
-
WebAssembly는 많은 관심과 기대에도 불구하고 웹에서는 제한적인 성공만 거둠
- 포토샵과 같은 성공 사례도 있지만, 전반적으로 WebAssembly를 활용한 프로젝트는 많지 않음
- 특히 DOM이 많이 사용되는 앱에서는 WebAssembly가 적합하지 않음
- JavaScript와 WebAssembly의 프로그래밍 모델 차이가 주요 원인 중 하나
-
WebAssembly는 C나 Rust 같은 언어 외에는 크게 성공하지 못함
- C#과 같은 언어는 가비지 컬렉터 등의 런타임을 함께 제공해야 하는 불편함이 있음
- 하지만 참조 타입과 가비지 컬렉션을 지원하는 WebAssembly의 새로운 기능이 곧 도입될 예정이어서 상황이 개선될 것으로 기대됨
컴파일러의 코드 최적화 능력이 WebAssembly 성공의 핵심
-
WebAssembly가 웹에서 성공하기 위해서는 컴파일러가 작고 효율적인 코드를 생성할 수 있어야 함
- 몇 킬로바이트 내외의 작은 파일 크기를 유지하는 것이 중요
- 그렇지 못한 경우 과대 광고나 특정 사용자 기반에 의존할 수밖에 없음
-
JavaScript 세계에서는 번들러 등을 통해 코드 사이즈 최적화가 이루어지고 있음
- Tree-shaking은 프로그램에서 실제 사용되는 함수와 데이터 타입만 포함시키는 기법
-
Tree-shaking은 horticultural, algorithmic 측면에서 부적절한 은유이기는 하지만 널리 사용되는 용어임
다른 언어에서의 Tree-shaking 현황
-
Go나 Python 등 런타임이 무거운 언어에서는 아직 Tree-shaking이 최적화되어 있지 않음
- 가장 간단한 Go 프로그램도 WebAssembly로 컴파일하면 2MB 이상의 크기가 나옴
- Python의 Pyodide 역시 20MB 정도의 파일을 다운로드 해야 함
-
서버 환경에서는 바이너리 크기가 큰 문제가 되지 않기 때문
- 모바일 등 제한된 환경을 위해서는 MicroPython, TinyGo 등의 경량화된 툴체인이 별도로 개발되기도 함
-
웹 환경을 위한 언어 구현체는 기존의 것과 차이가 있을 수밖에 없음
- DOM과 상호작용하는 것 자체가 특이한 환경이기 때문
- ClojureScript의 경우 Clojure와의 차이점을 별도 문서화하고 있음
Tree-shaking 알고리즘에 대한 논의
-
저자가 개발 중인 Hoot Scheme 컴파일러는 현재 70KB 내외의 Wasm 코드를 생성하고 있음
- 함수(프로시저) 정의만 포함하는 것은 비교적 쉬움
- 하지만 아래와 같은 몇 가지 어려운 점이 있음
-
letrec* 평가 모델에서는 바인딩이 재귀적이면서 순서가 있어 컴파일러가 분석하기 어려움
- 레코드 타입의 경우 vtable 콜백이 많은 코드를 살려두게 만듦
-
display와 같이 다형성이 높은 함수를 사용하면 관련된 많은 코드가 포함됨
- write-string 등 구체적인 함수를 사용하는 것이 나음
-
최적의 Tree-shaking을 위해서는 flow analysis가 필요함
- display에 bitvector 인자가 전달되지 않는다는 것을 알면 관련 코드를 제거할 수 있음
-
Python에서는 동적 디스패치, getattr 등의 동적 기능 때문에 더 어려움
- Python의 모듈 구조도 Tree-shaking을 복잡하게 만드는 요인
요약
- GC 지원으로 인해 WebAssembly에서 JavaScript 외의 언어로 DOM 프로그래밍이 가능해짐
- 하지만 결과물의 크기를 충분히 작게 만들기 위해서는 각 언어 툴체인에 상당한 투자가 필요함
- Tree-shaking 알고리즘을 적용한 별도의 툴체인 개발과 표준 라이브러리 최적화 등이 요구됨
GN⁺의 의견
-
WebAssembly가 GC를 지원하면서 웹 개발에 다양한 언어를 사용할 수 있게 되었지만, 기존 언어의 툴체인을 그대로 가져오기에는 어려움이 많아 보입니다. 웹 환경에 특화된 언어 구현과 최적화 기법이 개발되어야 할 것 같네요.
-
Tree-shaking이 동적 타이핑 언어에서 잘 동작하기 위해서는 정적 분석이 필수적일 것 같습니다. 하지만 Python 같은 언어는 동적 기능이 많아서 쉽지 않을 것 같아요. 차라리 처음부터 정적 분석에 유리한 언어를 새로 만드는 것도 방법이 될 수 있겠네요.
-
Hoot나 TinyGo 같은 실험적인 프로젝트들이 좋은 참고가 될 것 같습니다. 하지만 이런 프로젝트를 실제 제품에 적용하기에는 아직 시기상조일 수 있어요. 점진적으로 개선해 나가는 수밖에 없을 것 같네요.
-
성능에 민감하지 않고 빠른 개발이 중요한 프로젝트라면 Pyodide 같은 것을 써볼 만 할 것 같습니다. 하지만 사용자 경험을 중시하는 제품이라면 현재로서는 JavaScript가 최선의 선택일 것 같아요.
-
WebAssembly 자체에 Tree-shaking 같은 기능을 넣는 것도 생각해 볼 수 있을 것 같네요. 하지만 언어에 따라 요구 사항이 다르기 때문에 쉽지는 않겠죠. 또 Tree-shaking을 잘 지원하는 언어가 나오면 오히려 그 언어로 코딩하는 것이 이득일 수도 있고요. WebAssembly와 프로그래밍 언어 간의 역할 분담이 어떻게 이루어질지 궁금하네요.
Hacker News 의견
요약하면 다음과 같습니다:
- OpenEtG 프로젝트에서는 Rust로 작성된 WASM 바이너리 크기를 400KB 미만으로 유지하기 위해 다음과 같은 노력을 기울임
-
float
대신 고정 소수점 연산 사용 -
HashMap
대신Vec
사용 - 문자열 사용 최소화
- 작은 할당자(
talc
) 사용 - 의존성 최소화 (
rand
,fxhash
만 사용) - 제네릭 다양성 피하기
- 크기를 고려한 알고리즘 설계
-
-
Tree-shaking은 잘못된 명명법이며, Virgil 컴파일러에서는 이를 Reachability Analysis라고 부름. 컴파일 과정에서
main
진입점에서 탐색하여 도달 가능한 코드만 최종 바이너리에 포함시킴. - WasmGC 덕분에 Java와 Kotlin은 2-3KB 정도의 작은 WASM 바이너리를 생성할 수 있음. 하지만 API 선택에 주의해야 함.
- WASM을 이용한 DOM 조작은 여전히 JS에 의존적임.
- Tree Shaking 용어가 생겨난 이유는 Dead Code Elimination이 오래전부터 존재했기 때문임.
- WASM의 코드 크기 문제는 언어 런타임과 표준 라이브러리를 모두 번들링해야 하기 때문에 발생함.
- 이를 해결하기 위해 공유 라이브러리와 동적 링킹을 고려해볼 수 있음.
- Pyodide는 동적 링킹을 지원하는 대표적인 예시
- 브라우저가 인기 있는 언어 런타임을 미리 로드한다면, 웹 페이지에서 해당 런타임을 공유할 수 있음
- Zig 언어는 작은 WASM 바이너리 생성에 적합함. 하지만 100KB 미만이라면 크기는 중요한 요소가 아님.
- 내장 GC는 모든 앱에 중요한 것은 아니며, GC 없는 웹앱을 만드는 것이 좋음.
- WASM을 사용하는 앱의 성공 요인은 여전히 성능 향상임.
- ClojureScript, TypeScript, ReasonML 등 컴파일-투-JS 언어를 통해 오래전부터 JS 외의 언어로 DOM 프로그래밍을 해왔음.
- asm.js와 emscripten을 통해 WASM 이전에도 C 기반 언어를 웹에서 컴파일해 사용했음.
- Google Maps와 Google Earth는 WASM을 사용하는 대표적인 앱임.