Menskalakan aplikasi WebAssembly multi-thread dengan mimalloc dan WasmFS

Alon Zakai
Alon Zakai

Dipublikasikan: 30 Januari 2025

Banyak aplikasi WebAssembly di web mendapatkan manfaat dari multithreading, dengan cara yang sama seperti aplikasi native. Beberapa thread memungkinkan lebih banyak tugas terjadi secara paralel, dan memindahkan tugas berat dari thread utama untuk menghindari masalah latensi. Hingga baru-baru ini, ada beberapa poin masalah umum yang dapat terjadi dengan aplikasi multi-thread tersebut, yang terkait dengan alokasi dan I/O. Untungnya, fitur terbaru di Emscripten dapat sangat membantu mengatasi masalah tersebut. Panduan ini menunjukkan cara fitur ini dapat meningkatkan kecepatan hingga 10 kali lipat atau lebih dalam beberapa kasus.

Penskalaan

Grafik berikut menunjukkan penskalaan multi-thread yang efisien dalam beban kerja matematika murni (dari benchmark yang akan kita gunakan dalam artikel ini):

Diagram garis berjudul Penskalaan matematika menunjukkan hubungan antara jumlah core (sumbu x) dan waktu eksekusi dalam milidetik (sumbu y, berlabel

Ini mengukur komputasi murni, sesuatu yang dapat dilakukan setiap core CPU secara mandiri, sehingga performa meningkat dengan lebih banyak core. Garis menurun dari performa yang lebih cepat adalah tampilan penskalaan yang baik. Dan hal ini menunjukkan bahwa platform web dapat menjalankan kode native multi-thread dengan sangat baik, meskipun menggunakan pekerja web sebagai dasar untuk paralelisme, menggunakan Wasm, bukan kode native sejati, dan detail lainnya yang mungkin tampak kurang optimal.

Pengelolaan heap: malloc/free

malloc dan free adalah fungsi library standar yang penting di semua bahasa memori linear (misalnya, C, C++, Rust, dan Zig) yang diandalkan untuk mengelola semua memori yang tidak sepenuhnya statis atau di stack. Emscripten menggunakan dlmalloc secara default, yang merupakan implementasi yang ringkas tetapi efisien (juga mendukung emmalloc, yang bahkan lebih ringkas tetapi lebih lambat dalam beberapa kasus). Namun, performa multi-thread dlmalloc terbatas karena memerlukan kunci pada setiap malloc/free (karena ada satu alokator global). Oleh karena itu, Anda dapat mengalami pertentangan dan kelambatan jika memiliki banyak alokasi dalam banyak thread sekaligus. Berikut adalah hal yang terjadi saat Anda menjalankan benchmark yang sangat berat malloc:

Diagram garis berjudul penskalaan dlmalloc menunjukkan hubungan antara jumlah core (sumbu x) dan waktu eksekusi dalam milidetik (sumbu y, berlabel lebih rendah lebih baik). Tren ini menunjukkan bahwa peningkatan jumlah core akan menghasilkan waktu eksekusi yang lebih tinggi, dengan peningkatan linear yang stabil dari 1 hingga 4 core.

Performa tidak hanya tidak meningkat dengan lebih banyak core, tetapi semakin buruk, karena setiap thread akhirnya menunggu kunci malloc selama waktu yang lama. Ini adalah kasus terburuk yang mungkin terjadi, tetapi dapat terjadi dalam beban kerja sebenarnya jika ada alokasi yang cukup.

mimalloc

Versi dlmalloc yang dioptimalkan multi-thread ada, seperti ptmalloc3, yang menerapkan instance alokator terpisah per thread, sehingga menghindari pertentangan. Ada beberapa alokator lain dengan pengoptimalan multithreading, seperti jemalloc dan tcmalloc. Emscripten memutuskan untuk berfokus pada project mimalloc terbaru, yang merupakan allocator yang didesain dengan baik dari Microsoft dengan portabilitas dan performa yang sangat baik. Gunakan sebagai berikut:

emcc -sMALLOC=mimalloc

Berikut adalah hasil untuk tolok ukur malloc menggunakan mimalloc:

Diagram garis berjudul penskalaan mimalloc menunjukkan hubungan antara jumlah core (sumbu x) dan waktu eksekusi dalam milidetik (sumbu y, berlabel lebih rendah lebih baik). Tren ini menunjukkan bahwa peningkatan jumlah core akan mengurangi waktu eksekusi, dengan penurunan tajam dari 1 menjadi 2 core dan penurunan yang lebih bertahap dari 2 menjadi 4 core.

Sempurna! Sekarang performa diskalakan secara efisien, semakin cepat dan cepat dengan setiap core.

Jika Anda melihat dengan cermat data untuk performa core tunggal dalam dua grafik terakhir, Anda akan melihat bahwa dlmalloc memerlukan waktu 2.660 md dan mimalloc hanya 1.466, peningkatan kecepatan hampir 2 kali. Hal ini menunjukkan bahwa bahkan pada aplikasi dengan thread tunggal, Anda mungkin melihat manfaat dari pengoptimalan mimalloc yang lebih canggih, meskipun perlu diperhatikan bahwa hal ini akan mengurangi ukuran kode dan penggunaan memori (karena alasan tersebut, dlmalloc tetap menjadi default).

File dan I/O

Banyak aplikasi perlu menggunakan file karena berbagai alasan. Misalnya, untuk memuat level dalam game, atau font di editor gambar. Bahkan operasi seperti printf menggunakan sistem file di balik layar, karena mencetak dengan menulis data ke stdout.

Dalam aplikasi dengan thread tunggal, hal ini biasanya tidak menjadi masalah, dan Emscripten akan otomatis menghindari penautan dalam dukungan sistem file lengkap jika yang Anda butuhkan adalah printf. Namun, jika Anda menggunakan file, akses sistem file multi-thread akan sulit karena akses file harus disinkronkan antar-thread. Implementasi sistem file asli di Emscripten, yang disebut "JS FS" karena diimplementasikan dalam JavaScript, menggunakan model sederhana untuk menerapkan sistem file hanya di thread utama. Setiap kali thread lain ingin mengakses file, thread tersebut akan melakukan proxy permintaan ke thread utama. Ini berarti bahwa thread lain memblokir permintaan lintas thread, yang pada akhirnya ditangani oleh thread utama.

Model sederhana ini optimal jika hanya thread utama yang mengakses file, yang merupakan pola umum. Namun, jika thread lain melakukan operasi baca dan tulis, masalah akan terjadi. Pertama, thread utama akhirnya melakukan pekerjaan untuk thread lain yang menyebabkan latensi yang terlihat pengguna. Kemudian, thread latar belakang akhirnya menunggu thread utama bebas untuk melakukan pekerjaan yang diperlukan, sehingga semuanya menjadi lebih lambat (atau, yang lebih buruk, Anda dapat mengalami deadlock jika thread utama saat ini menunggu thread pekerja tersebut).

WasmFS

Untuk memperbaiki masalah ini, Emscripten memiliki implementasi sistem file baru, WasmFS. WasmFS ditulis dalam C++ dan dikompilasi ke Wasm, tidak seperti sistem file asli yang ada dalam JavaScript. WasmFS mendukung akses sistem file dari beberapa thread dengan overhead minimal, dengan menyimpan file dalam memori linear Wasm, yang dibagikan di antara semua thread. Semua thread kini dapat melakukan I/O file dengan performa yang sama, dan sering kali mereka bahkan dapat menghindari pemblokiran satu sama lain.

Benchmark sistem file sederhana menunjukkan keunggulan besar WasmFS dibandingkan dengan JS FS lama.

Diagram batang berjudul Performa sistem file membandingkan waktu eksekusi dalam milidetik (sumbu y, berlabel lebih rendah lebih baik) untuk JS FS dan WasmFS dalam dua kategori: thread utama dan pthread (sumbu x). FS JS memerlukan waktu yang jauh lebih lama dalam kasus pthread, sedangkan WasmFS tetap konsisten rendah dalam kedua kasus.

Hal ini membandingkan kode sistem file yang berjalan langsung di thread utama dengan menjalankannya di satu pthread. Di JS FS lama, setiap operasi sistem file harus di-proxy ke thread utama, yang membuatnya lebih lambat satu urutan magnitudo di pthread. Hal ini karena, bukan hanya membaca/menulis beberapa byte, JS FS melakukan komunikasi lintas thread, yang melibatkan kunci, antrean, dan penantian. Sebaliknya, WasmFS dapat mengakses file dari thread mana pun secara setara, sehingga diagram menunjukkan bahwa praktis tidak ada perbedaan antara thread utama dan pthread. Akibatnya, WasmFS 32 kali lebih cepat daripada JS FS saat berada di pthread.

Perhatikan bahwa ada juga perbedaan pada thread utama dengan WasmFS yang 2 kali lebih cepat. Hal ini karena JS FS memanggil JavaScript untuk setiap operasi sistem file, yang dihindari oleh WasmFS. WasmFS hanya menggunakan JavaScript jika diperlukan (misalnya, untuk menggunakan Web API), yang membuat sebagian besar file WasmFS berada di Wasm. Selain itu, meskipun JavaScript diperlukan, WasmFS dapat menggunakan thread helper, bukan thread utama, untuk menghindari latensi yang terlihat oleh pengguna. Oleh karena itu, Anda mungkin melihat peningkatan kecepatan dari penggunaan WasmFS meskipun aplikasi Anda tidak multi-thread (atau jika multi-thread, tetapi hanya menggunakan file di thread utama).

Gunakan WasmFS sebagai berikut:

emcc -sWASMFS

WasmFS digunakan dalam produksi dan dianggap stabil, tetapi belum mendukung semua fitur JS FS lama. Di sisi lain, versi ini menyertakan beberapa fitur baru yang penting seperti dukungan untuk sistem file pribadi asal (OPFS, yang sangat direkomendasikan untuk penyimpanan persisten). Kecuali jika Anda memerlukan fitur yang belum di-port, tim Emscripten merekomendasikan penggunaan WasmFS.

Kesimpulan

Jika memiliki aplikasi multi-thread yang melakukan banyak alokasi atau menggunakan file, Anda mungkin akan mendapatkan manfaat besar dengan menggunakan WasmFS dan/atau mimalloc. Keduanya mudah dicoba dalam project Emscripten dengan mengompilasi ulang dengan flag yang dijelaskan dalam postingan ini.

Anda bahkan dapat mencoba fitur tersebut jika tidak menggunakan thread: Seperti yang disebutkan sebelumnya, implementasi yang lebih modern dilengkapi dengan pengoptimalan yang terlihat bahkan pada satu core dalam beberapa kasus.