Penurunan performa pipe Linux
(qsantos.fr)- Saat menulis data ke pipe Linux dengan
write, performanya jauh lebih lambat daripada penulisan memori sederhana; dalam lingkungan pengujian, hanya mencapai sekitar 17GB/s untuk penulisan pipe dibanding 167GB/s untuk penulisan buffer memori - Bottleneck tidak bisa dijelaskan hanya oleh satu operasi penyalinan data; biaya alokasi halaman, locking, dan rutinitas penyalinan kernel di dalam
pipe_writeikut terakumulasi vmsplicemenghubungkan buffer ruang pengguna ke pipe tanpa menyalinnya ke kernel, sehingga melewati jalur mahal seperti__alloc_pages,_raw_spin_lock_irq, dancopy_user_enhanced_fast_string- Dalam contoh throughput Fizz Buzz, solusi yang memakai
vmsplicemencapai 60.8GiB/s pada satu core dan 208.3GiB/s pada multi-core, sementara contohvmsplicedalam eksperimen terpisah mencatat 210GB/s - Karena adanya koreksi, interpretasi tentang penalti tidak memakai SIMD sulit dipercaya, dan komunikasi antarproses tidak selesai hanya dengan cache L1 sehingga 167GB/s sulit diharapkan sebagai throughput pipe nyata
Titik awal: kesenjangan besar yang dibuat vmsplice
- Sebagian program memakai system call
vmspliceuntuk memindahkan data lebih cepat melalui pipe - Dalam kompetisi throughput Fizz Buzz di Code Golf StackExchange, solusi terbagi menjadi dua kelompok besar
- Solusi yang tidak memakai
vmspliceberada di kisaran beberapa GiB per detik, dan solusi neil mencapai 8.4GiB/s - Solusi yang memakai
vmsplicenaik hingga 15.5GiB/s milik tkluck, 60.8GiB/s milik ais523, dan 208.3GiB/s multi-core milik david
- Solusi yang tidak memakai
- Sulit menjelaskan selisih sekitar 7 kali lipat pada satu core hanya dengan efek mengurangi penyalinan antara ruang kernel dan ruang pengguna
- Dalam eksperimen sendiri, solusi ais523 mencatat 96.4GiB/s, sementara solusi david mencatat 277GB/s saat memakai 7 core, sekitar 40GB/s per core
Baseline: penulisan memori di ruang pengguna
- Program Rust yang berulang kali menyalin buffer 32KiB di memori ruang pengguna tanpa system call mencatat 167GB/s dalam lingkungan pengujian
- Angka ini dianggap selevel dengan kecepatan tulis cache L1 CPU yang digunakan
- Sistem pengujian memakai Ryzen 9 7950X3D, DDR5 6000T/s, Debian 12, dan Linux 6.1.0-18-amd64
- Opsi mitigasi CPU dinonaktifkan dengan
mitigations=off
- Profiling
ftracemenunjukkan sebagian besar waktu dihabiskan di__memset_avx512_unaligned_erms - Namun, koreksi membatasi interpretasi ini
- Instruksi pada titik interupsi adalah
rep stos, dan ini bukan instruksi AVX-512 - Alasan throughput tetap 167GB/s meski dibatasi hanya memakai AVX2 dan SSE2 juga karena
rep stosdigunakan dalam semua kasus
- Instruksi pada titik interupsi adalah
Biaya nyata saat menulis ke pipe dengan write
- Ketika buffer berukuran sama ditulis ke pipe lewat
stdout.write()dan dikonsumsi denganpv >/dev/null, throughput turun menjadi 17GB/s - Hasil profiling menunjukkan sebagian besar waktu dihabiskan di dalam system call
write, terutama 95% berada di dalampipe_write - Di dalam
pipe_write, persiapan halaman, locking, dan penyalinan bersama-sama menimbulkan biaya__alloc_pages: 36% dari total waktu, menyiapkan halaman memori baru untuk pipe__mutex_lock.constprop.0: 25% dari total waktu, menjadi biaya locking untuk penulisan pipe_raw_spin_lock_irq: 5% dari total waktu, muncul sebagai biaya locking terkait penulisan pipecopy_user_enhanced_fast_string: sekitar 20% dari total waktu, menyalin data dari ruang pengguna ke sisi kernel
- Karena
pvmengonsumsi dengan memindahkan halaman ke/dev/nullmenggunakansplice, sulit untuk terus memakai ulang beberapa halaman yang sama dalam loop
Rutinitas penyalinan kernel dan interpretasi yang dikoreksi
- Jika
copy_user_enhanced_fast_stringdibongkar dengan disassembly, penyalinan buffer besar diproses dengan instruksiREP MOV - Fungsi ini diimplementasikan bukan dalam C, melainkan dalam kode assembly kernel Linux, sehingga ini adalah jalur yang disengaja, bukan kegagalan optimasi compiler
- Dalam eksperimen awal, pemanggilan langsung
rep movsbdari ruang pengguna menghasilkan 80GB/s, lalu ini dikaitkan dengan interpretasi bahwa rutinitas penyalinan kernel sekitar 2 kali lebih lambat - Setelah koreksi, kondisi eksperimennya berubah
- Dua buffer 32KiB membuat cache data L1 jenuh
- Jika memakai buffer 16KiB, performa naik menjadi 153GB/s
- Karena itu, interpretasi awal bahwa tidak memakai instruksi vektor dalam penyalinan kernel adalah penalti besar sulit dipercaya
- Meski demikian, poin bahwa overhead manajemen memori dalam penulisan pipe itu besar tetap berlaku
Jalur kernel yang dihindari vmsplice
vmsplicemengurangi biaya penggunaan pipe dengan meneruskan seluruh buffer ruang pengguna ke pipe tanpa menyalinnya ke kernel- Contoh
./writeyang disertakan dalampipes-speed-testmilik Francesco digunakan sebagai contoh minimal yang menulis'X'tanpa henti - Contoh ini mencatat 210GB/s, tetapi berbeda dari pekerjaan pembuatan data umum karena buffer yang sama berulang kali diserahkan ke
vmsplice- Jika bukan stream byte konstan, data baru harus diisi ke buffer
- Saat itu, batas atas penulisan memori sederhana sebesar 167GB/s kembali relevan
- Pada jalur
vmsplice, 37% waktu tetap dihabiskan di__mutex_lock.constprop.0 - Namun,
__alloc_pages,_raw_spin_lock_irq, dancopy_user_enhanced_fast_stringyang terlihat pada jalurwritetidak muncul - Sebagai gantinya,
add_to_pipe,import_iovec, daniov_iter_get_pages2muncul sebagai jalur utama, menunjukkan bahwavmsplicemelewati bagian mahal dariwrite
Kesimpulan yang tersisa dan hal yang perlu diperhatikan
- Dalam eksperimen, jalur menulis ke pipe Linux dengan
writesekitar 10 kali lebih lambat daripada penulisan memori sederhana - Kesimpulan awalnya adalah bahwa biaya locking dan biaya simpan-pulih konteks SIMD dalam penulisan pipe besar, dan
splicesertavmsplicemelewatinya - Setelah koreksi, kesimpulan harus dilihat dengan lebih terbatas
- Overhead manajemen memori kernel tetap merupakan faktor penting dalam penurunan performa pipe
- Interpretasi bahwa tidak memakai instruksi vektor menimbulkan penalti sebesar yang diperkirakan tidak akurat
- Karena komunikasi antarproses tidak bisa berlangsung hanya dengan cache L1, 167GB/s sulit dicapai sebagai throughput yang mencakup pembacaan pipe nyata
- Beberapa kesalahan besar telah dikoreksi dan reliabilitas hasil bisa terbatas, jadi angka-angkanya sebaiknya ditafsirkan sebagai indikasi arah
- Untuk meningkatkan throughput pipe, jangan hanya mengurangi jumlah system call; jalur internal kernel yang dilalui
writedan cara pemrosesan buffer juga perlu dilihat bersama
1 komentar
Komentar Hacker News
Ada side project yang mencoba menangani masalah ini: https://lwn.net/Articles/976836/
Idenya adalah membuat system call untuk mendapatkan ring buffer bagi semua file descriptor yang didukung. Ini juga mencakup pipe; jika kedua ujung mendukung penggunaan ring buffer, keduanya dapat memetakan ring buffer yang sama untuk melakukan I/O tanpa salin, dan dalam beberapa kasus bahkan mungkin tidak perlu memanggil kernel sama sekali. Mereka sedang mencari orang untuk ikut mengerjakannya
Saat ring buffer penuh atau kosong, ia bisa sleep/poll, dan selain itu berjalan tanpa lock maupun overhead system call
ringbuffer_wait()juga bisa memberi sinyal dengan membuatnya muncul sebagai status dapat dibaca dipoll()Sedikit hiccup sesaat saja sudah cukup untuk membuat pipe benar-benar kacau jika buffering yang ada tidak memadai, dan ukuran buffer yang dibutuhkan berbeda-beda di tiap sistem
Alasan
JMPbukan sekadarRETadalah opsi CONFIG_RETHUNK. Yang terlihat dalam disassemblyobjdumpadalah hasil penggantianRETmenjadiJMP __x86_return_thunkhttps://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/lib/ret...
Instruksi NOP di awal dan akhir fungsi bukan untuk ftrace, melainkan berasal dari makro ASM_CLAC/ASM_STAC. Makro ini menyisakan ruang yang saat runtime akan diisi dengan instruksi CLAC/STAC jika X86_FEATURE_SMAP terdeteksi. Kedua instruksi itu sama-sama 3 byte, sama seperti jumlah NOP-nya
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/kernel/...
Menyebut Linux pipe itu “lambat” seperti menyebut Toyota Corolla itu “lambat”. Kalau bukan untuk kasus penggunaan ekstrem, performanya sudah lebih dari cukup
Apakah Anda sedang balapan mobil? Dan apakah itu cabang yang lebih mengutamakan kecepatan daripada teknik? Kalau ya, beli saja mobil yang lebih cepat. Kalau tidak, terus pakai Corolla
Jadi bahkan peningkatan efisiensi yang sangat kecil pun tetap masuk akal secara ekonomi untuk dioptimalkan dengan usaha besar
Selama lebih dari 10 tahun saya memakai pipe untuk berbagai hal, saya belum pernah terhambat oleh kecepatannya; biasanya yang menjadi bottleneck justru alat seperti tar, gzip, find, grep, atau nc. Tentu saja, alat-alat itu juga sudah cukup cepat untuk pekerjaan yang mereka lakukan
FFMpeg tidak bisa menyertakan SDK Non-Free di source-nya, dan menyimpan RGBA murni ke file itu sangat tidak realistis. Jadi pipe adalah satu-satunya cara, dan ada alasan yang sah untuk membutuhkan pipe throughput tinggi
https://www.toyota.com/grcorolla/
Mobil-mobil ini punya engineering dan performa yang luar biasa, dan juga semacam hack untuk mengakali aturan yang membuat GR Yaris—yang awalnya memang dimaksudkan—sulit dibawa ke pasar AS. Menurut saya, konteks engineering/performa/hacking/pasar di sini cukup kuat sehingga orang HN bisa menerima analogi ini dengan santai. Lagi pula, presiden perusahaannya juga masih mengemudikannya sendiri
Terlepas dari pokok utama tulisan ini, pada CPU modern
rep movsbsecepat versi tervektorisasi tercepat. Ini karena CPU tahu untuk mempercepat instruksi tersebutNama fungsi kernel
copy_user_enhanced_fast_stringjuga mengisyaratkan hal itu. Fitur CPU terkait adalah ERMS (Enhanced Repeat Move String, membuatrep movsbcepat di atas panjang tertentu) dan FSRM (Fast Short Repeat Move String, membuat penyalinan pendek juga cepat)rep movsbmemang cepat sampai ambang tertentu, tetapi setelah itu penyimpanan biasa atau non-temporal store lebih cepatSemua ambang dijelaskan di https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch...
Dan nilai-nilai ini juga tidak tetap; Noah Goldstein masih memperbaruinya tiap tahun
memcpypendek, saya masih menunggu sampairep movsbdanrep stosbcukup cepat sehingga versi loop C sederhana bisa dihapusmemcpy()dengan panjang variabel, seperti halnya mereka meng-inlinememcpydengan panjang tetapAda bagian tentang AVX512 yang tidak saya lihat di tulisan ini: selain overhead
xsave/xrstor, AVX512 juga boros daya dan memicu penskalaan frekuensi CPU. Untuk detail dan betapa subtilnya hal ini, lihat [1], [2][1] https://www.intel.com/content/dam/www/central-libraries/us/e...
[2] https://www.intel.com/content/www/us/en/developer/articles/t...
Hampir semua bentuk komunikasi antarproses itu “lambat”. Ini adalah pilihan untuk menukar performa demi keamanan
Khususnya, tidak banyak alasan mengapa komunikasi antarproses tercepat harus lebih lambat daripada pemanggilan fungsi yang panjang
Ini juga sedang terkena hug of death dari Hacker News. Berkat page caching WordPress, kali ini lebih baik daripada sebelumnya, tetapi mohon maklum kalau memuat halaman tetap butuh beberapa detik
Awalnya saya tidak begitu paham mengapa
spliceharus selambat itu. Sebagai alasan mengapa ia lebih lambat daripadavmsplice, disebutkan alokasi buffer dan penggunaan instruksi skalar, tetapi saya tidak mengerti mengapa itu diperlukanKenapa
splicetidak bisa diimplementasikan ulang begitu saja sepertivmsplice? Pasti ada alasan bagusnya, tapi sepertinya saya melewatkannyavmsplicetidak bekerja untuk semua jenis file descriptorVersi yang memakai io_uring juga akan menarik untuk dilihat. Kernel dan buffer bisa dibagikan lebih awal sehingga sebagian penyalinan bisa dihindari, dan overhead system call juga mungkin bisa dihindari. Namun, yang terakhir tampaknya hampir bisa diabaikan di sini
Ini klaim yang cukup berani untuk blog yang butuh sekitar 20 detik untuk dimuat
Tulisannya sendiri tampak bagus, dan ada banyak hal untuk dipelajari tentang apa yang terjadi di balik layar