2 poin oleh GN⁺ 2023-11-30 | 1 komentar | Bagikan ke WhatsApp
  • Titik awalnya adalah laporan bahwa pembacaan file di binding Python Apache OpenDAL lebih lambat daripada open().read() bawaan Python, tetapi bottleneck-nya ternyata bukan OpenDAL maupun PyO3 itu sendiri
  • Pada benchmark pembacaan file 64MiB, python-fs-read terukur sekitar 15–19ms, sementara Rust std::fs dan implementasi C sekitar 23ms, sehingga Rust/C tampak lebih lambat daripada Python
  • Saat ditelusuri dengan strace, eBPF, dan perf, perbedaannya terkait dengan offset tempat buffer tujuan syscall read berada di dalam halaman; penurunan performa dapat direproduksi di sekitar 0x10
  • Fenomena serupa dikonfirmasi pada lini AMD Ryzen 9 5900X, Ryzen 7 5700X, dan Ryzen 9 5900HX, dengan petunjuk utama berupa performa eksekusi rep movsb di dalam _copy_to_iter kernel
  • Python bukan secara intrinsik lebih cepat; hasil ini muncul dari kebetulan antara bug CPU terkait FSRM/rep movsb pada AMD Zen 3 dan offset memori, dan peningkatan dengan jemalloc juga bukan karena allocator itu sendiri, melainkan karena offset yang berbeda

Benchmark aneh yang bermula dari binding Python OpenDAL

  • Apache OpenDAL adalah lapisan akses data untuk membaca dan menulis data secara terpadu di berbagai layanan storage, dan binding Python-nya disediakan melalui PyO3
  • Pengguna melaporkan bahwa kode yang membaca file 150MB dengan binding Python OpenDAL lebih lambat daripada pembacaan file bawaan Python
    • open(...).read() bawaan Python 100 kali: 4.470868484000675
    • Binding Python OpenDAL 100 kali: 8.993250704006641
  • Bahkan pada pembacaan file 64MiB yang disederhanakan, binding OpenDAL tetap lebih lambat
    • python-fs-read: rata-rata 15.9ms
    • python-opendal-read: rata-rata 32.9ms
    • Pembacaan bawaan Python terukur 2.07 kali lebih cepat daripada binding OpenDAL

Penelusuran turun sampai Rust OpenDAL dan std::fs

  • Saat logika yang sama diimplementasikan dengan layanan fs OpenDAL di Rust, hasilnya juga lebih lambat daripada pembacaan bawaan Python
    • rust-opendal-fs-read: rata-rata 23.8ms
    • python-fs-read: rata-rata 15.6ms
    • Pembacaan bawaan Python terukur 1.52 kali lebih cepat daripada implementasi Rust OpenDAL
  • Karena layanan fs OpenDAL menggunakan std::fs milik Rust, implementasi berbasis std::fs dibuat terpisah untuk memeriksa biaya OpenDAL itu sendiri
  • Pada implementasi langsung Rust std::fs, pola yang sama terus muncul
    • rust-std-fs-read: rata-rata 23.1ms
    • python-fs-read: rata-rata 15.2ms
    • Pembacaan bawaan Python terukur 1.52 kali lebih cepat daripada Rust std::fs

Syscall dan mmap dilihat dengan strace

  • Analisis strace menunjukkan bahwa Rust maupun Python sama-sama memakai mmap untuk alokasi buffer besar
  • Eksekusi Rust std::fs membuka /tmp/file, membaca 64MiB sekali, memanggil read untuk memastikan EOF, lalu menutup file
  • Pembacaan bawaan Python menjalankan lebih banyak syscall seperti newfstatat, ioctl, dan lseek, tetapi total waktunya lebih singkat
  • Panggilan mmap(NULL, 67112960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) dipakai bukan untuk memetakan file, melainkan untuk alokasi memori anonim
    • 67112960 adalah ukuran 64MiB ditambah 4KiB
    • MAP_ANONYMOUS berarti alokasi memori yang tidak terkait dengan file
  • Build default Rust x86_64-unknown-linux-gnu menggunakan malloc dari glibc, dan glibc dapat memakai mmap untuk alokasi besar

Rust menjadi lebih cepat dengan jemalloc, lalu kesimpulan sementara dibalik

  • Ketika allocator global Rust diganti menjadi jemallocator::Jemalloc, hasilnya menjadi lebih cepat daripada Python
    • rust-std-fs-read-with-jemalloc: rata-rata 9.7ms
    • python-fs-read: rata-rata 15.8ms
    • Implementasi Rust yang memakai jemalloc terukur 1.64 kali lebih cepat daripada Python
  • Pada titik ini, penyebabnya tampak seperti mmap atau allocator memori default, tetapi interpretasi tersebut dikoreksi pada pembaruan berikutnya
  • Berdasarkan pembaruan 2023-12-01, perbedaannya bukan karena jemalloc, pymalloc, atau mimalloc secara intrinsik lebih cepat daripada glibc malloc
  • Perbedaan sebenarnya berasal dari offset di dalam halaman pada buffer yang dibuat oleh allocator
    • rust-std-fs-read: membaca pada offset 0x10 dari alamat awal mmap
    • rust-std-fs-read-with-jemalloc: membaca pada offset 0x740 dari alamat awal mmap
  • Rentang bermasalah diringkas sebagai 0x00..0x10 di dalam halaman, dan masalah yang sama juga dapat direproduksi dengan jemalloc

Masalah yang lebih bergantung pada perangkat daripada konfigurasi software

  • Seiring berlanjutnya diskusi, dikonfirmasi bahwa fenomena Rust lebih lambat daripada Python sangat menonjol pada mesin penulis
  • CPU penulis adalah AMD Ryzen 9 5950X 16-Core Processor, dengan memori DDR4 3200 MT/s 16GB DIMM
  • Mengubah berbagai pengaturan tidak menghilangkan selisih performa relatif
    • Mengaktifkan kembali mitigations=off kernel Linux tidak mengubah hasil
    • Mengubah Transparent Hugepage ke always, madvise, atau never mengubah nilai absolut, tetapi rasio relatif tetap sama
    • Mengikat proses ke core CPU tertentu dengan core_affinity menghasilkan hasil yang sama
  • Pengukuran latensi syscall read berbasis eBPF juga menunjukkan sisi Rust lebih lambat
    • Python read file: 8,134,049ns
    • Rust std::fs read file: 24,636,975ns
  • Dari pengamatan tersebut, sulit menjelaskan perbedaan hanya dengan OpenDAL, PyO3, atau standard library Rust; selisih waktu sudah muncul di level syscall

Petunjuk offset memori yang terlihat dari implementasi C

  • Implementasi pembacaan file 64MiB yang sama dengan C fopen/malloc/fread juga lebih lambat daripada Python
    • c-fs-read: rata-rata 23.8ms
    • python-fs-read: rata-rata 19.1ms
    • Pembacaan bawaan Python terukur 1.25 kali lebih cepat daripada implementasi C
  • Saat alamat pointer diperiksa dengan strace -e raw=read,mmap, offset awal buffer C dan Python berbeda
    • C: read pada offset 0x10 dari alamat yang dikembalikan mmap
    • Python: read pada offset 0x30 dari alamat yang dikembalikan mmap
  • Ketika offset disesuaikan dengan cara yang sama pada implementasi C, performa meningkat drastis
    • c-fs-read-with-offset: rata-rata 8.9ms
    • 2.15 kali lebih cepat daripada Python, dan 2.68 kali lebih cepat daripada implementasi C sebelumnya
  • Masalah ini juga direproduksi pada AMD Ryzen 9 5900X dan AMD Ryzen 7 5700X
  • Fenomena serupa juga dilaporkan di komunitas Rust dalam Std::fs::read slow?, dengan kaitan antara offset area memori dan performa syscall yang ditunjukkan

perf mengarah ke rep movsb

  • Seorang developer kernel mereproduksi c-fs-read dan versi dengan offset pada AMD Ryzen 9 5900HX, lalu menganalisisnya dengan perf
  • Nilai L1-dcache-prefetches dan L1-dcache-loads berbeda besar tergantung ada tidaknya offset
    • Tanpa offset: L1-dcache-loads sekitar 127,845,213, L1-dcache-prefetches sekitar 1,843,493
    • Dengan offset: L1-dcache-loads sekitar 13,965,813, L1-dcache-prefetches sekitar 395,578
  • Hotspot berada di jalur read kernel, dari shmem_file_read_itercopy_page_to_iter_copy_to_iter
  • Assembly inti di dalam _copy_to_iter adalah rep movsb, dan sebagian besar sample terkonsentrasi pada instruksi ini
  • Analisis lanjutan menyimpulkan bahwa petunjuk yang lebih penting bukan prefetch L1 itu sendiri, melainkan fenomena ketika performa rep movsb buruk pada data yang selaras halaman dan membaik ketika keselarasan halamannya terganggu

FSRM dan masalah AMD Zen 3

  • Laporan bug Ubuntu glibc yang dibagikan, Terrible memcpy performance on Zen 3 when using rep movsb, juga membahas masalah performa rep movsb
  • Contoh dalam laporan tersebut menjelaskan bahwa pada penyalinan 2113 byte, jalur rep movsb menunjukkan sekitar 3.2GB/s, dan ketika ukurannya diubah menjadi 2111 byte, meningkat menjadi lebih dari 100GB/s
  • FSRM adalah singkatan dari Fast Short REP MOV, fitur untuk mempercepat rep movsb dan rep movsd
  • FSRM adalah fitur yang dimulai dari Intel dan juga diperkenalkan di AMD; pada CPU yang menyatakan dukungan, glibc secara default menggunakan FSRM
  • Jadi, Python bukan secara intrinsik lebih cepat daripada C/Rust; kesimpulannya, bug CPU AMD membuat jalur pembacaan C/Rust melambat pada offset memori tertentu

Pembaruan: apakah AMD mengetahui masalah ini dan respons glibc

  • Menurut pembaruan 2023-12-01, AMD dipahami sudah mengetahui bug ini sejak 2021
  • Karena setelah artikel dipublikasikan beberapa pembaca mengirimkan tautannya ke AMD, penulis menganggap AMD mengetahui masalah tersebut
  • Penulis berpendapat AMD seharusnya bertanggung jawab memperbaiki bug ini di amd-ucode, tetapi menurut informasi yang belum terkonfirmasi, perbaikan amd-ucode pada Zen 3 mungkin sulit dilakukan
  • Harapan realistisnya adalah glibc menonaktifkan FSRM bila diperlukan
  • Di sisi glibc, pekerjaan x86: Improve ERMS usage on Zen3 sedang berlangsung

Kode reproduksi dan materi terkait

1 komentar

 
GN⁺ 2023-11-30
Komentar Hacker News
  • Ada sampai dua flag fitur CPU khusus yang menunjukkan bahwa REP STOS/MOV itu cepat dan bisa dipakai sebagai urutan instruksi pendek untuk memset/memcpy
    Selama puluhan tahun, tiap generasi CPU baru membuat orang harus menulis ulang rutin optimasi secara manual, jadi agak mengejutkan situasinya masih seperti ini; rasanya ini semestinya masuk ke suite pengujian timing milik vendor CPU

    • Ini murni dugaan, tetapi bisa saja pengaruh perbaikan bug yang masuk lewat pembaruan microcode pada saat-saat terakhir atau setelah rilis
      Mungkin ada masalah pada rep movs cepat yang page-aligned, atau rentan terhadap suatu serangan sehingga dinonaktifkan
    • Kalau pemahaman saya benar, saya penasaran apakah ini berarti untuk setiap build pada waktu kompilasi tertentu harus membuat dua executable, atau harus mengompilasi di hardware tertentu
      Saya juga tidak tahu seperti apa perbaikannya seharusnya, apakah perlu semacam pemeriksaan runtime atau tidak
    • Mudah untuk berpikir bahwa vendor CPU-lah yang paling memahami CPU mereka sendiri
      Kalau ada implementasi “software” yang lebih cepat, saya penasaran kenapa REP MOVS setidaknya tidak dibuat melakukan hal yang sama di microcode
  • Bug glibc yang terkait ada di sini. Namun yang ini untuk Zen 4: https://sourceware.org/bugzilla/show_bug.cgi?id=30994

  • Awalnya setelah membaca tulisan itu saya siap menertawakan penulis karena mengira ia salah memakai std::fs, tetapi ternyata ini tulisan yang menyenangkan, penuh lubang kelinci debugging dan misteri
    Tulisannya bagus dan sangat menarik

    • Benar-benar tulisan yang bagus. Cara debugging-nya cerdas, dengan membuat program uji lalu mengupas lapisan satu per satu; kesimpulannya menarik dan tak terduga, dan tulisannya jelas sehingga mudah diikuti
  • Premisnya agak membingungkan. Ini bukan membandingkan kode Python murni dengan kode native C/Rust, melainkan membandingkan metode baca file Python, yaitu wrapper Python di atas kode native, dengan OpenDAL, wrapper lain atas kode native
    Adanya perbedaan performa tetap menarik, tetapi menyebutnya “lebih lambat daripada Python” terasa cukup aneh. Apakah orang mengira seluruh pustaka standar Python ditulis dalam Python murni? Justru saya memperkirakan implementasi fungsi-fungsi pustaka standar Python bersifat native dan dioptimalkan tinggi secara terpisah
    Tidak mengejutkan bahwa kesimpulannya berkaitan dengan cara kerja kode native, tetapi jawaban spesifiknya memang tak terduga. Hanya saja awalnya membingungkan; tulisannya sendiri sangat menarik
    Selain itu, judul “C is slower than Python with specified offset” bagi penutur asli terbaca sebagai “C lebih lambat daripada Python meskipun offset ditentukan”. Padahal maksud sebenarnya kebalikannya: ketika offset yang dipakai di Python juga ditentukan di C, C menjadi lebih cepat

    • Saya justru kurang paham kenapa itu membingungkan
      Untuk tugas sesederhana membaca file, fakta bahwa pustaka standar Rust lebih lambat daripada pustaka standar Python adalah hal yang mengejutkan. Bahkan jika tahu bahwa panggilan pustaka standar Python semacam ini ditulis dalam C, orang tetap akan mengharapkan panggilan pustaka standar Rust memiliki kecepatan serupa
      Jadi biasanya orang akan menduga ada kesalahan penggunaan atau ada perilaku aneh di pustaka standar Rust, tetapi kali ini keduanya bukan penyebabnya; ternyata ini adalah tebing performa pada hardware tertentu yang muncul bergantung pada alignment alokasi
      Kita memang mengharapkan pembacaan sistem berkas di Python teroptimasi dengan baik, tetapi saya juga berpikir hal yang sama berlaku di Rust, jadi mengejutkan bahwa sisi Rust jauh lebih lambat, dan lebih mengejutkan lagi karena bergantung pada hardware dan allocator
    • Saya tidak paham kenapa ketika Python lambat ia dicela sebagai bahasa yang lambat, tetapi ketika cepat ia tidak diakui dengan alasan “itu bukan Python yang sebenarnya”
      Kalau kode yang saya tulis dengan Python berjalan cepat, bagi saya itu berarti Python itu cepat. Apakah implementasinya memakai bahasa lain atau karena alasan lain tidak terlalu penting
    • Saya tidak tahu apa alasan untuk mengharapkan bahwa semuanya “dioptimalkan tinggi secara terpisah”
      Yang terjadi di artikel asli nyaris murni kebetulan. Kode C CPython bahkan tidak terlalu memedulikan konsistensi const, dan banyak memakai alokasi memori dinamis serta panggilan pembantu/kemudahan. Bahkan operasi aritmetika pun melakukan alokasi memori dinamis
      Kalau punya pengalaman bekerja dengan CPython, biasanya Anda tidak akan mengharapkan performa yang baik. Saat ingin meningkatkan performa, Anda cenderung mencoba melewati fungsi-fungsi yang disediakannya
      Selain itu, Python tidak memiliki standar, jadi secara ketat tidak ada pustaka standar; pustaka yang didistribusikan bersamanya sebagian besar ditulis dalam Python. Sebagian memang ditulis dalam C, tetapi di antara kode C itu pun cukup banyak yang pada dasarnya merupakan hasil pemindahan mekanis kode Python ke C. Misalnya implementasi binary search di Python awalnya ditulis dalam Python, lalu kemudian diterjemahkan ke C menggunakan Python C API
      Yang masih masuk akal untuk diharapkan hanyalah bahwa fitur yang memetakan langsung ke fungsi sistem operasi memiliki wrapper yang relatif tipis. Artinya, pembacaan file pada dasarnya langsung masuk ke antarmuka sistem, sehingga tidak membutuhkan banyak kode binding
    • Terima kasih atas koreksinya. Judulnya sudah saya perbaiki
    • Premisnya adalah bahwa kalau memakai frasa seperti “Python lebih cepat daripada Rust”, walaupun tidak benar, tetap akan menghasilkan pageview
      Setelah puluhan tulisan serupa muncul, semua orang akhirnya menyadarinya
  • Tulisannya sendiri sangat bagus dan memuat banyak informasi menarik terkait isu ini
    Namun bagian yang lebih menarik perhatian dan mengkhawatirkan adalah bagaimana isu ini dilaporkan dan dicatat, serta bagaimana komunikasinya ditangani
    Laporannya dilakukan di Discord, lingkungan tertutup yang tidak diindeks, sulit dicari, dan tidak terarsip. Diskusi berlangsung di Discord dan Telegram; dalam konteks ini, Telegram mungkin bahkan lebih buruk
    Tulisan blog ini dan repositori GitHub adalah satu-satunya jejak yang tersisa. Jika Xuanwo tidak menuliskannya di blog, ini akan hilang begitu saja dalam linimasa. Situasi yang cukup menarik

    • Memang benar itu platform tertutup dan itu tidak baik. Namun tuduhan bahwa tidak bisa diindeks atau dicari sulit saya terima
      Hampir tidak ada messenger yang secara bawaan mengindeks dan menyediakan pencarian atas log yang dapat diakses publik. Tidak semua server IRC menyediakan log publik, begitu pula grup Matrix. Saya tidak paham mengapa diskusi di sana dianggap tidak akan hilang begitu saja dalam linimasa
      Alasan log publik bisa disediakan bukan karena platformnya tidak tertutup, melainkan karena ada API yang memungkinkan logging. Telegram juga punya API seperti itu, dan log grup diskusi kami yang bisa dicari dapat dilihat di sini: https://luoxu-web.vercel.app/#g=1264662201
      Tidak adanya pengindeksan publik terutama karena privasi, bukan karena platformnya tertutup
    • Inilah alasan mengapa setiap kali saya merindukan runtuhnya USENET, saya tidak bisa menerima jawaban “sekarang kan ada Discord”
      Dulu semua tulisan bisa dicari dengan rapi di DejaNews, lalu kemudian di Google
      Komunikasi penting dalam proyek open source penting seperti stack Internet/WWW serta alat dan library pemrograman inti seharusnya kembali menggunakan standar terbuka
  • Ini tulisan paling menarik yang saya baca minggu ini. Rangkuman yang luar biasa

  • Hal yang jelas harus dilakukan tampaknya adalah mengirim patch ke metode kernel copy_user_generic
    Jika CPU yang bermasalah terdeteksi dan memicu bug yang membuat penyelarasan memori melambat, cukup gunakan implementasi penyalinan memori lain

    • Itu tidak jelas. Jika bisa diperbaiki lewat microcode, tampaknya lebih baik membuat orang memakai microcode yang diperbarui daripada menebarkan kode perbaikan untuk masalah yang pada dasarnya bisa dipatch lewat software ke seluruh kernel
      Perbaikan yang bisa diterima bagi orang tanpa pengalaman kernel kemungkinan tidak sepele. Yang lebih penting, cara mengaktifkan workaround itu juga tidak jelas. Mungkin yang terbaik adalah mengukurnya saat boot, karena kalau tidak, agak sulit mengetahui model dan stepping mana yang terdampak
    • Ini bukan perbaikan sepele. AMD perlu mencari tahu mengapa aliasing rusak pada alamat yang mendekati penyelarasan halaman, jadi perbaikannya kemungkinan ada di sisi microcode
      Mitigasi software juga akan rumit. Kernel sebenarnya tidak bisa memakai instruksi vektor yang biasanya digunakan pada jalur alternatif ketika ERMS tidak dapat digunakan
  • jemalloc adalah allocator default Rust sampai 2018
    https://internals.rust-lang.org/t/jemalloc-was-just-removed-...

  • Bagian yang mengatakan “developer Rust bisa mempertimbangkan beralih ke jemallocator untuk meningkatkan performa” membuat penasaran
    Apakah siapa pun bisa mendapatkan peningkatan performa yang nyaris gratis, atau ada hal yang perlu diperhatikan? Juga penasaran apakah codebase C bisa mendapat manfaat, dan apakah ini performa yang saat ini begitu saja terlewatkan

    • Perlu diketahui bahwa memakai jemalloc dapat menimbulkan masalah observability karena MADV_FREE. htop tidak lagi menampilkan memori yang benar-benar sedang digunakan secara akurat
      https://github.com/jemalloc/jemalloc/issues/387#issuecomment...
      https://gitlab.haskell.org/ghc/ghc/-/issues/17411
      Sekarang tampaknya jemalloc memanggil MADV_DONTNEED 10 detik setelah MADV_FREE: https://github.com/JuliaLang/julia/issues/51086#issuecomment...
      Jadi ini memang “memperbaiki” isu tersebut, tetapi ada jeda yang membingungkan antara saat memori dibebaskan dan saat fakta itu terlihat di htop
      Namun menurut https://jemalloc.net/jemalloc.3.html, jeda bisa dihilangkan dengan menyetel opt.muzzy_decay_ms = 0
      Meski begitu, pembuat musl masih berhati-hati untuk menjadikan jemalloc sebagai default: https://www.openwall.com/lists/musl/2018/04/23/2
      Intinya, ada masalah seperti pembengkakan serius, melemahnya ASLR, dan optimasi yang condong ke membuatnya secepat mungkin tanpa terlalu peduli pada penggunaan memori. Nilai tuning di atas mungkin bisa meredakannya sampai taraf tertentu, tetapi kecenderungan umum antara fokus pada performa atau penggunaan memori kemungkinan besar tetap menjadi trade-off
    • Menurut saya ini performa yang terlewatkan hampir gratis. Biayanya hanya ukuran binary yang sedikit bertambah
      Belum tentu lebih cepat di semua situasi, tetapi hampir di sebagian besar kasus akan lebih cepat. Rust dulu juga memakai jemalloc sebagai default, tetapi diubah karena ada orang-orang yang menganggapnya mengejutkan untuk sebuah default
    • Mengganti allocator ke yang bukan default tidak selalu meningkatkan performa
      Ini sangat bergantung pada workload, jadi perlu profiling dan benchmarking. Meski begitu, bahasa tingkat rendah seperti C/C++/Rust memang perlu bisa memilih allocator semacam ini
      Satu hal yang perlu diperhatikan adalah ukuran binary. Allocator kustom menambahkan byte ekstra ke executable
    • Rust dulu memakai jemalloc sebagai default, tetapi sekitar 2018 kembali ke malloc sistem[0]
      Sekarang Rust memiliki trait GlobalAlloc dan atribut #[global_allocator], sehingga jika aplikasi menginginkannya, jemalloc bisa dipakai sebagai allocator. Saya tidak yakin apakah pengguna bisa menimpanya dengan cara seperti LD_PRELOAD
      jemalloc tidak selalu menjadi pilihan terbaik untuk semua workload dan use case. Allocator sistem sering kali jauh dari sempurna, tetapi setidaknya sudah banyak diuji sebagai allocator serbaguna
      [0] https://github.com/rust-lang/rust/issues/36963
    • Performa bukan skala satu dimensi yang menggerakkan program dari “lambat” ke “cepat”. Selalu ada faktor lain yang ikut bermain
      jemalloc mungkin pilihan yang tepat untuk sebagian aplikasi, tetapi dalam kasus lain allocator lain bisa lebih cepat. Atau meski lebih lambat, bisa jadi lebih sesuai dengan tujuan seperti dirty memory yang lebih sedikit, observability yang lebih baik, atau jaminan keamanan tertentu
  • Saya sudah mengirimkan ini ke orang-orang yang tepat

    • Maksudnya dikirim ke pihak AMD?