1 poin oleh GN⁺ 2024-08-27 | 1 komentar | Bagikan ke WhatsApp
  • 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_write ikut terakumulasi
  • vmsplice menghubungkan buffer ruang pengguna ke pipe tanpa menyalinnya ke kernel, sehingga melewati jalur mahal seperti __alloc_pages, _raw_spin_lock_irq, dan copy_user_enhanced_fast_string
  • Dalam contoh throughput Fizz Buzz, solusi yang memakai vmsplice mencapai 60.8GiB/s pada satu core dan 208.3GiB/s pada multi-core, sementara contoh vmsplice dalam 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 vmsplice untuk 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 vmsplice berada di kisaran beberapa GiB per detik, dan solusi neil mencapai 8.4GiB/s
    • Solusi yang memakai vmsplice naik hingga 15.5GiB/s milik tkluck, 60.8GiB/s milik ais523, dan 208.3GiB/s multi-core milik david
  • 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 ftrace menunjukkan 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 stos digunakan dalam semua kasus

Biaya nyata saat menulis ke pipe dengan write

  • Ketika buffer berukuran sama ditulis ke pipe lewat stdout.write() dan dikonsumsi dengan pv >/dev/null, throughput turun menjadi 17GB/s
  • Hasil profiling menunjukkan sebagian besar waktu dihabiskan di dalam system call write, terutama 95% berada di dalam pipe_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 pipe
    • copy_user_enhanced_fast_string: sekitar 20% dari total waktu, menyalin data dari ruang pengguna ke sisi kernel
  • Karena pv mengonsumsi dengan memindahkan halaman ke /dev/null menggunakan splice, sulit untuk terus memakai ulang beberapa halaman yang sama dalam loop

Rutinitas penyalinan kernel dan interpretasi yang dikoreksi

  • Jika copy_user_enhanced_fast_string dibongkar dengan disassembly, penyalinan buffer besar diproses dengan instruksi REP 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 movsb dari 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

  • vmsplice mengurangi biaya penggunaan pipe dengan meneruskan seluruh buffer ruang pengguna ke pipe tanpa menyalinnya ke kernel
  • Contoh ./write yang disertakan dalam pipes-speed-test milik 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, dan copy_user_enhanced_fast_string yang terlihat pada jalur write tidak muncul
  • Sebagai gantinya, add_to_pipe, import_iovec, dan iov_iter_get_pages2 muncul sebagai jalur utama, menunjukkan bahwa vmsplice melewati bagian mahal dari write

Kesimpulan yang tersisa dan hal yang perlu diperhatikan

  • Dalam eksperimen, jalur menulis ke pipe Linux dengan write sekitar 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 splice serta vmsplice melewatinya
  • 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 write dan cara pemrosesan buffer juga perlu dilihat bersama

1 komentar

 
GN⁺ 2024-08-27
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

    • Setidaknya untuk penggunaan di user space, saya tidak yakin fitur kernel baru benar-benar diperlukan. Dulu saya pernah mengimplementasikan ring buffer user-space single-producer/single-consumer yang cukup mirip dengan perilaku pipe menggunakan eventfd
      Saat ring buffer penuh atau kosong, ia bisa sleep/poll, dan selain itu berjalan tanpa lock maupun overhead system call
    • Saya penasaran apakah ada rencana mekanisme sinyal standar untuk memberi tahu pihak lain bahwa kedua ujung pipe mendukung ring buffer. Dengan begitu libc bisa menanganinya secara transparan; kalau tidak, khusus untuk pipe saya kurang melihat keuntungannya dibanding shared memory dan sinkronisasi futex
    • Mungkin ringbuffer_wait() juga bisa memberi sinyal dengan membuatnya muncul sebagai status dapat dibaca di poll()
    • Menarik apakah antarmuka ring buffer yang sudah ada akan memakai ini, atau justru terjadi situasi xkcd927. Bagaimanapun, ini tampak seperti percobaan yang menarik
    • Buffering ada karena suatu alasan, dan pendekatan ini bisa menciptakan mode kegagalan aneh serta kerentanan dalam skrip. Intinya, produsen stream apa pun bisa lebih lambat daripada konsumen tertentu
      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 JMP bukan sekadar RET adalah opsi CONFIG_RETHUNK. Yang terlihat dalam disassembly objdump adalah hasil penggantian RET menjadi JMP __x86_return_thunk
    https://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/...

    • Walaupun tahu soal seperti ini, kemungkinan hanya segelintir developer kernel yang masih akan memilih nama samaran iseng sekali pakai seperti itu
  • 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

    • Ini bukan kode proyek yang hanya akan dijalankan beberapa miliar kali sepanjang masa pakainya, melainkan kode yang sering dipakai di ratusan juta hingga miliaran komputer
      Jadi bahkan peningkatan efisiensi yang sangat kecil pun tetap masuk akal secara ekonomi untuk dioptimalkan dengan usaha besar
    • Dalam kasus penulisnya sendiri, pipe yang dibilang lambat itu sebenarnya memindahkan data pada 17GB/s, yaitu lebih dari 130Gbps
      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
    • Ada proyek yang memakai SDK proprietari untuk mendekode video mentah. Data hasil dekode dikeluarkan sebagai RGBA murni, lalu dibaca oleh FFMpeg melalui pipe untuk di-encode ulang ke codec standar
      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
    • Membuat sesuatu yang dipakai di mana-mana menjadi beberapa persen lebih cepat adalah investasi yang sangat layak. Meskipun tiap pekerjaan individual tidak jauh lebih cepat, jika dijumlahkan secara global, itu menghemat listrik dan waktu dalam jumlah besar
    • Terkadang Corolla yang lebih cepat memang benar-benar jawaban terbaik
      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 movsb secepat versi tervektorisasi tercepat. Ini karena CPU tahu untuk mempercepat instruksi tersebut
    Nama fungsi kernel copy_user_enhanced_fast_string juga mengisyaratkan hal itu. Fitur CPU terkait adalah ERMS (Enhanced Repeat Move String, membuat rep movsb cepat di atas panjang tertentu) dan FSRM (Fast Short Repeat Move String, membuat penyalinan pendek juga cepat)

    • Tapi bukan cuma itu. rep movsb memang cepat sampai ambang tertentu, tetapi setelah itu penyimpanan biasa atau non-temporal store lebih cepat
      Semua 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
    • Hal lain yang menarik adalah Linux telah beberapa kali mengubah cara memakai ERMS dan FSRM untuk penyalinan x86 sejak kernel 6.1 yang dipakai di artikel. Sebagai referensi, di mesin saya yang memiliki FSRM dan ERMS — dan mengejutkannya, yang pertama tidak mengimplikasikan yang kedua — Linux 6.8 menghasilkan 17GB/s hanya dengan pipe biasa dan buffer 32KiB
    • Untuk memcpy pendek, saya masih menunggu sampai rep movsb dan rep stosb cukup cepat sehingga versi loop C sederhana bisa dihapus
    • Kalau begitu, jadi penasaran kapan compiler C akan meng-inline memcpy() dengan panjang variabel, seperti halnya mereka meng-inline memcpy dengan panjang tetap
  • Ada 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...

    • Itu hanya berlaku pada model CPU Intel tertentu
  • Hampir semua bentuk komunikasi antarproses itu “lambat”. Ini adalah pilihan untuk menukar performa demi keamanan

    • Tidak perlu menanggung biaya sebesar itu. Pipe hampir tidak memberikan apa-apa, jadi biayanya juga seharusnya hampir nol
      Khususnya, tidak banyak alasan mengapa komunikasi antarproses tercepat harus lebih lambat daripada pemanggilan fungsi yang panjang
    • Pipe ada bukan demi keamanan, melainkan sebagai optimisasi untuk mengalirkan data antarprogram yang sudah ada
  • 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 splice harus selambat itu. Sebagai alasan mengapa ia lebih lambat daripada vmsplice, disebutkan alokasi buffer dan penggunaan instruksi skalar, tetapi saya tidak mengerti mengapa itu diperlukan
    Kenapa splice tidak bisa diimplementasikan ulang begitu saja seperti vmsplice? Pasti ada alasan bagusnya, tapi sepertinya saya melewatkannya

  • Versi 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

    • Karena tulisan ini naik sampai ke puncak Hacker News, sepertinya memang perlu sedikit dimaklumi
      Tulisannya sendiri tampak bagus, dan ada banyak hal untuk dipelajari tentang apa yang terjadi di balik layar