İçeriğe geç
edumintsBlog
Backend3 dk okuma

Mesele Bellek Gecikmesi, Aptal! (%100 CPU Yanılsaması ve Önbellek Optimizasyonu)

Kaynak: Hacker News

Paylaş
BackendEdumints Blog
İçindekiler 3 başlık

Veritabanınızın CPU kullanımını %100 olarak gördüğünüzde, işlemciniz gerçekte ne yapıyor? Basitlik adına CPU'ların kodları doğrusal olarak çalıştırdığını varsayarız; ancak gerçekte CPU'lar aynı anda birçok komutu işleyebilen veya konsolda %100 görünmesine rağmen bazen hiçbir şey yapmadan bekleyen son derece karmaşık yapılardır.

Bir CPU yalnızca hiçbir görev (Linux görevi) işlemci zamanına ihtiyaç duymadığında "boşta" (idle) kabul edilir. Dolayısıyla %100 kullanım, işlemci kapasitesinin tam anlamıyla doygunluğa ulaştığı anlamına gelmez. Peki bir CPU neden %100 görünürken tam kapasiteyle çalışmaz? Cevap genellikle bellektir (memory).

Bellek Gecikmesi ve CPU Önbellek Hiyerarşisi#

Bellek erişimleri, dahili CPU işlemlerinden katbekat daha yavaştır. Modern bir CPU nanosaniyede 4–8 komut işleyebilirken, RAM'e yapılan tek bir erişim 50–100 nanosaniye sürer. Başka bir deyişle, işlemciniz RAM'den gelecek tek bir veriyi beklerken 400 ila 800 komutu çalıştırabilecek kapasitededir:

  • CPU Çekirdeği: Nanosaniyede 4–8 komut
  • L1 Önbellek (Cache): ~1 ns gecikme · ~64 KB boyut · Erişim başına ~4–8 komut
  • L2 Önbellek (Cache): ~3–4 ns gecikme · ~1 MB boyut · Erişim başına ~15–30 komut
  • L3 Önbellek (Cache): ~10–15 ns gecikme · Birkaç yüz MB'a kadar · Erişim başına ~50–100 komut
  • DRAM (Ana Bellek): ~50–100 ns gecikme · GB'larca boyut · Erişim başına ~400–800 komut

CPU'lar bu darboğazı aşmak için sıfırdan CPU önbellekleri (caches) ile tasarlanmıştır. Çipin içine yerleştirilen L1 önbellek, RAM'den 50–100 kat daha hızlıdır. Ancak önbellekler oldukça küçüktür; L1 yaklaşık 64 KB iken, en üst düzey işlemcilerde bile en uzak önbellekler birkaç yüz MB ile sınırlıdır. Bu yüzden iyi bir veritabanı, önbelleklerden tam verim almak için veriyi küçük "parçalar" (chunks) halinde işler.

GROUP BY Sorgularında Önbellek Tuzağı#

Önbellek kullanımını daha iyi anlamak için GROUP BY toplamalarına (aggregations) bakalım. Bir veri kümesini belirli bir sütuna göre gruplayıp count veya sum gibi işlemler yaparken uygulanabilecek en "naif" yöntem, grup anahtarıyla indekslenen ve sürekli büyüyen bir HashTable açmaktır. Gelen her yeni satırda anahtarın tabloda olup olmadığını kontrol eder ve ilgili değeri güncellersiniz.

Sorun şu ki, milyonlarca benzersiz grubunuz varsa önbelleği hiç kullanamazsınız! Neredeyse her HashTable erişimi önbelleği ıskalayarak doğrudan RAM'e giden "soğuk" (cold) bir erişime dönüşür. Sonuç olarak CPU'nuz %100 kullanımda görünse bile gerçekte sürekli olarak yavaş RAM erişimlerini bekler.

Bunu kanıtlamak için gelişmiş Pivot motorundaki karmaşık mantığı kaldırıp şu basit kodu kullandığımızı düşünelim:

fn consume(&mut self, batch: RecordBatch, _sender: &mut dyn Sender<RecordBatch>) -> unary::Result<()> {
    let group_keys = batch.column(self.key_column).as_primitive::<Int64Type>();
    for &key in group_keys.values() {
        // Grup anahtarı yoksa 0 olarak ayarla, ardından 1 ekle
        *self.counts.entry(key).or_insert(0) += 1;
    }
}

Ardından basit bir SELECT count(*), UserID FROM hits sorgusu çalıştırdığımızda, rastgele bellek erişimlerinin işlemciyi nasıl darboğaza soktuğu açıkça ortaya çıkar.

Yazılım Geliştiriciler İçin Pratik Çıkarımlar#

  • %100 CPU metriğine aldanmayın: Yüksek CPU kullanımı her zaman yoğun hesaplama yapıldığını değil, işlemcinin bellekten veri beklerken (memory stall) zaman kaybettiğini gösterebilir.
  • Veri yerelliğini (Data Locality) gözetin: Büyük ölçekli HashMap yapılarında rastgele erişimler önbellek ıskalamalarına (cache miss) yol açar. Veriyi önbelleğe sığacak küçük yığınlar (chunks) halinde işlemek performansı katlayarak artırır.

Kaynak ve atıf

Bu yazı, Hacker News üzerinde yayımlanan içerikten yararlanılarak Edumints tarafından yapay zekâ desteğiyle Türkçe hazırlandı. Ayrıntılar ve yazarların kendi anlatımı için orijinaline bakabilirsin:

  • pivotlake.io/blog/its-the-memory-latency-stupid