- Linux pipe throughput'unu
perfve çekirdek yol analiziyle izleyerek, basitwrite/readtabanlı 3.7GiB/s uygulamasını sonunda 62.5GiB/s seviyesine çıkardı - Darboğazın başlangıç noktası, pipe'ın 4KiB sayfa referanslarını tutan bir ring buffer olarak çalışırken kopyalama, sayfa ayırma, kilit, bekleme-uyandırma maliyetlerini birlikte ödemesi
vmsplicevesplice, kullanıcı belleği ile çekirdek tamponu arasındaki kopyaları azaltarak throughput'u sırasıyla 12.7GiB/s ve 32.8GiB/s seviyelerine yükseltiyor- Sonrasında sanal adres aralığını
struct page'e dönüştürme maliyeti öne çıkıyor; 2MiB huge page kullanıldığında sayfa tablosu yürüyüşü yükü azalıyor ve hız 51.0GiB/s'ye ulaşıyor - Son optimizasyon olan
SPLICE_F_NONBLOCKve busy loop, throughput'u daha da artırıyor; ancak hazır olma durumunu beklerken CPU çekirdeğini sürekli meşgul ediyor
Deneyin amacı ve referans performans
- Amaç, Linux'ta Unix pipe'larının nasıl uygulandığını incelemek ve pipe'a veri yazıp okuyan test programını adım adım optimize etmek
- Deney ortamı Intel Skylake i7-8550U CPU ve Linux 5.17; Linux'un iç uygulaması sürekli değiştiği için sayılar ortama göre farklılık gösterebilir
- Başlangıç noktası,
writevereadsistem çağrılarıyla 256KiB tamponu tekrar tekrar pipe'tan geçiren bir programwrite, aynı 256KiB tamponu sürekli yazarread, toplam 10GiB okur ve throughput'u yazdırır- Kod
pipes-speed-testiçinde yer alıyor
- Karşılaştırma için alınan, yoğun biçimde optimize edilmiş FizzBuzz programı
pvölçümünde yaklaşık 36.2GiB/s çıktı veriyor- FizzBuzz, L2 önbellek boyutuyla aynı olan 256KiB bloklar kullanıyor
- Bu deney de aynı 256KiB blokları kullanıyor; ancak çıktı içeriğini hesaplamadan, pipe IO üst sınırına yakın bir değer görmek istiyor
- İlk uygulamanın sonucu şöyle
./write | ./read- 3.7GiB/s, 256KiB tampon, 40960 yineleme, toplam 10GiB aktarım
write ve read neden yavaş?
perf record -gveperf reportile bakıldığındawritetarafındaki sürenin yaklaşık %47'sipipe_writeiçinde harcanıyorpipe_writeiçindecopy_page_from_iterve__alloc_pagesbüyük pay alıyor- veri kopyalama
- sayfa ayırma
- zamanlama
- bekleme-uyandırma
- kilit alma-bırakma
- Linux pipe'ı, sayfa referanslarını tutan bir ring buffer
pipe_inode_info,head,tail,pipe_bufferdizisini içerirpipe_buffer,struct page *page,offset,lenalanlarını içerir- x86-64'te normal sayfa boyutu 4KiB'dir
- Varsayılan ring buffer yuva sayısı 16'dır; örnekteki 8 yuvalı pipe en fazla 32KiB taşıyabilir
headyazma sonunu,tailise okuma sonunu gösterir- pipe dolarsa
writebloklanır - pipe boşsa
readbloklanır
- pipe dolarsa
pipe_writekabaca şu sırayla ilerler- pipe zaten doluysa yer açılana kadar bekler
- Mevcut
head'in gösterdiği tamponda yer varsa önce onu doldurur - Boş yuva ve kalan veri varsa yeni sayfa ayırır, doldurur ve
head'i günceller
pipe_readise tersine sayfaları tüketir, tamamen okunan sayfaları serbest bırakır vetail'i günceller- Bu yolda her sayfa iki kez kopyalanır
- kullanıcı belleğinden çekirdeğe bir kez
- çekirdekten kullanıcı belleğine bir kez daha
- Kopyalama 4KiB sayfa birimleriyle yapılır; araya eşzamanlama ve sayfa ayırma-serbest bırakma karışır
- Aynı makinede tek iş parçacıklı sıralı RAM okuması yaklaşık 16GiB/s idi; pipe yolunun karmaşıklığı düşünülünce
write/read'in yaklaşık 4 kat daha yavaş olması şaşırtıcı değil
vmsplice ve splice ile kopyalamayı azaltmak
- Hızlı IO'da, kullanıcı belleği ile çekirdek tamponu arasındaki kopyalama maliyeti darboğaz olabilir
- Linux, pipe ile ilgili olarak kopyasız taşıma için sistem çağrıları sunar
vmsplice, pipe'a konacak tamponlarıstruct iovecdizisiyle belirtir- Dönüş değeri, gerçekten pipe'a giren bayt sayısıdır
- Pipe ring buffer boyut sınırı nedeniyle istenen toplam boyut tek seferde sığmayabilir
vmsplice, kullanıcı belleğini kopyalamadan pipe'a bağladığı için, okuma tarafı veriyi tüketmeden önce tamponun yeniden kullanılmamasına dikkat etmek gerekir- FizzBuzz programı bunun için double buffering kullanır
- 256KiB tamponu iki adet 128KiB yarıya böler
- Pipe boyutunu 128KiB yaparak 4KiB sayfa bazında 32 yuva oluşturur
- İki yarım tamponu sırayla doldurur ve
vmspliceile pipe'a verir
- Test programı gerçekte tampon içeriğini yeniden yazmasa da, gerçek çıktı üreten programlarda gereken yapıya benzer biçimde double buffering'i korur
writeyerinevmsplicekullanıldığında throughput 12.7GiB/s'ye çıkar- kopya miktarı yarıya iner
write/read'e göre 3 kattan fazla iyileşme sağlar
- Okuma tarafı da
spliceile değiştirildiğinde tüm kopyalar kaldırılır ve hız 32.8GiB/s'ye ulaşır - Ancak
vmsplice'ın güvenliği konusunda dikkat gerekir- sayfa yeniden splice edilirse ömrü uzayabilir
SPLICE_F_GIFTolmadan da güvenli olup olmadığı net değildir- kopyasız (zero-copy) pipe'ları güvenli kullanmak için özel dikkat gerekir
iov_iter_get_pages ve sayfa dönüştürme maliyeti
vmsplicevespliceuygulandıktan sonraperf,vmspliceyolunda belirgin süre gösteriyoriov_iter_get_pages__mutex_lock.constprop.0add_to_pipe
iov_iter_get_pages,vmsplice'a verilenstruct ioveciçindeki sanal bellek aralığını, pipe'ın taşıyabileceğistruct pagelistesinə dönüştürür- Süreçler fiziksel bellek adreslerini değil sanal adresleri kullanır
- CPU, sanal adresleri sayfa tabloları üzerinden fiziksel adreslere çevirir
- x86-64'te normal sayfa boyutu 4KiB'dir
- x86-64 sayfa tablosu, her seviyesi 512 kollu ağaç olan 4 seviyeli bir yapı olarak açıklanır
- Pipe'ın
pipe_bufferyapısıstruct page'e referans verirstruct page, çekirdeğin fiziksel sayfalarla ilgili metaveriyi yönettiği temel yapıdır- Bu nedenle
vmsplice, giriş sanal bellek aralığını fiziksel sayfa referanslarına çevirmek zorundadır
iov_iter_get_pagesiçindeki sürenin çoğuget_user_pages_fastiçinde harcanır- 128KiB tampon ve 4KiB sayfa temelinde toplam 32 sayfa alınmalıdır
- Pipe kodu uygulama gereği bir seferde
nr_pages = 16ile çağırır ve gerekirse tekrarlar; ancak splice edilen toplam sayfa sayısı 32'dir
get_user_pages_fast, CPU'nun yaptığına benzer şekilde sayfa tablosunu yazılımla yürüyerekstruct pagetoplar- Çağıran taraf sayfayı kullanırken fiziksel sayfanın yeniden kullanılmaması için
struct pagereferans sayısını artırmak gerekir- sonrasında
put_pageile referans sayısı düşürülmelidir
- sonrasında
- Başta tamponu
memsetile doldurmak, sayfa tablosu girdilerini önceden oluşturarakget_user_pages_fast'in yavaş yolundan kaçınabilirmemsetyapılmazsa örnekte throughput 25.0GiB/s'ye düşer- huge page kullanıldığında aynı etki görülmez
huge page ile sayfa işleme maliyetini azaltmak
- x86-64, 4KiB normal sayfaların yanında 2MiB ve 1GiB huge page'i destekler
- Sonraki deneyler yalnızca 2MiB huge page üzerinde duruyor
- 1GiB sayfalar nispeten nadir ve bu iş için fazla büyük görülüyor
- huge page, aynı bellek aralığını daha az sayfayla temsil ederek yönetim maliyetini düşürür
- sanal adresten fiziksel adrese çeviride sayfa tablosu seviyesi de bir azalır
- CPU'nun TLB yükünü azaltmaya da yardımcı olabilir
- Bu deneydeki doğrudan darboğaz, donanımın sayfa tablosu yürüyüşü değil; çekirdekteki yazılım yolu
get_user_pages_fast - Linux'ta 2MiB hizalı bellek ayırıp
madvise(..., MADV_HUGEPAGE)çağırarak huge page kullanımı istenebilir - huge page uygulandığında throughput 51.0GiB/s'ye çıkar
- Performans artışının nedeni,
struct page'in 2MiB sayfayı tek başına doğrudan göstermesi değildir- Çekirdek kodu genel olarak
struct page'in mevcut mimarinin standart sayfa boyutunu gösterdiğini varsayar - huge page, bir head
struct pageve birden çok tailstruct pageile temsil edilir - 2MiB huge page, 4KiB temelinde en fazla 512
struct pageile temsil edilebilir
- Çekirdek kodu genel olarak
- Yine de ilk giriş bulunduktan sonra ardışık
struct page'ler basit bir döngüyle üretilebilir; böylece sayfa tablosunu tekrar tekrar yürütme maliyeti azalır - Linux 5.17 sonrası çekirdekte, head page'i açıkça tanımlayan
struct foliobulunur; bu da çalışma zamanında head/tail kontrolü ihtiyacını azaltan bir iyileştirmedir
busy loop ile eşzamanlama maliyetini azaltmak
- huge page sonrasında
perf,wait_for_spaceve__wake_up_common_locksürelerinin öne çıktığını gösteriyor- yazılabilir alan bekleme maliyeti
- okuma tarafını uyandırma maliyeti
- Bu eşzamanlama maliyetinden kaçınmak için
vmsplicevespliceileSPLICE_F_NONBLOCKkullanılabilir- Pipe'a yazılamazsa hemen
EAGAINdöner - Çağıran taraf hazır olana kadar busy loop yapar
- Pipe'a yazılamazsa hemen
- busy loop uygulandığında throughput 62.5GiB/s'ye kadar çıkar
- Bunun bedeli de açık
vmspliceveyasplicehazır olana kadar beklerken CPU çekirdeği tamamen meşgul edilir- Daha fazla CPU kullanımı karşılığında gecikme ya da throughput kazanılır
- Sonuç olarak bu sentetik benchmark yaklaşık 3.5GiB/s seviyesinden yaklaşık 65GiB/s seviyesine kadar iyileştirilmiş olur
Kalan ayrıntılar ve pratik konular
- Optimizasyon süreci,
perfçıktısı ile Linux kaynak koduna birlikte bakılarak yürütüldü - Ele alınan konular, pipe ve splicing'in ötesinde daha geniş yüksek performanslı programlama başlıklarıyla bağlantılı
-
kopyasız işlemler
- ring buffer
- sayfalama ve sanal bellek
- eşzamanlama overhead'i
- Gerçek kodda iki tampon ayrı ayrı ayırılarak sayfa tablosu çekişmesi azaltılır
get_user_pages, sayfa tablosu girdilerinin referans sayısını artırır veput_pagebunu azaltır- İki tampon farklı sayfa tablosu girdileri kullanırsa referans sayısı güncelleme çekişmesi azalır
- Test,
tasksetile./writeve./readsüreçleri iki çekirdeğe sabitlenerek çalıştırıldı - Depoda,
get_user_pages_fastiçin bir sentetik benchmark da yer alıyor - huge page kullanımına göre hız farkı ölçülebilir
- splicing hâlâ muğlak ve riskli bir kavram olarak kalıyor; ilgili sorunlar çekirdek geliştiricileri için yük olmaya devam ediyor
-
1 yorum
Hacker News yorumları
Doğru anladıysam,
vmspliceokuyan ve yazan uçların ikisi de aynı anda kullandığında iki süreç arasında küçük bir paylaşımlı bellek mekanizmasına daha yakın görünüyorYani her iki sürecin de tamponun ne zaman okunup yazılacağına ve kullanımdan sonra nasıl iade edileceğine son derece dikkat etmesi gerekiyor. Hızlı ama aynı zamanda ürkütücü bir yöntem; herkesin yazabileceği naif uygulamanın mümkün performanstan 20 kat yavaş olması da üzücü
vmspliceın iki süreç arasında küçük bir paylaşımlı bellek mekanizması olduğunu sanmıyorum. Yalnızca kullanıcı belleğinden pipe'a doğru sıfır kopyayı destekliyor; ters yönde kopyalama gerçekleşiyorAyrıntılar için bkz. https://mazzo.li/posts/fast-pipes.html#fn10
Pipe, soket, dosya ve belleği soyutlayıp bu tür optimizasyonları da yapan bir veri işleme kütüphanesi olup olmadığını merak ediyorum
C, C++, Rust ya da başka sistem dillerinde böyle bir kütüphane var mı bilmek isterim. Yazıda geçen
splice()vevmsplice()gibi API'lere aşina olmadığım için, düşük seviyeli uygulamalar geliştirirken mümkün olduğunda bu optimizasyonları otomatik olarak kullanan bir kütüphane var mı diye merak ettim.libuv,tokio,Nettynin Linux'ta bunu otomatik halledip halletmediğini de merak ediyorum; kısa bir aramada belki öyle olabileceği izlenimine kapıldımÜst seviye kod bu özellikleri nadiren kullanır; çünkü oldukça özel amaçlıdır ve Linux'a göre özelleştirilmesi gerekir. Veriye bakmadan onu Linux üzerinde sadece taşıyorsanız
splicekullanışlıdır. TCP/UDP proxy gibi uygulamalarda kesinlikle gereklidir, ama sıradan bir HTTP sunucusuna pek uymaz. Böyle bir uygulama geliştiriyorsanız zero copy gibi anahtar kelimelerle sık karşılaşırsınız vesplicegöreceğiniz ilk sonuçlardan biri olurtokioiçin bir crate var. Otomatik değil ama ilginizi çekebilir: https://lib.rs/crates/tokio-spliceAraştırma ortamı dışında pek bilinmiyor gibi ve verimli bir Cosh uygulaması yazmak muhtemelen epey zaman alır. Özetle aktarım modları üç tane: move, share, copy. Örneğin move aktarımı, gönderenin okuma/yazma yetkisine sahip olduğu veriyi alıcıya tamamen devreder ve sayfa tablosunda sanal bellek yeniden eşlemesiyle uygulanabilir. Ayrıca gönderen ve alıcının iş birliği yapacağına güvenilip güvenilemeyeceğini ya da sanal bellek izinlerinin yeniden eşlenmesiyle katı biçimde yalıtılmaları gerekip gerekmediğini belirten strong/weak özellikleri de var. Açıkçası, aşırı optimize edilmiş bir pipe gibi şeylerle güvenilir biçimde boy ölçüşecek kadar iyi optimize edilip edilemeyeceğini bilmiyorum; bu, “yeterince akıllı derleyici” problemine de dönüşebilir. Yine de denemeye değer olduğunu düşünüyorum
[1] https://barrelfish.org/publications/trios14-baumann-cosh.pdf
2022 öncesi tartışma: https://news.ycombinator.com/item?id=31592934
4 yıl önce tesadüfen öğrendiğim şaşırtıcı bir gerçek: Linux pipe kullanmak nondeterministic davranış doğurabiliyor
https://www.gibney.org/the_output_of_linux_pipes_can_be_inde...
echoedilen verinin hiçbirini gerçekten aktarmıyor(echo red; echo green 1>&2) | echo blue,|işaretinin iki yanında iki subshell oluşturur. Subshell, mevcut shell’in çocuk süreci olduğundan açık dosya tanımlayıcıları tablosu gibi önemli özellikleri miras alır. İki subshell aynı anda çalışır; ebeveyn shell ise yalnızca tüm çocuk süreçlerin bitmesiniwait()eder. Hangi çocuğun önce çalışacağı çoğunlukla tahmin edilemez; çok çekirdekli sistemlerde kelimenin tam anlamıyla aynı anda da çalışabilirler. Soldaki subshell’in standart çıktısı pipe’ın yazma ucuna, sağdaki subshell’in standart girdisi okuma ucuna bağlanır. Amaecho bluegirdi okumaz, yalnızca çıktı verir; dolayısıyla pipe’tan hiçbir şey okunmaz.echo green >&2, standart çıktıyı pipe’a değil, standart hatanın işaret ettiği yere gönderir. Sonuçtaecho greenileecho blueaynı dosyaya, muhtemelen doğrudan terminale yazar ve bir yarış durumu oluşur; sıra, hangisinin önce zamanlandığına göre değişirAksi halde pipeline kullanışlı olmazdı. Örneğin
curlile bir tar dosyasını indirip hemen açan bir pipeline’da,curlbitene kadar bekleyip sonratarçalıştırılsa büyük ara tar dosyasının nereye kaydedileceği gibi sorunlar çıkar.tar,curlçalışırken onunla birlikte çalışmalı ki tampon küçük kalsın ve işlem hızlı yürüsün. Pipeline programları arasındaki tek denetim akışı standart girdi ve standart çıktı üzerinden olur. Örnekte standart hataya yazıldığı için bunun belirleyici denetim akışına dahil olmaması doğalEk olarak, karışıklığı önlemek için: “Indeterministic” bir felsefe terimi, bilgisayar bilimi terimi ise “nondeterministic”tir
0. https://blog.superpat.com/zero-copy-in-linux-with-sendfile-a...
Bu komut muhtemelen bilerek tuhaf yazılmış; bir kod gözden geçireni kesinlikle durup düşünürdü.
echo redvar ama hiçbir yere aktarılmıyor. “red herring” şakası da olabilir.echo greenstandart hataya gittiği için ancakecho bluedan önce biterse görünür. Kesin sıra çıktı tamponlamasına bağlıdır; bu da hangi zaman diliminin önce verildiğine göre değişir ve CPU sayısı ile yüke bağlıdır. Yani nondeterministic, amatopun nondeterministic olmasıyla aynı şekildeÖzetle, iki programın mümkün olduğunca en iyi şekilde yazıldığını varsayarsak pipe’ın azami hızı, sistemdeki tek bir çekirdeğin okuyup yazabildiği hıza yakındır
Esasen çekirdek, bir programın standart çıktısından diğer programın standart girdisine aynı fiziksel bellek sayfasını eşlediği için işlem sıfır kopya olur; ya da daha az ideal durumlarda hızlı tek kopyaya yakınsar. Bunu öğrendikten sonra, iki veya daha fazla aracı pipe ile bağlayan shell script’leriyle çok yüksek performanslı işler yapmak hem tatmin edici hem de biraz komik geliyor. Araç kutusundaki en kullanışlı araçlardan biri
spliceveyavmsplicekullanıldığında geçerlidir. Bu Linux’a özgü sistem çağrılarını kullanmak zordur; özelliklevmspliceböyledirProgramların ve shell filtrelerinin büyük çoğunluğu,
pvgibi göze çarpan istisnalar dışında bunları kullanmaz; bu yüzden veriyi çekirdek belleğine kopyalayıp sonra tekrar dışarı alma maliyetini öderGeliştirmekte olduğunuz uygulama, yük karakteristiğine bağlı olarak pipe+süreçler ya da green/kullanıcı alanı thread’leriyle daha temiz uygulanabilir. Daha az konforlu olabilir ama mesaj iletimi çoğu zaman deadlock cehenneminden iyidir
Bu yazı Linux pipe’larını hızlandırma yöntemlerini ele alıyor, ancak paylaşımlı bellek veya mesaj kuyrukları gibi başka yöntemler hâlâ daha hızlı olabilir
Çok miktarda veriyi hızlı taşımak gereken sistemlerde pipe’ın ek adımları hızı düşürebilir. Birden çok thread veri paylaştığında da pipe başka yöntemlere göre daha fazla sorun çıkarabilir. Bu yüzden yazıdaki iyileştirmeler, hızın önemli olduğu gerçek durumlarda çok yardımcı olmayabilir
io_uringgibi bir şeyi seçmenin avantajı olurAma çift yönlü iletişimde iki taraftan hangisi olursa olsun verinin hazır olduğuna dair bir bildirim gerekir. CPU yakarak polling yapmak istemeyebilirsin; bu seçeneklerin söz konusu senkronizasyonu pipe’dan nasıl daha hızlı yaptığı da bana pek açık değil
Sayfa tabloları gibi şeyleri biliyordum, ama bunu
perfüzerinden performans analizi ile ilişkilendirince throughput açısından ne kadar merkezi olduğu netleştiPipe’lar harika. Diğer sürecin başka bir CPU’da mı yoksa başka bir makinede mi olduğu açıkçası pek fark etmiyor
https://github.com/nathants/s4/blob/master/examples/nyc_taxi...
Pipe’lar
cat,sed,awk,cut,grep,uniq,jqvb. araçları tekrar tekrar birleştirmek için yeterince hızlıdır