- 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-readterukur sekitar 15–19ms, sementara Ruststd::fsdan implementasi C sekitar 23ms, sehingga Rust/C tampak lebih lambat daripada Python - Saat ditelusuri dengan
strace,eBPF, danperf, perbedaannya terkait dengan offset tempat buffer tujuan syscallreadberada di dalam halaman; penurunan performa dapat direproduksi di sekitar0x10 - Fenomena serupa dikonfirmasi pada lini AMD Ryzen 9 5900X, Ryzen 7 5700X, dan Ryzen 9 5900HX, dengan petunjuk utama berupa performa eksekusi
rep movsbdi dalam_copy_to_iterkernel - Python bukan secara intrinsik lebih cepat; hasil ini muncul dari kebetulan antara bug CPU terkait FSRM/
rep movsbpada AMD Zen 3 dan offset memori, dan peningkatan denganjemallocjuga 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.9mspython-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
fsOpenDAL di Rust, hasilnya juga lebih lambat daripada pembacaan bawaan Pythonrust-opendal-fs-read: rata-rata 23.8mspython-fs-read: rata-rata 15.6ms- Pembacaan bawaan Python terukur 1.52 kali lebih cepat daripada implementasi Rust OpenDAL
- Karena layanan
fsOpenDAL menggunakan std::fs milik Rust, implementasi berbasisstd::fsdibuat terpisah untuk memeriksa biaya OpenDAL itu sendiri - Pada implementasi langsung Rust
std::fs, pola yang sama terus munculrust-std-fs-read: rata-rata 23.1mspython-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
stracemenunjukkan bahwa Rust maupun Python sama-sama memakaimmapuntuk alokasi buffer besar - Eksekusi Rust
std::fsmembuka/tmp/file, membaca 64MiB sekali, memanggilreaduntuk memastikan EOF, lalu menutup file - Pembacaan bawaan Python menjalankan lebih banyak syscall seperti
newfstatat,ioctl, danlseek, 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 anonim67112960adalah ukuran 64MiB ditambah 4KiBMAP_ANONYMOUSberarti alokasi memori yang tidak terkait dengan file
- Build default Rust
x86_64-unknown-linux-gnumenggunakanmallocdariglibc, danglibcdapat memakaimmapuntuk 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 Pythonrust-std-fs-read-with-jemalloc: rata-rata 9.7mspython-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
mmapatau allocator memori default, tetapi interpretasi tersebut dikoreksi pada pembaruan berikutnya - Berdasarkan pembaruan 2023-12-01, perbedaannya bukan karena
jemalloc,pymalloc, ataumimallocsecara intrinsik lebih cepat daripadaglibc malloc - Perbedaan sebenarnya berasal dari offset di dalam halaman pada buffer yang dibuat oleh allocator
rust-std-fs-read: membaca pada offset0x10dari alamat awalmmaprust-std-fs-read-with-jemalloc: membaca pada offset0x740dari alamat awalmmap
- Rentang bermasalah diringkas sebagai
0x00..0x10di dalam halaman, dan masalah yang sama juga dapat direproduksi denganjemalloc
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=offkernel Linux tidak mengubah hasil - Mengubah Transparent Hugepage ke
always,madvise, ataunevermengubah nilai absolut, tetapi rasio relatif tetap sama - Mengikat proses ke core CPU tertentu dengan
core_affinitymenghasilkan hasil yang sama
- Mengaktifkan kembali
- Pengukuran latensi syscall
readberbasis eBPF juga menunjukkan sisi Rust lebih lambat- Python
read file: 8,134,049ns - Rust
std::fsread file: 24,636,975ns
- Python
- 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/freadjuga lebih lambat daripada Pythonc-fs-read: rata-rata 23.8mspython-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:
readpada offset0x10dari alamat yang dikembalikanmmap - Python:
readpada offset0x30dari alamat yang dikembalikanmmap
- C:
- 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-readdan versi dengan offset padaAMD Ryzen 9 5900HX, lalu menganalisisnya denganperf - Nilai
L1-dcache-prefetchesdanL1-dcache-loadsberbeda besar tergantung ada tidaknya offset- Tanpa offset:
L1-dcache-loadssekitar 127,845,213,L1-dcache-prefetchessekitar 1,843,493 - Dengan offset:
L1-dcache-loadssekitar 13,965,813,L1-dcache-prefetchessekitar 395,578
- Tanpa offset:
- Hotspot berada di jalur
readkernel, darishmem_file_read_iter→copy_page_to_iter→_copy_to_iter - Assembly inti di dalam
_copy_to_iteradalahrep 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 movsbburuk 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 movsbmenunjukkan 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 movsbdanrep movsd - FSRM adalah fitur yang dimulai dari Intel dan juga diperkenalkan di AMD; pada CPU yang menyatakan dukungan,
glibcsecara 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, perbaikanamd-ucodepada 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
- Xuanwo/when-i-find-rust-is-slow: kumpulan potongan kode dan skrip yang digunakan
- Std::fs::read slow?: laporan serupa dari komunitas Rust
- Terrible memcpy performance on Zen 3 when using rep movsb: masalah performa Zen 3
rep movsbyang dilaporkan ke Ubuntu glibc - binding/python: rust std fs is slower than python fs: issue terkait binding Python OpenDAL
1 komentar
Komentar Hacker News
Ada sampai dua flag fitur CPU khusus yang menunjukkan bahwa
REP STOS/MOVitu cepat dan bisa dipakai sebagai urutan instruksi pendek untukmemset/memcpySelama 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
Mungkin ada masalah pada
rep movscepat yang page-aligned, atau rentan terhadap suatu serangan sehingga dinonaktifkanSaya juga tidak tahu seperti apa perbaikannya seharusnya, apakah perlu semacam pemeriksaan runtime atau tidak
Kalau ada implementasi “software” yang lebih cepat, saya penasaran kenapa
REP MOVSsetidaknya tidak dibuat melakukan hal yang sama di microcodeBug 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 misteriTulisannya bagus dan sangat menarik
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
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
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
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 dinamisKalau 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
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
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
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_genericJika CPU yang bermasalah terdeteksi dan memicu bug yang membuat penyelarasan memori melambat, cukup gunakan implementasi penyalinan memori lain
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
Mitigasi software juga akan rumit. Kernel sebenarnya tidak bisa memakai instruksi vektor yang biasanya digunakan pada jalur alternatif ketika ERMS tidak dapat digunakan
jemallocadalah allocator default Rust sampai 2018https://internals.rust-lang.org/t/jemalloc-was-just-removed-...
Bagian yang mengatakan “developer Rust bisa mempertimbangkan beralih ke
jemallocatoruntuk meningkatkan performa” membuat penasaranApakah 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
jemallocdapat menimbulkan masalah observability karenaMADV_FREE.htoptidak lagi menampilkan memori yang benar-benar sedang digunakan secara akurathttps://github.com/jemalloc/jemalloc/issues/387#issuecomment...
https://gitlab.haskell.org/ghc/ghc/-/issues/17411
Sekarang tampaknya
jemallocmemanggilMADV_DONTNEED10 detik setelahMADV_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
htopNamun menurut https://jemalloc.net/jemalloc.3.html, jeda bisa dihilangkan dengan menyetel
opt.muzzy_decay_ms = 0Meski begitu, pembuat musl masih berhati-hati untuk menjadikan
jemallocsebagai default: https://www.openwall.com/lists/musl/2018/04/23/2Intinya, 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
Belum tentu lebih cepat di semua situasi, tetapi hampir di sebagian besar kasus akan lebih cepat. Rust dulu juga memakai
jemallocsebagai default, tetapi diubah karena ada orang-orang yang menganggapnya mengejutkan untuk sebuah defaultIni 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
jemallocsebagai default, tetapi sekitar 2018 kembali kemallocsistem[0]Sekarang Rust memiliki trait
GlobalAllocdan atribut#[global_allocator], sehingga jika aplikasi menginginkannya,jemallocbisa dipakai sebagai allocator. Saya tidak yakin apakah pengguna bisa menimpanya dengan cara sepertiLD_PRELOADjemalloctidak 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
jemallocmungkin 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 tertentuSaya sudah mengirimkan ini ke orang-orang yang tepat