1 puan yazan GN⁺ 2024-08-27 | 1 yorum | WhatsApp'ta paylaş
  • write ile Linux borusuna veri yazmak, basit bellek yazmaya göre çok daha yavaş hale geldi; deney ortamında bellek arabelleğine yazma 167GB/s düzeyine karşılık boruya yazma 17GB/s seviyesinde kaldı
  • Darboğaz tek bir veri kopyalama işlemiyle açıklanamıyor; pipe_write içindeki sayfa ayırma, kilitleme ve çekirdek kopyalama rutinlerinin maliyetleri birlikte birikiyor
  • vmsplice, kullanıcı alanı arabelleğini çekirdeğe kopyalamadan boruya bağlayarak __alloc_pages, _raw_spin_lock_irq, copy_user_enhanced_fast_string gibi pahalı yolları atlıyor
  • Fizz Buzz işlem hacmi örneğinde vmsplice kullanan çözüm tek çekirdekte 60.8GiB/s, çok çekirdekte 208.3GiB/s seviyesine kadar ulaştı; ayrı bir deneydeki vmsplice örneği ise 210GB/s kaydetti
  • Düzeltmeler nedeniyle SIMD kullanmama cezası yorumu güvenilir değil; süreçler arası iletişim de yalnızca L1 önbellekle bitmediği için 167GB/s'yi gerçek boru işlem hacmi olarak beklemek zor

Başlangıç noktası: vmsplicein yarattığı büyük fark

  • Bazı programlar, veriyi borular üzerinden daha hızlı taşımak için vmsplice sistem çağrısını kullanıyor
  • Code Golf StackExchange'deki Fizz Buzz işlem hacmi yarışmasında çözümler iki büyük gruba ayrılıyor
    • vmsplice kullanmayan çözümler saniyede birkaç GiB düzeyinde; neil'in çözümü 8.4GiB/s'ye ulaşıyor
    • vmsplice kullanan çözümler tkluck'ın 15.5GiB/s'sinden ais523'ün 60.8GiB/s'sine ve david'in çok çekirdekli 208.3GiB/s'sine kadar çıkıyor
  • Yalnızca çekirdek alanı ile kullanıcı alanı arasındaki kopyalamayı azaltma etkisi, tek çekirdek bazında yaklaşık 7 katlık farkı açıklamakta yetersiz kalıyor
  • Kendi deneylerinde de ais523'ün çözümü 96.4GiB/s, david'in çözümü 7 çekirdek kullanıldığında 277GB/s, çekirdek başına yaklaşık 40GB/s kaydetti

Temel çizgi: kullanıcı alanında bellek yazma

  • Sistem çağrısı olmadan kullanıcı alanı belleğinde 32KiB arabelleği tekrar tekrar kopyalayan Rust programı, deney ortamında 167GB/s kaydetti
  • Bu değerin, kullanılan CPU'nun L1 önbellek yazma hızıyla örtüşen bir seviye olduğu kabul ediliyor
    • Deney sistemi Ryzen 9 7950X3D, DDR5 6000T/s, Debian 12 ve Linux 6.1.0-18-amd64 ortamıydı
    • CPU hafifletme seçenekleri mitigations=off ile devre dışı bırakıldı
  • ftrace profillemesinde zamanın büyük bölümünün __memset_avx512_unaligned_erms içinde harcandığı görüldü
  • Ancak düzeltmeler bu yorumu sınırlıyor
    • Kesme noktasındaki komut rep stos idi ve bu AVX-512 komutu değil
    • Yalnızca AVX2 ve SSE2 kullanılacak şekilde sınırlandığında da işlem hacminin 167GB/s'de kalmasının nedeni, tüm durumlarda rep stos kullanılmasıydı

write ile boruya yazmanın gerçek maliyeti

  • Aynı boyuttaki arabelleği stdout.write() ile boruya yazıp pv >/dev/null ile tüketince işlem hacmi 17GB/s'ye düşüyor
  • Profilleme sonuçlarına göre zamanın büyük bölümü write sistem çağrısı içinde harcandı; özellikle %95'i pipe_write içinde kaldı
  • pipe_write içinde sayfa hazırlama, kilitleme ve kopyalama birlikte maliyet oluşturuyor
    • __alloc_pages: toplam zamanın %36'sı; boru için yeni bellek sayfaları hazırlar
    • __mutex_lock.constprop.0: toplam zamanın %25'i; boruya yazma için kilitleme maliyetini oluşturur
    • _raw_spin_lock_irq: toplam zamanın %5'i; boruya yazmayla ilgili kilitleme maliyeti olarak görünür
    • copy_user_enhanced_fast_string: toplam zamanın yaklaşık %20'si; veriyi kullanıcı alanından çekirdek tarafına kopyalar
  • pv, splice ile sayfaları /dev/null'a taşıyarak tükettiği için aynı birkaç sayfayı döngüde sürekli yeniden kullanmak zorlaşıyor

Çekirdek kopyalama rutini ve düzeltilmiş yorum

  • copy_user_enhanced_fast_string tersine derlendiğinde, büyük arabellek kopyalarının REP MOV komutuyla işlendiği görülüyor
  • Bu işlev C ile değil Linux çekirdeğinin assembly koduyla uygulanmış; yani derleyici optimizasyonu eksikliği değil, bilinçli seçilmiş bir yol
  • İlk deneyde kullanıcı alanında rep movsb doğrudan çağrılarak 80GB/s elde edilmiş ve bu, çekirdek kopyalama rutininin yaklaşık 2 kat yavaş olduğu yorumuyla ilişkilendirilmişti
  • Daha sonra yapılan düzeltmeyle deney koşulları değişti
    • İki adet 32KiB arabellek L1 veri önbelleğini dolduruyor
    • 16KiB arabellek kullanıldığında performans 153GB/s'ye çıkıyor
  • Bu yüzden çekirdek kopyalamada vektör komutlarının kullanılmamasının büyük bir ceza olduğu yönündeki ilk yorum güvenilir değil
  • Buna rağmen boruya yazmada bellek yönetimi ek yükünün büyük olduğu gerçeği geçerliliğini koruyor

vmsplicein kaçındığı çekirdek yolu

  • vmsplice, kullanıcı alanındaki tüm arabelleği çekirdeğe kopyalamadan boruya aktararak boru kullanım maliyetini düşürüyor
  • Francesco'nun pipes-speed-test içinde yer alan ./write örneği, sonsuz şekilde 'X' yazan minimal örnek olarak kullanılıyor
  • Bu örnek 210GB/s kaydetti; ancak aynı arabelleği tekrar tekrar vmsplicee verdiği için tipik bir veri üretme işinden farklı
    • Sabit bayt akışı değilse yeni verinin arabelleğe doldurulması gerekir
    • Bu noktada basit bellek yazmanın üst sınırı olan 167GB/s yeniden ilgili hale gelir
  • vmsplice yolunda da __mutex_lock.constprop.0 için zamanın %37'si harcanıyor
  • Ancak write yolunda görülen __alloc_pages, _raw_spin_lock_irq, copy_user_enhanced_fast_string görünmüyor
  • Bunun yerine add_to_pipe, import_iovec, iov_iter_get_pages2 ana yollar olarak ortaya çıkıyor ve vmsplicein writeın pahalı kısımlarını atlattığını gösteriyor

Geriye kalan sonuçlar ve dikkat edilmesi gerekenler

  • Deneylere göre Linux borusuna write ile yazma yolu, basit bellek yazmaya göre yaklaşık 10 kat daha yavaş
  • İlk sonuç, boruya yazmada kilitleme maliyetinin ve SIMD bağlamını kaydetme/geri yükleme maliyetinin büyük olduğu; splice ve vmsplicein bunları atlattığı yönündeydi
  • Düzeltmeden sonra sonuç daha sınırlı okunmalı
    • Çekirdeğin bellek yönetimi ek yükü, boru performans düşüşünde hâlâ önemli bir unsur
    • Vektör komutlarının kullanılmamasının beklenildiği kadar büyük bir ceza olduğu yorumu doğru değil
    • Süreçler arası iletişim yalnızca L1 önbellek üzerinden gerçekleşemeyeceği için 167GB/s'nin gerçek boru okumasını da içeren işlem hacmi olarak yakalanması zor
  • Bazı önemli hatalar düzeltildi ve sonuçların güvenilirliği sınırlı olabilir; bu nedenle sayılar yön gösterici olarak yorumlanmalı
  • Boru işlem hacmini artırmak için yalnızca sistem çağrısı sayısını azaltmak değil, writeın geçtiği çekirdek içi yolları ve arabellek işleme biçimini de birlikte değerlendirmek gerekiyor

1 yorum

 
GN⁺ 2024-08-27
Hacker News yorumları
  • Bu sorunu ele almaya çalışan bir yan proje var: https://lwn.net/Articles/976836/
    Fikir, desteklenen tüm dosya tanımlayıcıları için bir halka tampon elde etmeyi sağlayan bir sistem çağrısı oluşturmak. Borular da buna dahil; iki uç da halka tampon kullanımını destekliyorsa aynı halka tamponu eşleyip sıfır kopyalı G/Ç yapabilir, bazı durumlarda çekirdek çağrısı bile hiç yapılmayabilir. Birlikte çalışacak kişiler aranıyor

    • En azından kullanıcı alanı kullanımları için yeni bir çekirdek özelliğinin gerçekten gerekli olup olmadığından emin değilim. Daha önce eventfd ile boru davranışını oldukça yakından taklit eden, kullanıcı alanında tek üretici/tek tüketici halka tampon uygulamıştım
      Halka tampon dolu ya da boş olduğunda sleep/poll yapılabiliyor; diğer durumlarda ise kilitsiz ve sistem çağrısı ek yükü olmadan çalışıyor
    • Borunun iki ucunun halka tamponu desteklediğini karşı tarafa bildiren standart bir sinyal verme yöntemi planlanıp planlanmadığını merak ediyorum. Böylece libc bunu şeffaf biçimde ele alabilir; aksi halde, borular özelinde bunun paylaşımlı bellek ve futex senkronizasyonuna göre ne avantajı olduğunu pek göremiyorum
    • Muhtemelen ringbuffer_wait() de poll() içinde okunabilir durum haline getirilerek sinyal verebilir
    • Mevcut halka tampon arayüzlerinin bunu kullanıp kullanmayacağını, yoksa bir xkcd927 durumu mu yaşanacağını merak ediyorum. Her hâlükârda ilginç bir deneme gibi görünüyor
    • Tamponlama bir sebepten var; bu yaklaşım betiklerde tuhaf hata kipleri ve zafiyetler yaratabilir. İşin özü şu: Herhangi bir akış üreticisi, belirli bir tüketiciden daha yavaş kalabilir
      Anlık bir hiccup bile yeterli tamponlama yoksa boruyu tamamen arapsaçına çevirebilir; gereken tampon boyutu da sistemden sisteme değişir
  • JMP’in neden sadece RET olmadığı CONFIG_RETHUNK seçeneği yüzünden. objdump tersine derlemesinde görünen şey, RET’in JMP __x86_return_thunk ile değiştirilmiş sonucu
    https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
    https://github.com/torvalds/linux/blob/v6.1/arch/x86/lib/ret...
    Fonksiyonun başındaki ve sonundaki NOP komutları ftrace için değil, ASM_CLAC/ASM_STAC makrolarından geliyor. Bu makrolar, X86_FEATURE_SMAP algılandığında çalışma zamanında CLAC/STAC komutlarıyla doldurulacak alan bırakıyor. İki komut da 3 bayt olduğundan NOP sayısıyla aynı
    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/...

    • Bunları bilip yine de böyle muzır, tek kullanımlık bir takma ad seçecek çekirdek geliştirici sayısı herhalde bir elin parmaklarını geçmez
  • Linux borularına “yavaş” demek, Toyota Corolla’ya “yavaş” demek gibi. Uç kullanım durumları dışında yeterince hızlı
    Otomobil yarışı mı yapıyorsun? Üstelik hızın beceriden daha önemli olduğu bir dal mı? O zaman daha hızlı bir araba al. Değilse Corolla’yı sürmeye devam et

    • Bu, ömrü boyunca yalnızca birkaç milyar kez çalışacak herhangi bir proje kodu değil; milyonlarca ila milyarlarca bilgisayarda sık kullanılan kod
      Bu yüzden çok küçük verimlilik artışları için bile optimizasyona ciddi zaman ayırmak ekonomik olarak mantıklı
    • Aslında yazının yazarının durumunda yavaş denen boru, veriyi 17GB/s, yani 130Gbps’nin üzerinde bir hızla taşıyor
      10 yılı aşkın süredir boruları çeşitli amaçlarla kullanıyorum ama boru hızına takıldığım hiç olmadı; çoğu zaman darboğaz büyük olasılıkla tar, gzip, find, grep, nc gibi araçlardı. Elbette bu araçlar da yaptıkları işe kıyasla oldukça hızlı sayılır
    • Ham videoyu çözen kapalı kaynaklı bir SDK kullanan bir projem var. Çözülen veriyi saf RGBA olarak çıkarıyor ve FFMpeg’in bunu borudan okuyup standart bir kodekle yeniden kodlamasını sağlıyor
      FFMpeg, Non-Free SDK’yı kaynak koduna dahil edemiyor; saf RGBA’yı dosyaya yazmak ise akıl almaz derecede pratik dışı. Bu yüzden boru tek yol ve yüksek iş hacimli borulara ihtiyaç duymak için meşru bir neden var
    • Her yerde kullanılan bir şeyi birkaç yüzde puan hızlandırmak kesinlikle değerli bir yatırım. Tek tek işler büyük ölçüde hızlanmasa bile, dünya genelinde toplandığında muazzam miktarda enerji ve zaman tasarrufu sağlar
    • Bazen daha hızlı bir Corolla gerçekten de en iyi cevap olabilir
      https://www.toyota.com/grcorolla/
      Bu araçların mühendisliği ve performansı çok etkileyici; ayrıca başta hedeflenen GR Yaris’i ABD pazarına getirmeyi zorlaştıran kuralları aşmaya yönelik bir hack gibi de duruyorlar. HN kitlesinin hafife almayacağı kadar mühendislik/performans/hack/pazar bağlamı olduğunu düşünüyorum. Üstelik şirketin başkanı da hâlâ bizzat sürüp kullanıyor
  • Yazının ana noktasından bağımsız olarak, modern CPU’larda rep movsb en hızlı vektörleştirilmiş sürüm kadar hızlıdır. Çünkü CPU’nun bunu hızlandırdığını biliyoruz.
    Çekirdek işlevi adı copy_user_enhanced_fast_string da bunu ima ediyor. İlgili CPU özellikleri ERMS (Enhanced Repeat Move String, belirli bir uzunluğun üzerinde rep movsb’yi hızlandırır) ve FSRM’dir (Fast Short Repeat Move String, kısa kopyalamaları da hızlandırır).

    • Hepsi bu değil. rep movsb belirli bir eşik değere kadar hızlıdır, ama sonrasında normal store veya non-temporal store daha hızlıdır.
      Tüm eşik değerleri https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch... adresinde açıklanıyor.
      Üstelik bu değerler sabit de değil; Noah Goldstein hâlâ her yıl güncelliyor.
    • Linux’un, yazıda kullanılan 6.1 çekirdeğinden sonra x86 kopyalamada ERMS ve FSRM kullanımını birkaç kez değiştirdiğini de görmekte fayda var. Bu arada, hem FSRM hem de ERMS bulunan makinemde — şaşırtıcı biçimde ilki ikincisini ima etmiyor — Linux 6.8’de sıradan bir pipe ve 32KiB buffer ile 17GB/s görüyorum.
    • Kısa memcpy için, basit C döngüsü sürümünü silebileceğimiz kadar rep movsb ve rep stosb hızlansın diye hâlâ bekliyorum.
    • Öyleyse C derleyicilerinin sabit uzunluklu memcpy’yi inline etmesi gibi, değişken uzunluklu memcpy()’yi de ne zaman inline edeceğini merak ediyorum.
  • AVX512 hakkında yazıda görmediğim bir nokta var: xsave/xrstor ek yükünün yanı sıra AVX512 çok güç tüketir ve CPU frekans ölçeklemesini tetikler. Ayrıntılar ve bunun ne kadar incelikli hâle gelebileceği için [1] ve [2]’ye bakılabilir.
    [1] https://www.intel.com/content/dam/www/central-libraries/us/e...
    [2] https://www.intel.com/content/www/us/en/developer/articles/t...

    • Bu yalnızca belirli Intel CPU modelleri için geçerli.
  • Süreçler arası iletişimin neredeyse her biçimi “yavaş”tır. Güvenlik için performans maliyeti ödemeyi seçmiş oluyoruz.

    • Bu kadar büyük bir bedel ödemek gerekmiyor. Pipe’ın sunduğu şey çok az; bu yüzden maliyeti de çok az olmalı.
      Özellikle en hızlı süreçler arası iletişimin uzun bir fonksiyon çağrısından daha yavaş olması için pek neden yok.
    • Pipe’lar güvenlik için değil, mevcut programlar arasında veri aktarmaya yönelik bir optimizasyon olarak vardır.
  • Yine Hacker News’in hug of death etkisine maruz kalıyorum. WordPress sayfa önbelleklemesi sayesinde geçen seferden daha iyi, ama yine de sayfanın yüklenmesi birkaç saniye sürebilir; anlayışınız için teşekkürler.

  • splice’ın aslında neden bu kadar yavaş olması gerektiğini pek anlayamamıştım. vmsplice’dan daha yavaş olmasının nedeni olarak buffer ayırma ve skaler komut kullanımı gösterilmiş, ama bunun neden gerekli olduğunu bilmiyorum.
    Neden splice basitçe vmsplice gibi yeniden uygulanamıyor? Elbette iyi bir nedeni vardır, ama sanırım kaçırıyorum.

  • io_uring kullanan bir sürümü görmek de ilginç olurdu. Çekirdek ve buffer’ı önceden paylaşarak bazı kopyalamalardan kaçınmak, ayrıca sistem çağrısı ek yükünden de kaçınmak mümkün olabilir. Ancak burada ikincisi neredeyse ihmal edilebilir görünüyor.

  • Yüklenmesi yaklaşık 20 saniye süren bir blog için oldukça cesur bir iddia.

    • Bu yazı Hacker News’in en üstüne kadar çıktığına göre biraz hoşgörülü olmak gerekebilir.
      Yazının kendisi harika görünüyor ve içeride neler olup bittiğine dair öğrenilecek çok şey var.