22 puan yazan GN⁺ 2025-06-24 | 3 yorum | WhatsApp'ta paylaş
  • Linux'ta uygulanan Unix borularının performansı, kademeli optimizasyonlarla analiz ediliyor
  • İlk basit boru programının bant genişliği yaklaşık 3.5GiB/s olarak ölçülüyor; profilleme ve sistem çağrısı değişiklikleriyle bunun 20 katın üzerinde nasıl artırıldığı ele alınıyor
  • vmsplice, splice gibi Zero-Copy sistem çağrıları kullanılarak gereksiz veri kopyalamalarının azaltılması ve sayfa boyutunun büyütülmesi gibi çeşitli optimizasyon teknikleri açıklanıyor
  • Huge Page kullanımı ve busy loop tekniği uygulanarak darboğazlar gideriliyor ve en fazla 62.5GiB/s işleme hızına ulaşılıyor
  • Borular, sayfalama, senkronizasyon maliyeti, Zero-Copy gibi yüksek performanslı sunucu ve çekirdek programlamasında önemli unsurlar hakkında içgörü sunuluyor

Genel Bakış ve Giriş

  • Bu yazı, Linux'ta Unix borularının nasıl uygulandığını, boru üzerinden veri okuyup yazan test programları bizzat yazılarak performansın adım adım nasıl optimize edildiğini ele alıyor
  • Başlangıçta yaklaşık 3.5GiB/s bant genişliğine sahip basit bir programla başlanıyor ve çeşitli optimizasyonlarla yaklaşık 20 kat performans artışı elde ediliyor
  • Her optimizasyon adımı, perf aracıyla yapılan profilleme sonuçlarına göre belirleniyor; ilgili kaynak kodlar GitHub - pipes-speed-test üzerinde açık olarak bulunuyor
  • Esin kaynağı, borular üzerinden veri işleme hızını gösteren yüksek performanslı FizzBuzz programını (36GiB/s) görmenin ardından başlayan bir inceleme
  • C dili hakkında temel düzeyde bilgi sahibi olmak, içeriği anlamak için yeterli

Boru Performansını Ölçme: İlk Yavaş Sürüm

  • Yüksek performanslı FizzBuzz programının örnek çalıştırma sonucunda, boru üzerinden saniyede 36GiB veri işlediği görülüyor
  • FizzBuzz, L2 önbellek boyutuna (256KiB) uygun bloklarla çıktı vererek bellek erişimi ile IO ek yükü arasında denge kuruyor
  • Bu yazıda geliştirilen boru performans testi programı da 256KiB bloklarla tekrar tekrar çıktı üretiyor (read/write) ve ölçüm için read ile write uçlarının ikisi de doğrudan uygulanıyor
  • write.cpp aynı 256KiB tamponu tekrar tekrar yazıyor; read.cpp ise 10GiB okuduktan sonra sonlanıp aktarım hızını gösteriyor
  • Test sonucunda, boru üzerinden yapılan read/write işlemleri 3.7GiB/s hız veriyor; bu da FizzBuzz'a göre yaklaşık 10 kat daha yavaş

write Davranışındaki Darboğazlar ve İç Yapı

  • Program perf aracıyla çalıştırılıp çağrı grafiği izlendiğinde, toplam sürenin yaklaşık yarısının boruya yazma aşamasında, yani pipe_write içinde harcandığı görülüyor
  • pipe_write içinde zamanın büyük kısmı bellek sayfası kopyalama ve ayırma işlemlerinde (copy_page_from_iter, __alloc_pages) geçiyor
  • Linux boruları, halka tampon (ring buffer) yapısıyla uygulanıyor ve her giriş gerçek verinin tutulduğu bir sayfayı işaret ediyor
  • Borunun toplam tampon boyutu sabit; boru dolarsa write bloklanıyor, boşsa read bloklanıyor
  • C yapılarında (pipe_inode_info, pipe_buffer) head ve tail sırasıyla yazma/okuma konumlarını gösteriyor; ayrıca ayrı ayrı sayfaların ofset ve uzunluk bilgileri tutuluyor

Borunun Okuma/Yazma Mantığı

  • pipe_write şu sırayla çalışıyor
    • Boru doluysa yer açılana kadar bekliyor
    • Önce mevcut head konumundaki kalan alanı dolduruyor
    • Daha fazla alan gerekiyorsa yeni sayfa ayırıyor, veriyi tampona kopyalıyor ve head'i güncelliyor
  • Tüm işlemler kilitlerle (lock) korunuyor; bu da senkronizasyon ek yükü oluşturuyor
  • Okuma (read) tarafı da aynı yapıyla tail'i ilerletiyor ve okunan sayfaları serbest bırakıyor
  • Özünde veri kullanıcı belleğinden çekirdeğe, ardından tekrar kullanıcı alanına iki kez kopyalanıyor; bu da ciddi ek yük yaratıyor

Zero-Copy: Splice/vmsplice ile Optimizasyon

  • Hızlı IO için genel yaklaşım, çekirdeği atlamak (bypass) ya da kopyalamayı en aza indirmek
  • Linux, splice ve vmsplice sistem çağrıları ile boru ve kullanıcı alanı arasında veri taşınırken kopyalamanın atlanmasını destekliyor
    • splice: boru ile dosya tanımlayıcısı arasında veri taşıma
    • vmsplice: kullanıcı belleği ile boru arasında veri taşıma
  • Her iki sistem çağrısı da gerçek veriyi taşımadan yalnızca referansları aktararak çalışabiliyor
  • Örneğin vmsplice kullanılırken 256KiB tampon ikiye bölünüyor ve double buffering yöntemiyle her yarı sırayla boruya vmsplice ediliyor
  • Uygulamada vmsplice ile hız 3 katın üzerinde artıyor (yaklaşık 12.7GiB/s); okuma tarafına splice uygulanınca 32.8GiB/s'ye kadar ek artış sağlanıyor

Sayfa İlgili Darboğazlar ve Huge Page Kullanımı

  • perf analizi, vmsplice tarafındaki darboğazların boru kilidi (mutex_lock) ve sayfa elde etme (iov_iter_get_pages) üzerinde yoğunlaştığını gösteriyor
  • iov_iter_get_pages, kullanıcı belleğini (virtual address) gerçek fiziksel sayfalara (physical page) çevirip boru içinde bu sayfalara ait referansları saklıyor
  • Linux sayfalaması yalnızca 4KiB sayfalarla sınırlı değil; mimariye bağlı olarak 2MiB (huge page) gibi farklı boyutları da destekliyor
  • Huge Page (ör. 2MiB) kullanıldığında, sayfa tablosu yönetimi ve referans sayısı azaldığı için sayfa dönüşüm ek yükü belirgin biçimde düşüyor
  • Programda huge page uygulandığında en yüksek aktarım hızı 51.0GiB/s'ye çıkıyor; bu da yaklaşık %50 ek artış anlamına geliyor

Busy Loop Uygulaması

  • Geriye kalan darboğazlar, boruda yazma alanı açılmasını bekleme ve okuyucuyu uyandırma gibi senkronizasyon işlemleri
  • SPLICE_F_NONBLOCK seçeneği kullanılıp EAGAIN oluştuğunda busy loop ile çağrılar tekrarlanarak çekirdeğin zamanlayıcı ek yükü ortadan kaldırılıyor
  • Bu teknik uygulandığında en yüksek aktarım hızı 62.5GiB/s'ye ulaşıyor ve %25 daha artıyor
  • Busy loop, CPU kaynaklarını %100 tüketiyor; ancak yüksek performanslı sunucularda bu sık görülen bir yaklaşım

Özet ve Diğer Noktalar

  • perf ve Linux kaynak kodu analizi ile boru performansını adım adım çarpıcı biçimde artırma yöntemleri anlatılıyor
  • Borular, splice, sayfalama, Zero-Copy, senkronizasyon maliyeti gibi yüksek performanslı programlamanın temel meseleleri gerçek örneklerle incelenebiliyor
  • Gerçek kodda, tamponları farklı sayfalara yerleştirerek refcount contention'ı azaltmak gibi ek performans ayarları da uygulanıyor
  • Testlerde her program süreci ayrı çekirdeğe sabitlenerek (taskset) çalıştırılıyor
  • Splice ailesi tasarım gereği riskli olabilir ve bazı çekirdek geliştiricileri arasında uzun süredir tartışma konusu

3 yorum

 
iolothebard 2025-06-27

Vay canına! Çok eğlenceli! (Ne anlattığını hiç anlamıyorum ama...)

 
doolayer 2025-06-26

|

 
GN⁺ 2025-06-24
Hacker News görüşleri
  • Linux pipe tabanlı uygulamaları Windows’a port etme deneyimini unutamıyorum; POSIX standardı olduğu için performansın çok farklı olmayacağını düşünmüştüm ama inanılmaz derecede yavaştı. Pipe bağlantısının kurulmasını beklerken tüm Windows’un neredeyse donduğu bir sorun yaşamıştım. Birkaç yıl sonra aynı şeyi Win10 üzerinde C# ile yeniden uyguladığımda biraz daha iyiydi ama performans farkı hâlâ büyük bir utanç kaynağıydı.

    • Son birkaç yılda Windows’a AF_UNIX soketlerinin eklendiğini biliyorum; Win32 pipe’lara kıyasla hangisinin daha iyi performans verdiğini merak ediyorum. Tahminimce AF_UNIX daha iyi olacaktır.

    • “Performans berbattı” derken, pipe zaten bağlandıktan sonraki I/O’dan mı söz ediyorsunuz, yoksa bağlantı öncesi süreçten mi? Eğer bağlantı kurulduktan sonrasından bahsediyorsanız bu şaşırtıcı olur, ama sorun bağlantı/bağlantı koparma tekrarındaysa işletim sisteminin bunu optimize etmemiş olabileceğini kabul ederim. Zaten pratikte buna pek ihtiyaç olmaz; kullanım senaryosuna göre değerlendirmek gerekir.

    • Yakın zamanda kontrol ettiğimde Windows’ta yerel TCP performansının pipe’lardan çok daha iyi olduğunu gördüm.

    • POSIX’in yalnızca davranışı tanımladığını, performansı tanımlamadığını; her platform ve işletim sisteminin kendine özgü performans tuhaflıkları olduğunu hatırlatmak gerekir.

    • Eskiden bunun tersine bir deneyim yaşamıştım. Pipe değildi ama Linux’ta bir PHP uygulaması .NET tabanlı bir SOAP API ile iletişim kurarken, .NET tarafının yanıt süresi daha iyiydi.

  • Bu arada readv() / writev(), splice(), sendfile(), funopen(), io_buffer() gibi çeşitli yöntemler var. splice(), pipe ile UNIX soketi arasında zero-copy ile büyük veri aktarımında çok etkileyici ama Linux’a özgü. Veri aktarırken kullanıcı alanı bellek ayırma, ek buffer yönetimi, memcpy() ya da iovec taraması olmadan doğrudan işlendiği için en hızlı yöntem. BSD ailesinde pipe’lar için readv()/writev()’nin gerçekten en iyi seçenek olup olmadığına dair doğrulama da isteniyor. Her hâlükârda bu makale çok etkileyici bulunmuş.

    • sendfile(), dosya→soket zero-copy yöntemiyle çok yüksek performans sağlar; hem Linux hem BSD’de kullanılabilir. Ancak yalnızca dosya→soket yönünü destekler. sendmsg() ise genel amaçlı pipe’larda kullanılamaz; UNIX domain/INET/diğer soketler içindir. Bu arada Linux’ta sendfile’ın dahili olarak splice ile uygulanması sayesinde, dosya→blok aygıtı aktarımı için de gerçekten kullandım.

    • splice() Linux’ta pipe’lar arasında ultra hızlı büyük hacimli veri aktarımı için zirvede, ancak io_uring doğru kullanılırsa benzer hatta daha iyi performans beklenebilir.

    • shm_open gibi paylaşımlı bellek ve file descriptor aktarımı pratikte daha hızlıdır ve tamamen taşınabilirdir.

  • Bunun önceki bir HN tartışmasında da yoğun şekilde ele alındığını belirterek şu bağlantılar paylaşılmış: https://news.ycombinator.com/item?id=31592934 (200 yorum), https://news.ycombinator.com/item?id=37782493 (105 yorum)

  • Bunun gerçekten harika bir makale olduğu ve düzenli olarak yeniden gündeme gelmesinin de çok sevindirici olduğu söyleniyor.

    • Yazım düzeltmesi olarak comes → comes up belirtilmiş.
  • Henüz hiç yorum olmamasının üzücü olduğu söylenmiş; splice’ı daha fazla kullanmak istiyorum ama yazının sonunda bahsedilen güvenlik veya ABI uyumluluğu sorunları endişe veriyor. splice’ın gelecekte de korunup korunmayacağı ve performansı artırmak için varsayılan pipe’ın her zaman splice kullanacak şekilde yamalanmasının ne kadar zor olduğu da soruluyor.

  • Modern Linux’ta SunOS’taki Doors’a benzer bir şey olup olmadığı soruluyor; gecikmeye çok duyarlı küçük veri alışverişleri gerektiren gömülü uygulamalarda AF_UNIX’ten daha iyi bir teknik aranıyor.

    • Gecikme açısından en hızlısı paylaşımlı bellek, ancak görev uyandırma gerekir (genelde futex kullanılır). Google FUTEX_SWAP sistem çağrısını geliştiriyordu; bunun bir görevden diğerine doğrudan devir yapabilmesi planlanıyordu ama sonrasındaki durumu bilmiyorum.

    • ‘Doors’ fazla genel bir kelime olduğu için araması zor; açıklama istenmiş.

    • Şu anda AF_UNIX ile tam olarak neyin sorun olduğu soruluyor: Gerekli bir özellik mi eksik, gecikme istenenden mi yüksek, yoksa sunucu/istemci soket API yapısı mı uygun değil?

  • Makalenin yazım tarihinin 2022 olduğu bilgisi kısaca eklenmiş.