Allocator Musl Menyebabkan Penurunan Performa 700x pada Aplikasi Multi-threaded

Tim Komunitas BigGo
Allocator Musl Menyebabkan Penurunan Performa 700x pada Aplikasi Multi-threaded

Sebuah investigasi terbaru terhadap masalah performa pada musl libc telah mengungkap hasil yang mengejutkan dan dapat mempengaruhi banyak aplikasi. Library C musl, yang umum digunakan dalam container Alpine Linux dan static builds, menyertakan memory allocator yang dapat menyebabkan perlambatan dramatis pada program multi-threaded. Dalam kasus ekstrem, aplikasi mengalami penurunan performa hingga 700 kali lebih lambat dari yang diharapkan.

Dampak Performa Ekstrem Berdasarkan Jumlah Core

Mesin 6-core: Perlambatan 7x Mesin 8-core: Perlambatan 4x
Mesin 48-core: Perlambatan 700x

Degradasi performa meningkat secara dramatis dengan semakin banyak core CPU, karena lebih banyak thread yang bersaing untuk mendapatkan satu lock allocator. Instance cloud modern dapat memiliki 192+ core, membuat masalah ini menjadi lebih parah lagi untuk aplikasi yang dapat diskalakan.

Akar Masalah: Lock Contention

Masalah performa ini berasal dari desain allocator musl, yang menggunakan single shared lock untuk semua operasi memori. Ketika beberapa thread mencoba mengalokasikan atau membebaskan memori secara bersamaan, mereka harus menunggu satu sama lain, menciptakan bottleneck. Desain ini bekerja dengan baik untuk program single-threaded tetapi menjadi masalah besar ketika lebih banyak CPU core dan thread terlibat.

Komunitas telah mengidentifikasi bahwa allocator musl mengandalkan single heap dengan locking untuk mendukung multiple thread, yang berarti setiap operasi memori harus memperoleh lock ini. Alternatif modern seperti mimalloc menggunakan per-thread heap sebagai gantinya, di mana setiap thread mengelola ruang memorinya sendiri. Pendekatan ini bekerja sangat baik dengan bahasa pemrograman seperti Rust, di mana objek jarang berpindah antar thread.

Dampak Nyata di Berbagai Proyek

Masalah ini bukanlah hal baru, tetapi terus mengejutkan para developer. Beberapa proyek telah mendokumentasikan masalah serupa, dengan perlambatan berkisar dari 2x hingga 20x pada aplikasi biasa. Variasi ini tergantung pada berapa banyak thread yang bersaing untuk memori dan seberapa sering mereka mengalokasikan memori baru.

Beberapa proyek besar telah beralih dari default allocator musl atau meninggalkan musl sepenuhnya. Dampak performa menjadi lebih parah pada sistem multi-core modern, di mana aplikasi secara alami menggunakan lebih banyak thread untuk memanfaatkan kekuatan pemrosesan yang tersedia.

Perbandingan Performa: glibc vs musl

Metrik glibc musl Perbedaan
Waktu pengguna (detik) 1.31 2.72 2.1x lebih lambat
Waktu sistem (detik) 0.32 6.13 19.2x lebih lambat
Waktu berlalu (detik) 0.17 1.18 7x lebih lambat
Pergantian konteks sukarela 1,196 159,786 167x lebih banyak
Utilisasi CPU 943% 745% 21% kurang efisien

Allocator Baru Tidak Membantu

Banyak developer berharap bahwa allocator mallocng yang lebih baru dari musl, yang diperkenalkan dalam versi 1.2.1, akan menyelesaikan masalah performa ini. Sayangnya, pengujian menunjukkan bahwa hal ini tidak memberikan perbedaan berarti untuk aplikasi multi-threaded. Tim pengembang musl merancang allocator baru ini untuk memprioritaskan penggunaan memori yang rendah dan keamanan daripada performa mentah.

Allocator mallocng dirancang untuk mengutamakan overhead memori yang sangat rendah, biaya fragmentasi worst-case yang rendah, dan penguatan yang kuat daripada performa.

Pilihan desain ini mencerminkan filosofi musl dalam menyediakan default yang aman sambil memungkinkan aplikasi untuk memilih allocator yang lebih cepat ketika diperlukan.

Solusi Sederhana Tersedia

Untungnya, memperbaiki masalah ini cukup mudah untuk sebagian besar aplikasi. Developer dapat dengan mudah mengganti dengan allocator alternatif seperti mimalloc atau jemalloc, yang menangani beban kerja multi-threaded dengan jauh lebih baik. Untuk proyek Rust, menambahkan beberapa baris ke file konfigurasi dapat menyelesaikan masalah sepenuhnya.

Perbaikan ini melibatkan penggunaan allocator yang berbeda secara kondisional hanya ketika building dengan musl, sehingga tidak mempengaruhi platform lain. Pendekatan ini memberikan developer manfaat dari ukuran kecil musl dan kompatibilitas cross-platform tanpa penalti performa.

Perbaikan Cepat untuk Proyek Rust

Tambahkan baris-baris berikut ke file Cargo.toml Anda untuk menghindari masalah performa alokator musl:

 Hindari alokator default musl karena masalah lock contention
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "1.4.0"

Alokator alternatif:

  • mimalloc: Desain heap modern per-thread
  • jemalloc: Alternatif matang yang banyak digunakan
  • tcmalloc: Thread-caching malloc milik Google

Ketika Performa Penting

Beberapa developer berpengalaman berpendapat bahwa mencapai batas performa allocator menunjukkan desain program yang buruk. Mereka menyarankan bahwa kode yang ditulis dengan baik harus meminimalkan alokasi memori di bagian yang kritis terhadap performa. Meskipun saran ini memiliki nilai, hal ini tidak memperhitungkan kenyataan bahwa banyak aplikasi perlu bekerja dengan baik dengan praktik coding yang wajar, bukan hanya yang dioptimalkan dengan sempurna.

Diskusi komunitas mengungkap perpecahan antara mereka yang melihat ini sebagai cacat fundamental dan mereka yang melihatnya sebagai trade-off yang dapat diterima untuk manfaat lain dari musl. Untuk aplikasi yang tidak sering mengalokasikan memori atau menggunakan lebih sedikit thread, dampak performa mungkin dapat diabaikan.

Referensi: Default musl allocator considered harmful (to performance)