4 puan yazan GN⁺ 2024-03-31 | 1 yorum | WhatsApp'ta paylaş
  • Çekirdekler ve sürücüler gibi performans ve eşzamanlılığın önemli olduğu kodlarda bile bellek serbest bırakma basit bir free() çağrısıyla bitmez; kilitsiz paylaşım teknikleri gerekebilir
  • RCU (Read, Copy, Update), sık okunan ve nadiren değişen verileri kopyalayıp işaretçiyi atomik olarak değiştirerek okuma yolunu engellemeyen bir yöntemdir
  • Önceki sürüm hemen delete edilirse hâlâ okuyan thread'lerde use-after-free oluşabilir; bu yüzden RCU okuma aralıklarını izler ve serbest bırakmayı güvenli bir ana kadar erteler
  • RCU Linux'ta on binlerce yerde kullanılır; Folly C++ kütüphanesinde ve Rust'ın crossbeam-epoch paketinde de bulunur. Kullanımda olup olmamasına göre daha sonra temizlenmesi bakımından GC'ye benzer bir yapıya sahiptir
  • Elle bellek yönetiminin her zaman daha hızlı ve öngörülebilir olduğu ikili karşıtlığı zayıftır; free(), referans sayımı ve OS bellek davranışları da maliyet ve belirsizlik taşıdığı için modern GC de sistem programlamanın araçlarından biri olabilir

Çekirdek kodu neden RCU gibi teknikler kullanır

  • İşletim sistemleri, her gün çalışan programlar arasında performansa duyarlılığı yüksek olanlar arasında yer alır
  • OS hızlandığında kullanıcılar daha fazla hesaplama yapabildiği için çekirdek ve sürücü geliştiricileri kod optimizasyonuna büyük emek harcar
  • İşletim sistemi, kullanıcı alanındaki süreç ve thread'lerin yanı sıra çekirdeğin kendi içindeki birçok thread'i ve donanım kesme işleyicilerini de yönetmek zorundadır
  • Gecikme arttığında kullanıcının zamanı çalındığı için çekirdek kodunda thread'ler arasında veriyi kilit kullanmadan paylaşmaya yönelik çeşitli teknikler ortaya çıkmıştır

RCU'nun temel işleyişi

  • RCU (Read, Copy, Update) çok sık okunan ama nadiren yazılan verilere uygun bir yöntemdir
    • O anda bağlı olan USB aygıtları kümesi gibi neredeyse hiç değişmeyen ama değişmesi mümkün olan veriler buna örnektir
    • Değişiklik atomik olarak gerçekleşmeli ve hâlihazırda okuyan okuyucuları engellememelidir
  • Yazar paylaşılan durumu şu sırayla günceller
    • Mevcut veriyi işaretçiden okur
    • Mevcut veriyi kopyalar, gerekli değişikliği uygular ve yeni sürümü oluşturur
    • İşaretçiyi atomik olarak güncelleyerek yeni sürümü göstermesini sağlar
  • Okuyucu yalnızca paylaşılan işaretçiyi okuduğu için okuma yolu basittir ve beklemeden çalışır
  • Bu yöntem kullanımı kolay ve wait-free'dir; ancak eski sürümler temizlenmezse bellek sızıntısı oluşur

Hemen serbest bırakılamayan eski sürüm

  • İşaretçi yeni sürümle değiştirildikten hemen sonra eski sürüm delete edilirse use-after-free riski doğar
  • Kilitsiz çalıştığı için yazar, eski sürümü hâlâ okuyan okuyucu olup olmadığını bilemez
  • Okuyucu, rcu_read_lock() ve rcu_read_unlock() ile okuma tarafındaki kritik bölgeyi işaretleyebilir
    • Okuyucu yine de bloklanmaz
    • Yazar, bu okuyucular çıkana kadar eski veriyi kaldırmaz
  • rcu_synchronize() tüm okuyucuların ortadan kalkmasını değil, yalnızca eski sürümü görme ihtimali olan eski okuyucuların bitmesini beklemek zorundadır
    • Yeni işaretçiyi gören okuyucu yeni sürümü kullanacağı için eski sürümün ömrüyle ilgisi yoktur

Ertelenmiş serbest bırakma ve GC biçimi

  • Yazar güncelleme fonksiyonu içinde beklemek zorunda olmasa da eski veri bir gün güvenli biçimde serbest bırakılırsa kod doğru çalışır
  • rcu_defer(old) benzeri bir yöntem, mevcut okuyucular kritik bölgeden çıktıktan sonra herhangi bir zamanda old'un serbest bırakılmasını sağlayabilir
  • Özel bir thread'in referans verilmeyen eski sürümleri periyodik olarak temizlemesi nesilsel GC'ye benzer
  • RCU bir düşünce deneyi değil, pratikte yaygın kullanılan bir tekniktir
    • Linux RCU'yu on binlerce yerde kullanır
    • Facebook'un Folly C++ library içinde RCU sağlanır
    • Rust'ta crossbeam-epoch adıyla kullanılır ve popüler eşzamanlılık kütüphanelerinin temelini oluşturur
  • RCU'nun “gerçek GC” olup olmadığına dair sınıflandırma tartışmasından daha önemli olan nokta, belleğin kullanımda olup olmadığına göre daha sonra temizlenmesi yapısının GC ile aynı olmasıdır

Elle serbest bırakmanın gizli maliyetleri

  • GC'nin elle bellek yönetiminden özünde daha az verimli olduğu yönündeki yaygın kanı, uygulama ayrıntılarına bakıldığında zayıflar
  • free() bedava değildir

    • Genel amaçlı bellek ayırıcıları, çekirdekten alınan sayfalar, boyuta göre bucket bölümleri ve kullanımda olan bucket'lar gibi içsel global durumu yönetmek zorundadır
    • Birden fazla thread ayırıcı durumunu kilitlemeye çalıştığında çekişme oluşabilir
    • jemalloc gibi thread-local havuzlar kullanılsa bile bunları senkronize etmek için ek kod gerekir
  • RAII ve lifetime da ayırıcı maliyetini ortadan kaldırmaz

    • Rust'ın lifetime'ları veya C++'ın RAII yaklaşımı, belleğin otomatik serbest bırakılması ve doğruluk açısından yardımcı olur; ancak ayırıcının iç yapısındaki karmaşıklığı ortadan kaldırmaz
    • Birçok senaryoda shared_ptr veya Arc kullanımına dönmek gerekir
    • Referans sayısı adlı ek metadata gerekir ve bu değer çekirdekler ile cache arasında gidip gelerek maliyet yaratabilir
    • Liveness grafiğindeki döngülerin sızıntıya yol açmasına da neden olabilir
  • GC'nin sunabileceği optimizasyonlar da vardır

    • Taşınabilir nesilsel GC, heap'i periyodik olarak yeniden sıkıştırır
    • Ayırma, işaretçi artırmaya yakın hâle gelerek yüksek throughput sağlayabilir
    • Sıralı ayırmanın locality'si iyileştiği için cache performansına da yardımcı olur

Bellek yönetimi üzerinde denetim yanılgısı

  • GC'ye karşı çıkan birçok geliştirici soft real-time sistemler geliştirir
    • Video oyunlarında FPS veya streaming codec'lerde sıkıştırma performansı gibi mümkün olan en hızlı çalışmayı isterler
    • Ancak ara sıra 1 milisaniye fazla sürmesi sistemi bozan ya da insan öldüren hard latency gereksinimleri değildir
  • Programcının bellek yönetiminin ne zaman gerçekleşeceğine karar verebildiği inancı basit değildir
    • İşletim sistemi donanım etkileşimini soyutlar
    • Linux varsayılan olarak bellek isteği sırasında neredeyse hiçbir şey yapmaz ve belleği gerçekten kullanılmak istendiğinde verebilir
    • madvise(), memory-mapped I/O ve dosya sistemi cache'i işin içine girince “neyin ne zaman ayrıldığına” dair basit bir cevap kalmaz
    • Kötü bir günde basit bir işaretçi erişimi disk I/O'ya bile dönüşebilir
  • Programcının bellek yönetimi için durmaya uygun anı her zaman bildiği inancı da sınırlıdır
    • Video oyunu yükleme ekranları gibi net durumlar vardır
    • Birçok yazılımda tek yanıt, daha önemli bir işle meşgul olunmayan zamandır
    • shared_ptr ve Arc kullanan tekil kodlar, son sahip olup temizliği üstlenip üstlenmeyeceklerini önceden bilemez
  • free() çağrısının belleği hemen OS'e geri verdiği inancı da her zaman doğru değildir
    • Bellek OS'ten sayfa birimleriyle ayrılır
    • Ayırıcılar çoğu zaman sayfaları program bitene kadar elde tutup yeniden kullanmak ister
    • OS swap aracılığıyla sayfaları geri alabilir

GC'yi sistem programlama aracı olarak görmenin nedeni

  • Her yazılım GC'den fayda görmez
  • Ancak 2024'e yakın dönemde bile sistem programcıları arasında GC tartışması yanlış ikili karşıtlıklara ve korku, belirsizlik, şüpheye kolayca gömülebilir
  • GC kullanan dillerin elle bellek yönetimi yapan dillerden “açıkça” daha yavaş olduğu düşüncesi gerçek olmaktan çok ideolojiye yakındır
  • Hayati önem taşıyan sistemler geliştiren ekiplerde bile, neredeyse her satırda ayırma yapan GC'li bir dille mikrosaniyenin altında gecikme sunulan örnekler vardır
  • Sistemin bir bölümünün mutlaka n clock cycle içinde çalışması gerekiyorsa yalnızca o belirli bölüm GC dışı kod veya donanım olarak ayrılabilir
  • GC her derde deva değildir; ancak korkmadan kullanılabilecek araç kutusundaki araçlardan biridir

1 yorum

 
GN⁺ 2024-03-31
Hacker News yorumları
  • Umut vadeden modern paralel çöp toplama teknikleri arasında MPL veya MaPLe ve onun yeni Automatic Management of Parallelism yaklaşımına bakılabilir
    POPL 2024 distinguished paper award ve ACM SIGPLAN dissertation award 2023 kazanan bu çalışmanın özü iki nokta: a) disentanglement temelli, kanıtlanabilir biçimde verimli paralel çöp toplama, b) kanıtlanabilir biçimde verimli otomatik ayrıntı düzeyi denetimi
    [1] MaPLe (MPL): https://github.com/MPLLang/mpl
    [2] Automatic Parallelism Management: https://dl.acm.org/doi/10.1145/3632880

    • Standard ML ve topluluğu, bellek yönetimi literatürüne oldukça etkileyici katkılar yaptı
      Hem bağlantısı verilen makale var hem de bölge tabanlı bellek yönetiminin erken kullanıcıları ve öncülerinden biri olan MLKit var
    • Buradaki “kanıtlanabilir biçimde verimli” ifadesinin tam olarak ne anlama geldiğini merak ediyorum
    • Bu yaklaşımın, OCaml tarafında yakın zamanda yürütülen çok çekirdekli paralellik desteği çalışmalarıyla nasıl karşılaştırıldığını merak ediyorum
    • Daha bilgili olanlara sormak istiyorum: bu, başka dillere ne kadar uygulanabilir?
      Örneğin böyle bir yaklaşım Go'nun çöp toplayıcısını ciddi ölçüde hızlandırabilir mi, yoksa mevcut dil tasarımındaki sorunlara mı takılır?
  • RCU kullanım örnekleri ikna edici, ancak diğer durumlarda çöp toplama deneyimi iyi değildi
    Bu yazı, statik ömrün dinamik ömürden genel olarak daha iyi olduğu iddiasından ziyade, özelleştirilmiş bellek yönetimi çözümlerinin en iyi performansı verebildiği iddiası gibi okunuyor
    free()'nin belleği işletim sistemine geri verdiğine değil, ayırıcıya geri verdiğine inanıyorum. Bu, onu işletim sistemine geri vermekten çok daha iyidir ve sistem çağrıları yavaştır. Yine de mimalloc gibi, serbest bırakılan belleği bir sonraki malloc için hemen değil yalnızca periyodik olarak kullanılabilir hâle getirenler olabilir
    800 bayt ayırıp hemen free etmeyi 1 milyon kez tekrarlayıp benzersiz işaretçi sayısını sayarsanız, glibc malloc 1, jemalloc 1, mimalloc 4, Julia çöp toplayıcısı ise 62767 verdi
    62767 tane, yani yaklaşık 48MiB; çok kötü değil ama yine de bilgisayarımdaki L3 önbelleğini dışarı itiyor. Çöp toplama kullandığınızda yeni ayırmaların önbellekten değil RAM'den gelmesi neredeyse garanti ve bu da ayırma yoğun kodun performansını mahvediyor. Önemli olan yalnızca bellek yönetiminin kendi hızı değil, onun verdiği bellekle ne kadar hızlı çalışabildiğiniz de
    Julia'da bunu gösteren bir benchmark paylaştım: https://discourse.julialang.org/t/blog-post-rust-vs-julia-in...
    malloc/free, gerçek çalışma belleği yeterince küçükse onun önbellekte sıcak kalma şansı verir. mimalloc gibi ayırıcılar da, sıkıştırmalı çöp toplama gibi, ardışık ayırmaların birbirine yakın yerleştirilmesini hedefler; benim gördüğüm mimalloc'taki 4 benzersiz işaretçi 896 bayt aralıklıydı
    Sıkıştırmalı çöp toplama konusunda daha fazla deneyimim olsaydı belki daha az alaycı düşünürdüm, ancak çöp toplamanın güvenli bellek yönetimi için Rust'ın ödünç alma denetleyicisi gibi bir şeye göre çok daha karmaşık bir çözüm olduğunu düşünüyorum. Bu karmaşıklık derleyici ve çalışma zamanı geliştiricilerine yüklendiği için kullanıcılar açısından genelde sorun olmaz ve performansa duyarlı olmayan kod yazarken kabul edilebilir bir ödündür. Statik ömürlü RAII de daha özelleştirilmiş bir yaklaşım gerektirmeyen kod için makul bir ödündür ve yazıdaki örnek açıkça özelleştirilmiş çözüm gerektiren bir durum

    • Çöp toplama daha fazla işaretçi verdi, dolayısıyla önbellek yerelliği kötü demek tek başına yeterli değil
      Sıkıştırmalı çöp toplama, uzun süre çalışan programlarda yığın parçalanmasının TLB önbellek girdileri ile nesneler arasındaki boş alanı israf etmesi nedeniyle, neredeyse her zaman malloc'tan daha iyi önbellek kullanımı sağlar. Sıkıştırmalı çöp toplamanın bump allocator'ı, free belleği geri kazanmadığı için her ayırmada yeni bir işaretçi verir ama bu ayırmalar sıralıdır ve yığını sürekli tüketirken yalnızca en yeni nesnelere dokunulan durumlarda yine de önbellekte kalırlar. Ayırıcıların ve çöp toplayıcıların dalga etkilerini benchmark etmek son derece zordur ve bu tür sentetik benchmark'lara neredeyse her zaman şüpheyle yaklaşırım
    • Yazı, bu yaklaşımın neden RCU bağlamında iyi çalıştığını ve neden genel durumda pek iyi olmadığını açıklayıp sonra bunu bir kenara bırakıp yok sayıyor
      İnsanların endişelendiği şey, belleğin artık kullanılmadığını anlama sürecinin, kaynak kullanımının bittiğini ayırıcıya doğrudan söylemekten daha verimsiz ve deterministik olmaması. Serbest bırakmayı ertelemenin kendisini dert eden kimse görmedim
      Tüm canlı kümenin taranması seyrektir ve 30 yıldır çöp toplama algoritmaları iyileştirilerek neredeyse fark edilmeyecek seviyeye gelindi, ancak bu ifade insanların gerçekten sorun ettiği noktayı isteyerek ya da istemeden görmezden geliyor. Serviste çöp toplama sorunu çıktığında, orayı burayı ayarlayıp öfkeli ruhu gölgeler dünyasına geri göndermeyi uman bir şaman çağırmak gerekiyormuş gibi geliyor
      Çöpü işaretleyip artık kullanılmadığında bildirim alma yöntemi olsaydı, bu sürecin tamamı ortadan kalkardı. Çöp toplamada bellek ayırma çok hızlı olabilir, ancak adil karşılaştırma için işaretleme ve sıkıştırma maliyetlerini de amortize ederek dahil etmek gerekir
      Bir diğer büyük sorun da, aynı performansı elde etmek için çöp toplamanın genelde elle bellek yönetiminden çok daha fazla bellek gerektirmesidir. Hâlâ referans olup olmadığını tekrar tekrar ve fazladan doğrulamak için ek CPU gerekir; ayrıca iyimser sıkıştırma için ek bellek kopyalamayı da göze almak gerekir
      Son olarak yazı, ömür belirsiz olduğunda Rust'taki Arc/Rc gibi elle bellek yönetimi gerektiğini eleştiriyor, ama çöp toplayıcılı dillerde de finalizer'ın mutlaka çağrılacağına güvenilemediği için dış kaynakları kapatmak adına fiilen aynı altyapının kurulduğunu görmezden geliyor
      Bu tartışma son 20~30 yılda fazlasıyla tekrarlandı ve bu yazı, çöp toplamayla ilgili meşru kaygıları meme diye geçiştirmenin dışında yeni bir şey getirmiyor gibi görünüyor. Memeler eğlenceli, o yüzden sorun değil, ama doğru cevap genel geçer tek bir doğru olmadığıdır. Sisteminizin tasarım kısıtlarını karşılamaya uygun aracı kullanmanız yeterli
    • Bu iddia çok anlaşılır gelmiyor. Nesiller arası çöp toplama modelinde 0. neslin önbellekte olma ihtimali yüksektir ve üretim ile imhanın çoğu da tam burada gerçekleşir
      Bunun dışındaki daha uzun ömürlü ayırmalar, tanım gereği önbellek açısından kontrol edilmesi kolay olmayan şeylerdir. Yerellik, çöp toplamanın büyük avantajlarından biridir ve benim bildiğim tek sorun stop-the-world işaretle-süpürdür. Modern çöp toplayıcıların arka plan iş parçacıkları kullandığını biliyorum ama yine de stop-the-world olayları yaşandığını sanıyorum
    • free()'nin belleği ayırıcıya geri verdiği doğru, ancak uzun süre çalışan sunucularda bellek parçalanması ile uğraşmak hiç keyifli değil
      Özellikle slab allocator'ın yönettiği sayfaların iç parçalanması böyledir; yaygın bir sorun değildir ama ele alması zordur
    • Önbellek kullanımı bu kadar büyük bir kaygıysa, arena allocation elle bellek ayırmada olduğu kadar burada da iyi çalışır
      Neyse ki çöp toplamanın böylesine avantajlı gösterildiği örneklerle rekabet etmesi gereken alan çok fazla değil
  • Tüm belleğin kolayca arena olarak ele alınabildiği özel durumlar dışında, iyi bir izlemeli çöp toplama yaklaşımı throughput açısından manuel bellek yönetimini çoktan geride bıraktı; son dönemde gecikmeye etkisi de uygulamaların büyük çoğunluğu için yeterince kabul edilebilir düzeyde
    OpenJDK’nin ZGC’si tipik olarak iki ya da üç basamaklı mikrosaniye düzeyinde duraklama süreleri veriyor ve makul tahsis oranlarında en kötü durumda bile neredeyse hiç 1ms’i aşmıyor; bu da işletim sisteminin tetiklediği duraklamalarla benzer bir aralıkta
    Gerçekten önemli olan tek ödünleşim bellek kullanımı. Arena yaklaşımının her şeye iyi uyduğu ve en kötü gecikmenin düşük mikrosaniye aralığında olduğu özel nişler dışında temel soru şu: Uygulamam bellek kısıtlı bir ortamda mı çalışıyor, ya da RAM kullanımını azaltmak için başka şeylerden ödün vermeye değer mi?

    • Benim deneyimime göre tam tersi geçerli. Nesne başına ayrı yaşam süresi, nadir görülen özel bir durum; gerçek dünyadaki kodun büyük kısmında ise aynı ya da çok benzer yaşam süresine sahip ilişkili çok sayıda nesne var
      Böyle bir kodda tek tek nesnelerin yaşam süresini izlemek aşırıya kaçıyor. Sonuçta bellek yönetimi yaşam süreleriyle ilgilidir ve çok sayıda ayrı yaşam süresi yerine daha az sayıda ayrı yaşam süresi her zaman daha iyidir. Çünkü ister manuel ister otomatik olsun, yapılacak iş azalır
      Nesne yaşam sürelerini düşünmek zorunda olmamak son derece kullanışlıdır; iyi bir çöp toplayıcının iç karmaşıklığı epey yüksek olsa da çöp toplamalı dillerin başarılı olmasının nedeni de budur
    • Duraklama süreleri bir ölçüde çözüldü ama çöp toplamanın CPU kullanımı hâlâ oldukça yüksek
      Öngörmesi zor kuyruk gecikmeleri ve çeşitli istisnai durumların etkisi de sürüyor
    • İş yerinde ZGC ve Shenandoah için benchmark yapıyoruz; p100 duraklama süresi genelde 500us’un altında
      ZGC, Shenandoah’a göre daha az sayıda duraklama yapıyor gibi görünüyor; bu yüzden her duraklamada daha fazla iş yapıp biraz daha iyi performans veriyor
      Hâlâ üretim ortamı testleri yapmamız gerekiyor ama şu ana kadar, ZGC ve özellikle Java 21 sonrası nesilsel ZGC kullanıldığında çöp toplama duraklamaları büyük ölçüde çözülmüş bir sorun gibi görünüyor
    • Tüm belleğin kolayca arena olarak ele alınabilmesi ölçütü bana haksız görünüyor. Nesnelerin çoğu kolayca arena içinde tahsis edilebiliyorsa, bu tek başına çöp toplamaya duyulan ihtiyacın büyük bölümünü ortadan kaldırır
      Jai perspektifindeki gibi, bellek tahsisini yaygınlık sırasına göre dört kategoriye ayırabiliriz: 1) son derece kısa ömürlü olup fonksiyon stack’ine konabilenler, 2) kısa ömürlü olup yaşam süresi iyi tanımlandığı için frame/istek düzeyinde bellek arenasına konabilenler, 3) uzun ömürlü olup sahibi iyi tanımlandığı için alt sistemlere özel havuzlarla yönetilebilenler, 4) uzun ömürlü olup sahibi belirsiz olduğu için dinamik bellek yönetimi gerektirenler
      İzlemeli çöp toplamanın genel olarak manuel bellek yönetiminden üstün olduğunu savunacaksak, bunu sağda solda malloc/free çağıran bir sistemle değil, bu bakış açısı gözetilerek yazılmış sistemlerle karşılaştırmak gerekir. Modern C++/Rust pratikleriyle karşılaştırmak daha adil olabilir
      Yine de çoğu sistemde izlemeli çöp toplamaya dayanmanın çok daha pratik olacağına katılıyorum; ama bu tamamen farklı bir iddia
    • Bunun için kanıt gerekli
  • Yazı, RCU için motivasyon oluşturduktan sonra aniden direksiyonu kırıp genel amaçlı çöp toplamayı genel olarak savunmaya başlıyor
    Truva atı kadar olmasa da oldukça sert bir yön değişikliği gibi hissettiriyor

    • RCU’ya çöp toplama demezdim. Çünkü hiçbir anda nesneler çöp durumunda olmuyor
      Nesneler üç durumdan birinde bulunuyor ve mümkün olduğunca hızlı geçiş yapıyor: active, obsolete but alive for old readers, deallocated
      Kodun nasıl yazıldığına bağlı olarak, “obsolete-but-alive” nesneleri güvenli biçimde yeni new tahsisleri için yeniden kullanmak mümkün olabilir; ancak performansı tam olarak analiz etmiş değilim
      Çöp toplama tartışmalarında sık görüldüğü gibi, ne zaman shared_ptr/Arc kullanımına “geri dönmek” gerektiği çok muğlak. Pratikte referans sayımından kaçınmak, yani zaten sahiplik olduğunu kanıtlamak ya da dolaylı başvurudan tamamen kaçınmak, ciddi bir referans sayımı tabanlı sistemin özüdür. Hiçbir şey yapmamak, çöp toplamanın “bir gün bir şey yapması”ndan açıkça daha iyidir
  • Benim kullandığım yazılımlarda iki durum var. (1) her zaman özel tahsis edicilerin kullanıldığı ve tahsisten kaçınılacak sıcak yollar, (2) bunun dışındaki her şey
    (1) için çöp toplama olsun ya da olmasın fark etmez; ben zaten bunun dışına çıkarım. (2) için ise çöp toplama gerçekten kullanışlı ve doğru

    • Katılıyorum. Java/C++ birlikte kullanılan bir yerde bu ikiliği birlikte çalışabilirlikle çözmeye çalıştık ama sonunda çözdüğümüzden daha fazla sorun çıkardık
      Java’nın modern çöp toplayıcılarla yaptığı işler etkileyici ama onların bile Valhalla ile dolaylı olarak tahsissiz/düşük tahsisli kodun bir yeri olduğunu kabul ettiğini görüyoruz
  • Modern kullanıcı işletim sistemlerinde, yani özel RTOS olmayan işletim sistemlerinde yerleşik bir çöp toplayıcı bulunduğu gözlemi burada biraz ıskalanıyor
    Biz buna sadece böyle demiyoruz, bellek yönetimi diyoruz. Yerleşik çöp toplayıcısı olan dillere ne denir? Bellek yönetimli diller denir
    Eski tip “yukarıdan aşağıya” çalışan C programlarında bunu sık görürsünüz. Tahsis eder, sistem kaynaklarını temizlersiniz ama free ile pek uğraşmazsınız. Program bittiğinde işletim sistemi o belleğin tamamını geri alacaksa neden uğraşasınız ki
    Burada, programla daha az yalıtılmış, işletim sistemi düzeyinde bir çöp toplayıcı ya da dil çalışma zamanı çöp toplayıcısı gibi kaynakları ele alan bir işletim sistemi yapma fırsatı var. Ama genelde çöp toplamalı dillerde çöp toplayıcı çalışma zamanının neredeyse her satırına karmaşık şekilde dolanmıştır; bu yüzden yalnızca tek bir işletim sistemi dağıtımında denetimi işletim sistemine devredecek bir yapı kurmak pratik değildir
    Yine de yazık. Çünkü program düzeyi bellek yönetimi ile işletim sistemi düzeyi bellek yönetimi arasındaki yapay ayrımdan doğan kronik sorunları iyileştirmek için çok yer var

    • Program sonlandığında işletim sistemi halledeceği için yalnızca belleği değil, dosya, soket, thread gibi diğer kaynakları da özellikle serbest bırakmak gerekmez. AmigaOS değilse tabii
      Belleği serbest bırakmanın tek nedeni, uzun ömürlü uygulamalarda işletim sisteminden yeni bellek almadan onu başka tahsisler için yeniden kullanmaktır. Bir kez çalışıp biten komut satırı araçlarında buna genelde gerek yoktur
    • İşletim sistemi belleği ancak süreç sonlandığında serbest bırakabileceğini bilir. Dosya tanıtıcıları ve diğer kaynaklar için de aynısı geçerlidir
      Süreç işini bitirince çıkacak şekilde tasarlanmışsa, işletim sistemini bir çöp toplayıcı gibi kullanabilirsiniz
      Ama çalışan bir programın içinde hangi belleğin artık kullanılmadığını işletim sisteminin bilmesinin bir yolu hiçbir zaman olmadı. Muğlak araştırma amaçlı işletim sistemleri istisna olabilir. Bu yüzden bunu kaçırılmış bir fırsattan çok, kaçırıldığı söylenen şeyin anlamlı bir biçimde hiç var olmaması olarak görüyorum
      Öte yandan, çok basit ve kısa ömürlü programlar yazma tarzı tamamen meşrudur. CLI araçları ve onları betikleyen betik dilleri böyle çalışır, eski web sunucuları da CGI vb. ile böyle çalışıyordu ve bugün bile gayet makul bir yaklaşımdır
    • Java’nın Epsilon GCsi tam olarak budur
  • (1) RCU’dan genel amaçlı izlemeli çöp toplamaya geçiş, yem atıp sonra konuyu değiştirmek gibi geliyor
    (2) Elle bellek yönetimi sadece malloc/free çağrıları değil, yerleşimle de ilgilidir. Örneğin yapı dizilerinin ayrılması, inline yerleştirme, örtük ofsetler, packing gibi şeyler vardır

    • (2) hakkında, Virgil çeşitli düzeylerde bellek yerleşimini kontrol etmeyi sağlayan birçok özelliğe sahip
      Sanırım kastedilen “structure of arrays”; bu, tuple dizileriyle yapılabilir ve hedefe bağlı olarak doğal biçimde düzleştirilip normalleştirilir. Yani native hedeflerde bu, yapı dizisine dönüşür
      Bayt düzeyinde tam yerleşim de tanımlanabilir[1]; bu daha çok başka yazılımlarla birlikte çalışabilirlik veya ikili biçim ayrıştırma için kullanılır. Cebirsel veri tipleri unbox edilebilir ve yakında cebirsel veri tiplerinin tam kodlaması bile kontrol edilebilir olacak
      Virgil çöp toplama kullanır
      [1] https://github.com/titzer/virgil/blob/master/doc/tutorial/La...
      1. maddenin elle bellek yönetimi olduğuna katılmıyorum
        Modern yönetimli dillerde bellek yerleşimi kontrolünün yetersiz olduğu doğru, ama düşük seviye dillerde de durum mükemmel olmaktan uzak ve etki etmenin yolları kesinlikle var
  • Bu yazıda eksik kalan bir şey de async/await’in çöp toplamayla çok iyi uyuşması
    Kişisel ve biraz tuhaf üslup nedenleriyle async/await’ten hoşlanmıyorum ama uzatmayacağım
    Bunu TypeScript/JavaScript’te çok kullandım, Dart’ta da kullandım ve orada beklendiği gibi çalışıyor
    Rust’ta da kullandım ama bana göre felaket. Çok iş parçacıklı bir çalışma zamanında async/await kullanmak için gereken türden bellek yönetimini zorla araya sıkıştırmak tam bir cehennem
    https://doc.rust-lang.org/std/pin/index.html

    • Rust’ın tipik asenkron kodunda pinning gerekmez. Bu, kütüphane yazarları içindir
  • Çöp toplamayı savunan birçok yazıda gözden kaçan bir nokta var; bu yazıda da öyle görünüyor: bellek sadece bir kaynak türü
    Özellikle sistem programlamada doğru kodun dosya tanıtıcıları, soketler gibi harici kaynakları da yönetmesi gerekir. Çöp toplama yalnızca uygulamanın bellek kısmını çözdüğü için bu tür harici kaynakların işlenmesinde hiç yardımcı olmaz. Hatta işleri çok daha karmaşık hale getirebilir; .NET'te hiç de önemsiz olmayan bir IDisposable uygulamasını doğru yazmak için neler gerektiğine bakmak bile bunu gösterir
    Deneyimime göre RAII veya referans sayımı gibi yaklaşımlar, bellek ve harici kaynakları birleşik bir şekilde ele almayı çok daha kolaylaştırıyor; böylece doğru kod yazmak ve bunun üzerine akıl yürütmek de kolaylaşıyor
    Bu, çöp toplamaya açıkça karşı olduğum anlamına gelmiyor. Diğer her şey gibi artıları ve eksileri olan bir araç. Yazıda bahsedilen “manuel GC” olan RCU yaklaşımı, belirli işler için ilginç

  • Bellek ile diğer kaynaklar arasında büyük bir fark var. Bellek, işlem gücü gibi tüm hesaplamaların temel unsurudur
    Çoğu kuramsal hesaplama modelinin sonsuz bellek varsayması boşuna değil. İşletim sistemi çekirdeği ya da katı gerçek zamanlı uygulamalar gibi bazı yazılımlarda işlem gücü de elle tahsis edilir, ancak işlem gücünün elle tahsis edilmesini gerektiren diller neredeyse yoktur
    Benzer nedenlerle otomatik bellek yönetimi, hesaplamayı soyutlamak için çok faydalıdır. Bir alt yordamın bellek ayrıntılarının çağırana sızmamasını sağlar; CPU kullanımında da bu tür ayrıntılar nadiren görünür
    Önemsiz olmayan her hesaplama, sabit olmayan bir miktarda işlem ve bellek içerir; ancak G/Ç genellikle sistemin sınırlarında gerçekleşir. G/Ç yönetimi elbette çok önemlidir, ancak hesaplama kavramı ve hesaplama soyutlamasının merkeziliği açısından işlem ve bellek kadar temel değildir

  • Doğru; bellek güvenliği argümanı diğer kaynaklara da uygulanır
    Örneğin Rust sonunda G/Ç güvenliğine de kavuştu; bu yüzden Unix'teki OwnedFd gibi dosya tanıtıcıları veya Windows'taki OwnedHandle gibi tanıtıcılar, 4 sayısı gibi bir tamsayı değil, sahip olunan nesneler haline geliyor
    Yüzeyde bu, tanıtıcılar üzerinde aritmetik yapmak ya da ayrılmış bir değeri yanlışlıkla sentinel olarak kullanmak gibi aptalca hataları önleme meselesi gibi görünebilir; ancak sahiplik modeli sayesinde, tanıtıcılarla karmaşık işler yapılsa bile açık sahiplik oluşur ve bu da sonradan bakım yapacak kişiler için şeffaf olur

  • Ağırlıklı olarak C++'tan C#'a geçince, çöp toplamalı bellek yönetimini beğeniyorum ama dosya tanıtıcıları, soketler vb. şeyleri takip etmekten hoşlanmıyorum
    RAII'nin değerini gerçekten çok hissettim

  • Çöp toplamalı dilleri eleştirirken sıkça gözden kaçan nokta şu: Bunların çoğu, belirlenimli kaynak yönetimi için özelliklere sahip, ama pek çok kişi bunları öğrenmiyor
    Bazı dillerde RAII vardır, bazıları anahtar sözcük sağlar, bazıları ise arena benzeri yönetim ya da örtük yönetime sahip lambda'lar sunar. Bazı diller tür sisteminden biraz yardım alır, bazıları ise yukarıdakileri biraz biraz birleştirir
    Ayrıca sistem geliştiricilerinin statik çözümleyicilere güvenmek zorunda kalması gibi, bu dillerin statik çözümleyicileri de tür sisteminin tek başına yeterli olmadığı yerlerde gözden kaçanları doğrulayabilir

  • RAII elbette harika, ancak çöp toplama ve düzgün istisna işleme olan bir dilde kaynaklar güvenli biçimde ele alınabilir
    Örneğin Java'daki try-with-resources ifadesi, istisna oluşsa bile kaynakların güvenli şekilde serbest bırakılacağını garanti eder: https://docs.oracle.com/javase/tutorial/essential/exceptions...
    Bu temel yapı taşlarıyla da oldukça sağlam ve kaynak açısından güvenli sistemler kurulabilir