mimalloc 및 WasmFS로 멀티스레드 WebAssembly 애플리케이션 확장

Alon Zakai
Alon Zakai

게시일: 2025년 1월 30일

웹의 많은 WebAssembly 애플리케이션은 네이티브 애플리케이션과 마찬가지로 멀티스레딩의 이점을 누립니다. 여러 스레드를 사용하면 더 많은 작업을 동시에 실행하고, 과도한 작업을 기본 스레드 외부로 전환하여 지연 시간 문제를 방지할 수 있습니다. 최근까지 이러한 멀티스레드 애플리케이션에서 할당 및 I/O와 관련된 몇 가지 일반적인 문제가 발생했습니다. 다행히 Emscripten의 최근 기능이 이러한 문제 해결에 큰 도움이 될 수 있습니다. 이 가이드에서는 이러한 기능을 사용하면 경우에 따라 속도가 10배 이상 향상되는 방법을 보여줍니다.

확장

다음 그래프는 순수 수학 워크로드에서의 효율적인 멀티스레드 확장 (이 도움말에서 사용할 벤치마크)을 보여줍니다.

수학 확장이라는 제목의 선 차트에는 코어 수 (x축)와 밀리초 (y축, 라벨이 지정됨)의 실행 시간 간의 관계가

이는 각 CPU 코어에서 자체적으로 실행할 수 있는 순수 계산을 측정하므로 코어 수가 많을수록 성능이 향상됩니다. 성능이 향상되는 하향선은 바로 확장이 잘 이루어지고 있다는 의미입니다. 또한 웹 플랫폼은 동시 로드의 기반으로 웹 워커를 사용하고, 실제 네이티브 코드 대신 Wasm을 사용하며, 최적화되지 않은 것처럼 보일 수 있는 기타 세부정보에도 불구하고 멀티스레드 네이티브 코드를 매우 잘 실행할 수 있음을 보여줍니다.

힙 관리: malloc/free

mallocfree는 완전히 정적 메모리가 아니거나 스택에 있지 않은 모든 메모리를 관리하는 데 사용되는 모든 선형 메모리 언어 (예: C, C++, Rust, Zig)의 중요한 표준 라이브러리 함수입니다. Emscripten은 기본적으로 dlmalloc를 사용합니다. dlmalloc는 작지만 효율적인 구현입니다. emmalloc도 지원합니다. emmallocdlmalloc보다 훨씬 작지만 경우에 따라 속도가 느립니다. 그러나 dlmalloc의 멀티스레드 성능은 제한적입니다. 단일 글로벌 할당자가 있으므로 각 malloc/free에 잠금이 적용되기 때문입니다. 따라서 한 번에 여러 스레드에 할당이 많으면 경합 및 느림이 발생할 수 있습니다. malloc 사용량이 매우 많은 벤치마크를 실행하면 다음과 같은 일이 발생합니다.

'dlmalloc 확장'이라는 제목의 선 차트는 코어 수 (x축)와 밀리초 단위의 실행 시간 (y축, 낮을수록 좋음) 간의 관계를 보여줍니다. 이 추세는 코어 수가 증가할수록 실행 시간이 늘어나며 1개에서 4개 코어로 꾸준히 선형적으로 증가함을 나타냅니다.

코어 수가 늘어나도 성능이 개선되지 않을 뿐만 아니라 각 스레드가 malloc 잠금을 오랜 시간 동안 기다리게 되어 성능이 점점 더 악화됩니다. 이는 최악의 경우이지만 할당이 충분한 경우 실제 워크로드에서 발생할 수 있습니다.

mimalloc

dlmalloc의 멀티스레드 최적화 버전(예: ptmalloc3)이 있습니다. 이 버전은 스레드별로 별도의 할당자 인스턴스를 구현하여 경합을 방지합니다. jemalloctcmalloc와 같이 멀티스레딩 최적화 기능이 있는 다른 할당자가 여러 개 있습니다. Emscripten은 Microsoft에서 설계한 멋진 할당자이며 휴대성과 성능이 매우 우수한 최근 mimalloc 프로젝트에 집중하기로 결정했습니다. 다음과 같이 사용합니다.

emcc -sMALLOC=mimalloc

다음은 mimalloc를 사용한 malloc 벤치마크의 결과입니다.

'mimalloc 확장'이라는 제목의 선 차트는 코어 수 (x축)와 밀리초 단위의 실행 시간 (y축, 낮을수록 좋음) 간의 관계를 보여줍니다. 이 추세를 보면 코어 수가 증가할수록 실행 시간이 줄어드는 것을 알 수 있습니다. 1에서 2코어로 갈 때는 급격히 감소하고 2에서 4코어로 갈 때는 점진적으로 감소합니다.

그렇다면 이제 성능이 효율적으로 확장되어 코어 수가 늘어날수록 속도가 점점 빨라집니다.

마지막 두 그래프에서 단일 코어 성능에 관한 데이터를 자세히 살펴보면 dlmalloc는 2, 660ms가 소요되었고 mimalloc는 1, 466ms만 소요되었으며 속도가 거의 2배 향상된 것을 알 수 있습니다. 이는 단일 스레드 애플리케이션에서도 mimalloc의 더 정교한 최적화의 이점을 얻을 수 있음을 보여줍니다. 단, 코드 크기와 메모리 사용량이 늘어나는 단점이 있습니다. 이 때문에 dlmalloc가 기본값으로 유지됩니다.

파일 및 I/O

많은 애플리케이션은 다양한 이유로 파일을 사용해야 합니다. 예를 들어 게임에서 레벨을 로드하거나 이미지 편집기에서 글꼴을 로드하는 경우 printf와 같은 작업도 내부적으로 파일 시스템을 사용합니다. stdout에 데이터를 쓰면서 출력하기 때문입니다.

단일 스레드 애플리케이션에서는 일반적으로 문제가 되지 않으며, 필요한 것이 printf뿐인 경우 Emscripten은 전체 파일 시스템 지원을 링크하지 않습니다. 하지만 파일을 사용하는 경우 파일 액세스를 스레드 간에 동기화해야 하므로 멀티스레드 파일 시스템 액세스가 까다로워집니다. Emscripten의 원래 파일 시스템 구현(JavaScript로 구현되었기 때문에 'JS FS'라고 함)은 기본 스레드에서만 파일 시스템을 구현하는 간단한 모델을 사용했습니다. 다른 스레드가 파일에 액세스하려고 할 때마다 기본 스레드에 요청을 프록시합니다. 즉, 다른 스레드는 교차 스레드 요청에서 차단되며 기본 스레드가 이를 처리합니다.

이 간단한 모델은 기본 스레드만 파일에 액세스하는 경우에 최적이며 이는 일반적인 패턴입니다. 하지만 다른 스레드에서 읽기 및 쓰기를 실행하면 문제가 발생합니다. 첫째, 기본 스레드가 다른 스레드의 작업을 실행하여 사용자에게 표시되는 지연 시간이 발생합니다. 그러면 백그라운드 스레드는 필요한 작업을 수행하기 위해 기본 스레드가 비어 있을 때까지 기다리게 되므로 속도가 느려집니다. 더 나쁜 경우 기본 스레드가 현재 해당 작업자 스레드를 기다리고 있으면 교착 상태가 발생할 수 있습니다.

WasmFS

이 문제를 해결하기 위해 Emscripten에는 새로운 파일 시스템 구현인 WasmFS가 있습니다. WasmFS는 JavaScript로 작성된 원래 파일 시스템과 달리 C++로 작성되고 Wasm으로 컴파일됩니다. WasmFS는 모든 스레드 간에 공유되는 Wasm 선형 메모리에 파일을 저장하여 최소한의 오버헤드로 여러 스레드의 파일 시스템 액세스를 지원합니다. 이제 모든 스레드가 동일한 성능으로 파일 I/O를 실행할 수 있으며, 서로 차단되는 것을 피할 수도 있습니다.

간단한 파일 시스템 벤치마크는 이전 JS FS와 비교하여 WasmFS의 큰 이점을 보여줍니다.

'파일 시스템 성능'이라는 제목의 막대 그래프가 표시되어 있습니다. 이 그래프는 JS FS와 WasmFS의 실행 시간을 밀리초 단위로 비교하며 (y축, 낮을수록 좋음) 두 가지 카테고리 (기본 스레드 및 pthread, x축)를 기준으로 합니다. JS FS는 pthread 사례에서 훨씬 더 오래 걸리는 반면 WasmFS는 두 경우 모두 일관되게 낮습니다.

기본 스레드에서 직접 파일 시스템 코드를 실행하는 것과 단일 pthread에서 실행하는 것을 비교합니다. 이전 JS FS에서는 모든 파일 시스템 작업을 기본 스레드에 프록시해야 하므로 pthread에서 훨씬 느려집니다. 이는 JS FS가 일부 바이트를 읽거나 쓰는 대신 잠금, 대기열, 대기가 포함된 교차 스레드 통신을 실행하기 때문입니다. 반면 WasmFS는 모든 스레드에서 파일에 동등하게 액세스할 수 있으므로 차트에서 메인 스레드와 pthread 사이에는 실질적으로 차이가 없음을 알 수 있습니다. 따라서 WasmFS는 pthread에서 JS FS보다 32배 빠릅니다.

WasmFS가 2배 더 빠른 기본 스레드에도 차이가 있습니다. 이는 JS FS가 모든 파일 시스템 작업에 대해 JavaScript를 호출하기 때문입니다. WasmFS는 이를 방지합니다. WasmFS는 필요한 경우에만 JavaScript를 사용합니다 (예: 웹 API 사용). 따라서 대부분의 WasmFS 파일은 Wasm에 남아 있습니다. 또한 JavaScript가 필요한 경우에도 WasmFS는 기본 스레드 대신 도우미 스레드를 사용하여 사용자에게 표시되는 지연 시간을 방지할 수 있습니다. 따라서 애플리케이션이 멀티스레드가 아니거나 멀티스레드이지만 기본 스레드에서만 파일을 사용하는 경우에도 WasmFS를 사용하면 속도가 향상될 수 있습니다.

다음과 같이 WasmFS를 사용합니다.

emcc -sWASMFS

WasmFS는 프로덕션에서 사용되며 안정적인 것으로 간주되지만 아직 이전 JS FS의 모든 기능을 지원하지는 않습니다. 반면에 원본 비공개 파일 시스템(OPFS, 영구 저장소에 권장됨) 지원과 같은 몇 가지 중요한 새 기능이 포함되어 있습니다. 아직 포팅되지 않은 기능이 필요한 경우가 아니라면 Emscripten팀에서는 WasmFS를 사용하는 것이 좋습니다.

결론

할당을 많이 하거나 파일을 사용하는 멀티스레드 애플리케이션이 있는 경우 WasmFS 또는 mimalloc를 사용하면 큰 이점을 얻을 수 있습니다. 이 게시물에 설명된 플래그를 사용하여 다시 컴파일하기만 하면 Emscripten 프로젝트에서 간단하게 시도할 수 있습니다.

스레드를 사용하지 않는 경우에도 이러한 기능을 사용해 볼 수 있습니다. 앞서 언급한 대로 최신 구현에는 경우에 따라 단일 코어에서도 눈에 띄는 최적화가 포함되어 있습니다.