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):
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
:
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
:
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.
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.