- Çıkış noktası, Apache OpenDAL’in Python bağlamalarında dosya okumanın Python’un yerleşik
open().read()çağrısından daha yavaş olduğuna dair bir bildirimdi; ancak darboğaz OpenDAL’in ya da PyO3’nün kendisi değildi - 64MiB dosya okuma benchmark’ında
python-fs-readyaklaşık 15~19ms, Ruststd::fsve C uygulaması ise yaklaşık 23ms olarak ölçüldü; bu da Rust/C’nin Python’dan yavaş göründüğü anlamına geliyordu strace,eBPF,perfizlenince farkın,readsistem çağrısının hedef tamponunun sayfa içinde bulunduğu ofset ile bağlantılı olduğu görüldü;0x10civarında performans düşüşü yeniden üretilebildi- AMD Ryzen 9 5900X, Ryzen 7 5700X, Ryzen 9 5900HX serilerinde benzer olgu doğrulandı; çekirdek içindeki
_copy_to_iteriçinderep movsbyürütme performansı temel ipucuydu - Python’un doğası gereği daha hızlı olması değil, AMD Zen 3’ün FSRM/
rep movsbile ilgili CPU hatası ve bellek ofsetinin tesadüfen bu sonucu yaratmasıydı;jemallociyileşmesi de ayırıcının kendisinden değil, farklı ofsetten kaynaklanıyordu
OpenDAL Python bağlamalarında başlayan tuhaf benchmark
- Apache OpenDAL, farklı depolama servislerinde verileri birleşik bir şekilde okumak ve yazmak için kullanılan bir veri erişim katmanıdır; Python bağlamaları PyO3 üzerinden sağlanır
- Kullanıcı, OpenDAL Python bağlamasıyla 150MB dosya okuyan kodun Python’un yerleşik dosya okumasından yavaş olduğunu bildirdi
- Python yerleşik
open(...).read()100 kez:4.470868484000675 - OpenDAL Python bağlaması 100 kez:
8.993250704006641
- Python yerleşik
- Basitleştirilmiş 64MiB dosya okumasında da OpenDAL bağlaması daha yavaştı
python-fs-read: ortalama 15.9mspython-opendal-read: ortalama 32.9ms- Python yerleşik okuma, OpenDAL bağlamasından 2.07 kat daha hızlı ölçüldü
İz sürme Rust OpenDAL ve std::fs’e kadar indi
- Aynı mantık Rust’ın OpenDAL
fsservisiyle uygulandığında da Python yerleşik okumadan yavaştırust-opendal-fs-read: ortalama 23.8mspython-fs-read: ortalama 15.6ms- Python yerleşik okuma, Rust OpenDAL uygulamasından 1.52 kat daha hızlı ölçüldü
- OpenDAL’in
fsservisi Rust std::fs kullandığı için, OpenDAL’in kendi maliyetini kontrol etmek üzerestd::fstabanlı ayrı bir uygulama yazıldı - Rust
std::fsdoğrudan uygulamasında da aynı akış sürdürust-std-fs-read: ortalama 23.1mspython-fs-read: ortalama 15.2ms- Python yerleşik okuma, Rust
std::fs’ten 1.52 kat daha hızlı ölçüldü
strace ile görülen sistem çağrıları ve mmap
straceanalizinde hem Rust hem Python’un büyük tampon ayırmaları içinmmapkullandığı görüldü- Rust
std::fsçalıştırması/tmp/filedosyasını açıyor, 64MiB’ı bir kez okuyor, EOF kontrolü içinreadçağırıyor ve ardından kapatıyordu - Python yerleşik okuma
newfstatat,ioctl,lseekgibi daha fazla sistem çağrısı yürütse de toplam süre daha kısaydı mmap(NULL, 67112960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)çağrısı dosya eşleme için değil, anonim bellek ayırma için kullanılıyordu67112960, 64MiB’a 4KiB eklenmiş boyutMAP_ANONYMOUS, dosyayla ilişkisi olmayan bellek ayırma anlamına gelir
- Rust’ın
x86_64-unknown-linux-gnuvarsayılan derlemesiglibc’ninmalloc’unu kullanır;glibcbüyük ayırmalardammapkullanabilir
jemalloc ile hızlanan Rust ve tersine dönen ara sonuç
- Rust genel ayırıcısı
jemallocator::Jemallocolarak değiştirildiğinde Python’dan hızlı hale geldirust-std-fs-read-with-jemalloc: ortalama 9.7mspython-fs-read: ortalama 15.8ms- jemalloc kullanan Rust uygulaması Python’dan 1.64 kat daha hızlı ölçüldü
- Bu aşamada neden
mmapya da varsayılan bellek ayırıcısı gibi görünüyordu; ancak sonraki güncellemede yorum düzeltildi - 2023-12-01 güncellemesine göre fark,
jemalloc,pymalloc,mimalloc’unglibc malloc’tan doğası gereği daha hızlı olmasından kaynaklanmıyordu - Gerçek fark, ayırıcının oluşturduğu tamponun sayfa içi ofsetinden geliyordu
rust-std-fs-read:mmapbaşlangıç adresinden0x10ofsetinde okumarust-std-fs-read-with-jemalloc:mmapbaşlangıç adresinden0x740ofsetinde okuma
- Sorunlu aralık sayfa içindeki
0x00..0x10aralığı olarak toparlandı; aynı sorunjemallocile de yeniden üretilebiliyor
Yazılım ayarlarından çok cihaza bağlı yeniden üretilebilirlik baskındı
- Tartışma ilerledikçe Rust’ın Python’dan yavaş olduğu olgunun özellikle yazarın makinesinde belirgin olduğu doğrulandı
- Yazarın CPU’su AMD Ryzen 9 5950X 16-Core Processor idi; bellek yapılandırması DDR4 3200 MT/s 16GB DIMM’di
- Çeşitli ayarlar değiştirilse de göreli performans farkı ortadan kalkmadı
- Linux çekirdeği
mitigations=offyeniden açıldığında sonuç değişmedi - Transparent Hugepage
always,madvise,neverolarak değiştirildiğinde mutlak değerler değişti, ancak göreli oran korundu core_affinityile belirli bir CPU çekirdeğine sabitlendiğinde de sonuç aynıydı
- Linux çekirdeği
- eBPF tabanlı
readsistem çağrısı gecikmesi ölçümünde de Rust tarafı daha yavaştı- Python
read file: 8,134,049ns - Rust
std::fsread file: 24,636,975ns
- Python
- Gözlemlere göre farkı yalnızca OpenDAL, PyO3 veya Rust standart kütüphanesiyle açıklamak zordu; süre farkı zaten sistem çağrısı seviyesinde açılmıştı
C uygulamasında ortaya çıkan bellek ofseti ipucu
- Aynı 64MiB dosya okuması C
fopen/malloc/freadile uygulandığında da Python’dan yavaştıc-fs-read: ortalama 23.8mspython-fs-read: ortalama 19.1ms- Python yerleşik okuma, C uygulamasından 1.25 kat daha hızlı ölçüldü
strace -e raw=read,mmapile işaretçi adresleri kontrol edilince C ve Python’un tampon başlangıç ofsetlerinin farklı olduğu görüldü- C:
mmapdönüş adresinden0x10ofsetinderead - Python:
mmapdönüş adresinden0x30ofsetinderead
- C:
- C uygulamasında ofset aynı şekilde ayarlandığında performans belirgin ölçüde iyileşti
c-fs-read-with-offset: ortalama 8.9ms- Python’dan 2.15 kat, mevcut C uygulamasından 2.68 kat hızlı
- Bu sorun AMD Ryzen 9 5900X ve AMD Ryzen 7 5700X üzerinde de yeniden üretildi
- Rust topluluğundaki Std::fs::read slow? başlığında da benzer bir olgu bildirildi ve bellek bölgesi ofseti ile sistem çağrısı performansı arasındaki ilişkiye işaret edildi
perf analizinin işaret ettiği rep movsb
- Bir çekirdek geliştiricisi
AMD Ryzen 9 5900HXüzerindec-fs-readve ofset uygulanmış sürümü yeniden üretipperfile analiz etti - Ofsetin olup olmamasına göre
L1-dcache-prefetchesveL1-dcache-loadsdeğerleri büyük ölçüde değişiyordu- Ofset yok:
L1-dcache-loadsyaklaşık 127,845,213,L1-dcache-prefetchesyaklaşık 1,843,493 - Ofset var:
L1-dcache-loadsyaklaşık 13,965,813,L1-dcache-prefetchesyaklaşık 395,578
- Ofset yok:
- Sıcak nokta, çekirdeğin
readyolundashmem_file_read_iter→copy_page_to_iter→_copy_to_iterzincirinde yer alıyordu _copy_to_iteriçindeki temel assemblyrep movsbidi ve örneklerin çoğu bu komutta yoğunlaşıyordu- Sonraki analizlerde, L1 prefetch’in kendisinden ziyade, sayfaya hizalı verilerde
rep movsbperformansının kötü olması ve sayfa hizası bozulduğunda iyileşmesi daha önemli bir ipucu olarak değerlendirildi
FSRM ve AMD Zen 3 sorunu
- Paylaşılan Ubuntu glibc hata raporu Terrible memcpy performance on Zen 3 when using rep movsb da
rep movsbperformans sorununu ele alıyor - Rapordaki örnek, 2113 bayt kopyalamada
rep movsbyolunun yaklaşık 3.2GB/s gösterdiğini, boyut 2111 bayta değiştirildiğinde ise 100GB/s üzerine çıktığını açıklıyor - FSRM, Fast Short REP MOV’un kısaltmasıdır;
rep movsbverep movsdkomutlarını hızlandırmaya yönelik bir özelliktir - FSRM Intel’de başlayan bir özellik olup AMD’ye de getirildi; desteğini ilan eden CPU’larda
glibcvarsayılan olarak FSRM kullanır - Dolayısıyla Python’un C/Rust’tan doğası gereği daha hızlı olması değil, AMD CPU hatası nedeniyle C/Rust’ın okuma yolunun belirli bellek ofsetlerinde yavaşlaması söz konusudur
Güncelleme: AMD’nin haberdar olup olmadığı ve glibc yanıtı
- 2023-12-01 güncellemesine göre AMD’nin bu hatadan 2021’den beri haberdar olduğu anlaşılıyor
- Yazı yayımlandıktan sonra çok sayıda okur bağlantıyı AMD’ye ilettiği için AMD’nin sorundan haberdar olduğu düşünülüyor
- Yazar, AMD’nin bu hatayı
amd-ucodeiçinde sorumluluk alıp düzeltmesi gerektiğini düşünüyor; ancak doğrulanmamış bilgilere göre Zen 3’teamd-ucodedüzeltmesi zor olabilir - Gerçekçi umut, glibc’nin gerektiğinde FSRM’i devre dışı bırakması yönünde
- glibc tarafında x86: Improve ERMS usage on Zen3 çalışması devam ediyor
Yeniden üretim kodu ve ilgili kaynaklar
- Xuanwo/when-i-find-rust-is-slow: Kullanılan kod parçaları ve betikler derlemesi
- Std::fs::read slow?: Rust topluluğundan benzer rapor
- Terrible memcpy performance on Zen 3 when using rep movsb: Ubuntu glibc’ye bildirilen Zen 3
rep movsbperformans sorunu - binding/python: rust std fs is slower than python fs: OpenDAL Python bağlamasıyla ilgili issue
1 yorum
Hacker News yorumları
REP STOS/MOV’un hızlı olduğunu vememset/memcpyiçin kısa komut dizileri olarak kullanılabileceğini gösteren iki ayrı özel CPU özellik bayrağı bile var.On yıllardır her yeni CPU neslinde optimizasyon rutinlerini elle yeniden yazma eziyeti sürüyor; hâlâ böyle bir durumda olmamız, bunun CPU tedarikçilerinin zamanlama test paketlerinde yer alması gerekmez miydi diye düşündürüyor.
Sayfa hizalı hızlı
rep movsile ilgili bir sorun olmuş ya da bir saldırıya açık olduğu için devre dışı bırakılmış olabilir.Düzeltmenin nasıl olması gerektiğini, çalışma zamanı denetimi gibi bir şey gerekip gerekmediğini bilmiyorum.
Daha hızlı bir “yazılım” uygulaması varsa
REP MOVS’un en azından mikrokod içinde aynı işi yapar hâle getirilmemesinin nedenini merak ediyorum.İlgili glibc hatası burada. Ancak bu taraf Zen 4: https://sourceware.org/bugzilla/show_bug.cgi?id=30994
İlk başta yazıyı okuyunca yazarın
std::fs’i yanlış kullandığını söyleyip dalga geçmeye hazırlanmıştım; ama aslında hata ayıklama tavşan deliği ve gizemle ilerleyen keyifli bir yazı çıktı.İyi yazılmış ve çok ilginçti.
Çıkış noktası biraz kafa karıştırıcı. Saf Python koduyla yerel C/Rust kodu karşılaştırılmıyor; yerel kodun üzerindeki bir Python sarmalayıcısı olan Python dosya okuma yöntemiyle, başka bir yerel kod sarmalayıcısı olan OpenDAL karşılaştırılıyor.
Performans farkı olması hâlâ ilginç, ama bunu “Python’dan daha yavaş” diye ifade etmek epey tuhaf. Python standart kütüphanesinin tamamının saf Python ile yazıldığını mı bekliyorlar diye düşündürüyor. Aksine, Python standart kütüphanesindeki fonksiyon uygulamalarının yerel olduğunu ve tek tek oldukça optimize edilmiş olacağını beklerim.
Sonucun yerel kodun çalışma biçimiyle ilgili olması şaşırtıcı değildi; ama somut yanıt beklenmedikti. Yalnızca başlangıç kısmı kafa karıştırıcıydı; yazının kendisi ise çok ilginçti.
Ayrıca “C is slower than Python with specified offset” başlığı da ana dili İngilizce olan biri için “ofset belirtilmiş olsa bile C, Python’dan daha yavaş” gibi okunuyor. Oysa gerçekte bunun tersi kastedilmişti: Python’da kullanılan ofset C’de de belirtilince C daha hızlı olmuştu.
Dosya okumak gibi basit bir işin Rust standart kütüphanesinde Python standart kütüphanesinden daha yavaş olması şaşırtıcı. Bu Python standart kütüphanesi çağrısının C ile yazıldığını bilseniz bile, Rust standart kütüphanesi çağrısının da benzer hızda olmasını beklersiniz.
Bu yüzden normalde kullanımın yanlış olduğunu ya da Rust standart kütüphanesinde garip bir davranış olduğunu beklersiniz; ama bu sefer ikisi de değildi, belirli donanımlarda ayırma hizalamasına bağlı oluşan bir performans uçurumuydu.
Dosya sistemi okumanın Python’da iyi optimize edilmiş olmasını bekleriz; ama Rust’ta da aynısını düşünürüz. Bu yüzden Rust tarafının çok daha yavaş olması şaşırtıcıydı; özellikle de donanıma ve ayırıcıya bağlı olması daha da şaşırtıcıydı.
Python ile yazdığım kod hızlıysa benim için Python hızlıdır. Uygulamanın başka bir dilde yazılmış olması mı yoksa başka bir neden mi olduğu pek önemli değil.
Orijinal yazıda olan şey neredeyse tamamen tesadüf. CPython’ın C kodu
consttutarlılığını bile pek umursamaz; dinamik bellek ayırma ve yardımcı/kolaylık çağrıları çoktur. Aritmetik gibi şeyler bile dinamik bellek ayırır.CPython ile çalışma deneyiminiz varsa genelde performansının iyi olmasını beklemezsiniz. Performansı iyileştirmek istediğinizde, onun sunduğu özellikleri baypas etmeye çalışırsınız.
Ayrıca Python’un bir standardı yoktur; bu yüzden teknik olarak standart kütüphanesi de yoktur ve birlikte dağıtılan kütüphanelerin çoğu Python ile yazılmıştır. Bazıları C ile yazılmıştır ama o C kodlarının içinde bile aslında Python kodunu mekanik olarak C’ye taşımış olanların oranı epey yüksektir. Örneğin Python’un ikili arama uygulaması önce Python ile yazılmış, daha sonra Python C API kullanılarak C’ye çevrilmiştir.
Beklenebilecek şey, işletim sistemi işlevlerine basitçe eşlenen özelliklerin nispeten ince bir sarmalayıcıya sahip olmasıdır. Yani dosya okuma özünde doğrudan sistem arayüzüne girdiği için çok fazla bağlama kodu gerektirmemelidir.
Benzer yazılar onlarca kez yayımlandıktan sonra herkes bunu fark etti.
Yazının kendisi harika ve bu meseleyle ilgili çok sayıda ilginç bilgi içeriyor.
Ancak daha çok ilgimi çeken ve endişelendiren kısım, sorunun nasıl raporlanıp kayda geçirildiği ve iletişimin nasıl yürütüldüğü.
Raporlama Discord üzerinden yapılıyor; bu kapalı bir ortam, indekslenmiyor, araması zor ve kalıcı olarak korunmuyor. Tartışmalar Discord ve Telegram’da yapılıyor; bu bağlamda Telegram daha da kötü olabilir.
Bu blog yazısı ve GitHub deposu, geriye kalan tek iz. Xuanwo blogda yazmamış olsaydı zaman akışının içinde kaybolup gidecekti. Oldukça ilginç bir durum.
Varsayılan olarak herkese açık erişilebilen kayıtları indeksleyip aranabilir hâle getiren mesajlaşma uygulaması neredeyse yok. Tüm IRC sunucuları herkese açık log sağlamıyor; Matrix grupları da öyle. Oradaki tartışmaların neden zaman akışında kaybolmadığını düşündüğünüzü bilmiyorum.
Herkese açık log sunulabilmesinin nedeni kapalı olmaması değil, loglamaya izin veren bir API olması. Telegram’da da böyle bir API var ve bizim tartışma grubumuzun aranabilir kayıtları burada görülebiliyor: https://luoxu-web.vercel.app/#g=1264662201
Herkese açık indekslemenin olmaması çoğunlukla gizlilik yüzünden; platformun kapalı olmasından değil.
Eskiden tüm gönderiler DejaNews’te, daha sonra Google’da tertemiz aranabiliyordu.
İnternet/WWW yığını ve temel programlama araçları ile kütüphaneleri kadar önemli açık kaynak projelerinin kritik iletişimi açık standartlar üzerinden yürümeli.
Bu hafta okuduğum yazılar arasında en ilginç olanıydı. Harika bir derleme.
Yapılacak bariz şey,
copy_user_generickernel metoduna bir yama göndermek gibi görünüyor.Sorunlu CPU algılandığında ve bellek hizalamasının yavaşlamasına yol açan bug tetiklendiğinde farklı bir bellek kopyalama uygulaması kullandırmak yeterli olur.
Kernel deneyimi olmayan birinin kabul ettirebileceği bir düzeltme önemsiz olmayacaktır. Daha da önemlisi, geçici çözümün hangi şekilde etkinleştirileceği de açık değil. Muhtemelen en iyisi açılışta ölçüm yapmak olur; aksi hâlde hangi model ve stepping’lerin etkilendiğini nasıl bileceğimiz belirsiz.
Yazılım tarafındaki hafifletme de karmaşık olacaktır. Çünkü kernel, ERMS kullanılamadığında normalde alternatif yolda kullandığı vektör komutlarını gerçekte kullanamaz.
jemalloc, 2018’e kadar Rust’ın varsayılan ayırıcısıydı.https://internals.rust-lang.org/t/jemalloc-was-just-removed-...
“Rust geliştiricileri performansı artırmak için
jemallocator'a geçmeyi düşünebilir” kısmını merak ediyorumHerkesin neredeyse bedavaya performans artışı elde edip edemeyeceğini, yoksa dikkat edilmesi gereken noktalar olup olmadığını bilmiyorum. C kod tabanlarının da bundan fayda görüp görmeyeceğini, şu anda sadece kaçırdığımız bir performans olup olmadığını merak ediyorum
jemallockullanıncaMADV_FREEnedeniyle gözlemlenebilirlik sorunları çıkabileceğini bilmek gerekir.htopartık gerçekten kullanımda olan belleği doğru şekilde göstermezhttps://github.com/jemalloc/jemalloc/issues/387#issuecomment...
https://gitlab.haskell.org/ghc/ghc/-/issues/17411
Görünüşe göre artık
jemalloc,MADV_FREEsonrasında 10 saniye sonraMADV_DONTNEEDçağırıyor: https://github.com/JuliaLang/julia/issues/51086#issuecomment...Bu yüzden bu sorunu “düzeltmiş” oluyor, ancak belleğin serbest bırakıldığı an ile bunun
htopta gözlemlendiği an arasında kafa karıştırıcı bir gecikme oluşuyorÖte yandan https://jemalloc.net/jemalloc.3.html sayfasına göre
opt.muzzy_decay_ms = 0ayarlanarak gecikme kaldırılabiliyorYine de musl yazarı,
jemalloc'ı varsayılan yapmak konusunda çekimser: https://www.openwall.com/lists/musl/2018/04/23/2Ana fikir; ciddi şişkinlik, ASLR'nin zayıflaması ve bellek kullanımını önemsemeden mümkün olduğunca hızlı olmaya odaklanan optimizasyon sorunları olduğu. Yukarıdaki ayarlarla bir ölçüde hafifletilebilir, ancak performansa mı bellek kullanımına mı odaklanılacağına dair genel eğilim muhtemelen hâlâ bir ödünleşim olarak kalacaktır
Her durumda mutlaka daha hızlı olmayacaktır, ama büyük çoğunlukta daha hızlı olur. Rust da eskiden varsayılan olarak
jemallockullanıyordu, ancak bunu varsayılan olarak şaşırtıcı bulanlar olduğu için değiştirildiİş yüküne büyük ölçüde bağlıdır; bu yüzden profil çıkarma ve kıyaslama gerekir. Yine de C/C++/Rust gibi düşük seviyeli dillerin bu ayırıcıları seçebilmesi gerekir
Dikkat edilmesi gereken bir nokta ikili dosya boyutudur. Özel ayırıcılar yürütülebilir dosyaya ek baytlar ekler
jemallockullanıyordu, ancak 2018 civarında tekrar sistemmalloc'ına döndü[0]Şu anda Rust'ta
GlobalAlloctrait'i ve#[global_allocator]özniteliği var; dolayısıyla uygulama isterse ayırıcı olarakjemallockullanabilir. Kullanıcının bunuLD_PRELOADgibi yöntemlerle geçersiz kılıp kılamayacağından pek emin değilimjemallocher iş yükü ve kullanım senaryosu için her zaman en iyi seçenek değildir. Sistem ayırıcıları çoğu zaman mükemmel olmaktan uzaktır, ama en azından genel amaçlı ayırıcılar olarak yaygın biçimde test edilmiştir[0] https://github.com/rust-lang/rust/issues/36963
jemallocbazı uygulamalar için doğru tercih olabilir, ancak başka durumlarda başka bir ayırıcı daha hızlı olabilir. Ya da daha yavaş olsa bile daha az kirli bellek, daha iyi gözlemlenebilirlik veya belirli güvenlik garantileri gibi hedeflere daha uygun olabilirBunu uygun kişilere gönderdim