Geliştirici günlüğü

Geliştirici Günlüğü #3: Sütun deposu ve bellek düzeni

Büyük strateji motoru 2 milyon nüfus birimini bellekte nasıl tutar? Sütun deposu, tek bellek bölgesi, varlık kimlikleri ve kutu düzenine karşı ölçüm.

İçindekiler
Solda dağınık kutuların arasında koşarak her birinden bir para toplayan yorgun bir işçi; sağda aynı paralar tek bir bantta dizili, işçi hepsini tek hamlede süpürüyor.

Kısaca: MKA Engine dünyayı tablolar ve sütunlar halinde tutar: bir sütunun bütün değerleri bellekte art arda durur (struct of arrays). Bütün sütunlar oyun yüklenirken bir kez ayrılan tek bir bölgededir, bu yüzden bir oyun günü yeni bellek istemez. Basit ölçümümüzde 2 milyon birimin toplamı sütun düzeninde, her birimin ayrı nesne olduğu düzenden 8 ila 20 kat hızlı çıktı.

Bir büyük strateji oyununun dünyası kocaman bir kayıt defteridir: 30.000 konum, 250 ülke ve 2 milyon nüfus birimi, her birinin onlarca bilgisi. Motor bu defteri her oyun gününde baştan sona okur ve yazar. Defterin bellekte nasıl dizildiği, oyunun ne kadar hızlı akacağını büyük ölçüde belirler. Bu günlük, MKA Engine'in veri deposunu anlatıyor: dünyanın bellekteki hali.

Kutu kutu mu, sütun sütun mu?

Bir nüfus birimini düşünün: bulunduğu konum, kişi sayısı, zenginliği, memnuniyeti, kültürü, dini. Programlamada alışılmış yol, her birimi bu bilgilerin hepsini taşıyan ayrı bir "kutu" olarak tutmaktır. Görseldeki soldaki işçi bu düzende çalışır: "bütün nüfusun kişi sayısını topla" dendiğinde 2 milyon kutunun her birine gidip içinden tek bir sayıyı alır. Kutular bellekte dağınıksa her biri ayrı bir yolculuktur.

Bizim yolumuz bir Excel sayfasına benzer. Dünya tablolardan oluşur ("konumlar", "ülkeler", "nüfus birimleri") ve her tablo sütunlardan. Fark şu: bir sütunun bütün değerleri bellekte art arda durur. 2 milyon birimin kişi sayıları tek bir şerittir; toplamak, sağdaki işçi gibi o şeridi baştan sona tek geçişte okumaktır. İşlemci art arda duran veriyi çok hızlı okur, çünkü bir sonraki değeri istemeden önce getirir.

ÖlçütKutu kutu (her birim bir nesne)Sütun sütun (MKA Engine)
Bellekte ne yan yana dururBir birimin bütün bilgileriBütün birimlerin aynı bilgisi
"Bütün kişi sayılarını topla"Her kutuya ayrı ayrı gidilirTek bir şerit baştan sona okunur
Okunmayan bilgilerKutuyla birlikte boşuna taşınırHiç dokunulmaz
Çok iş parçacığına bölmekKimin hangi kutuya yazdığı izlenmeliŞerit parçalara bölünür, her parça bir iş parçacığının
İki düzen

Ne kadar fark eder?

Bunu basit bir ölçümle gördük. 2 milyon nüfus biriminin kişi sayılarını toplayan aynı hesabı üç düzende koşturduk: sütun, art arda dizili kutular ve bellekte dağınık kutular. Kutuya, gerçek bir oyundaki gibi on kadar bilgi koyduk.

2 milyon birimin kişi sayısını toplamak (ms)

0102030401,5912,9531,64Sütun (MKA Engine)Art arda kutularDağınık kutular
2 milyon birimin kişi sayısını toplamak
EtiketSüre
Sütun (MKA Engine)1,59
Art arda kutular12,95
Dağınık kutular31,64
Küçük olan iyi. Aynı hesap, aynı veri, üç farklı bellek düzeni.Kaynak: Basit ölçüm, 5 koşunun en iyisi, AMD Ryzen 7 5700G, 15 Ekim 2026

Sütun düzeni art arda dizili kutulardan yaklaşık 8 kat, dağınık kutulardan yaklaşık 20 kat hızlı çıktı. Sebep basit: sütunda işlemci yalnız istenen sayıyı okur; kutuda her sayıyla birlikte okunmayan dokuz bilgiyi de taşır, dağınık kutularda ise her kutu için belleğin başka bir köşesine gider. Bir oyun günü bu türden yüzlerce geçişten oluşur.

Bütün dünya tek bir bölgede

Motor bütün tabloların bütün sütunlarını bellekte tek, bitişik bir bölgeye yerleştirir. Tablolar ve sütunlar oyun yüklenirken bir kez bildirilir (buna şema diyoruz); bölgenin boyu şemadan hesaplanır ve bir kez ayrılır. Sonrasında bir oyun günü yeni bellek istemez. Günlük #0'daki "gün döngüsünde sıfır bellek ayırma" ölçümü buradan gelir.

  • 2.000.000 nüfus birimi (satır)
  • 28 bayt bir birimin dört bilgisi: konum, kişi sayısı, zenginlik, memnuniyet
  • 56 MB bütün nüfus tablosu
  • 62 MB 600 oyun yıllık koşunun tepe bellek kullanımı
Ölçek dünyasının nüfus tablosu

Her sütunun türü bellidir ve boyu sabittir: sayılar 8 bayt, tarihler ve küçük kimlikler 4 bayt, daha küçük değerler 2 ya da 1 bayt. Metin ya da değişken boylu veri bölgeye girmez. Bu sayede bir satırın yeri, satır numarasından tek bir çarpmayla bulunur.

Bu bölgenin nerede durduğu da bir karardı. Modların formülleri WebAssembly içinde çalışıyor; veriyi onlara her seferinde parça parça kopyalamak yerine bölgeyi onlarla paylaşılan bir WebAssembly belleği üzerine kurduk. Ölçtük: önbelleğe sığan hafif işte parça kopyası 2,2 ila 3,3 kat yavaştı; paylaşılan bellek ağır işte düz Rust ile aynı hızdaydı, hafif işte %12 ila 34 yavaştı. Bedeli de yazalım: WebAssembly'nin çok iş parçacıklı belleği, kullandığımız çalışma ortamında ikinci kademe bir özellik; eski sürümlere güvenlik düzeltmesi geri taşınmıyor.

Değişti işareti

Her satırın yanında tek bir bit durur: "bu adımda değiştim". Bir sistem bir satıra yazınca bit kalkar. Adımın sonunda dünyanın parmak izi (Günlük #2'de anlattığımız) yalnız değişen satırların bulunduğu parçalar için yeniden hesaplanır; değişmeyen parçalar eski izini korur. 2 milyon satırın bir kısmının değiştiği sıradan bir günde bu, işin büyük kısmını atlamak demektir.

Varlıkların yaşamı

Strateji oyununda dünya sabit değildir: ordular kurulur ve dağılır, karakterler doğar ve ölür, ülkeler yıkılır. Bunun için tabloların bir kısmı "dinamiktir": satırları oyun sırasında eklenir ve silinir.

Kimlik: koltuk ve kuşak

Bir varlığın kimliği iki sayıdan oluşur: oturduğu koltuk ve o koltuğun kaçıncı sahibi olduğu, yani kuşak. Bir karakter ölünce koltuğu boşalır; yeni bir karakter aynı koltuğa oturduğunda kuşak numarası bir artar. Ölen karakteri hâlâ "düşmanım" diye anan bir kayıt, koltuk aynı olsa bile kuşak tutmadığı için yeni karaktere değil "hiç kimseye" çıkar. Boş bir kimlik hiçbir zaman yaşayan bir varlığa denk gelmez.

Ekleme ve silme gün sonunda

Bir gün içinde varlık eklemek ya da silmek istenirse istek bekletilir ve adımın sonunda topluca uygulanır. Böylece bir günün bütün sistemleri aynı dünyayı görür; biri hesap yaparken başka biri satırı ortadan kaldıramaz.

Bir varlık silinince ona bağlı kayıtlara ne olacağı, bağlantıyı tanımlayan sütunda baştan yazılıdır:

KuralNe olurÖrnek
ZincirlemeBağlı kayıt da silinirÜlke yıkılınca onun orduları da dağılır
BoşaltBağlantı "hiç kimse" olurHükümdar ölünce "danışmanı" alanı boşalır
KoruEski kimlik kalır, hiçbir şeye çıkmazBir savaşın "başlatan" kaydı tarih için durur
Silinen bir varlığa bağlı kayıtlar

İstenirse silinen satırların bir kopyası gizli bir arşiv tablosuna yazılır. Ölen bir hükümdar oyundan çıkar ama soy ağacı ve tarih ekranı için kaydı kaybolmaz.

Dinamik bir tablo bölgedeki yerini aşarsa kapasitesi iki katına çıkar. Bu, bellek ayrılan ender anlardan biridir ve günde bir değil, tablonun büyüklüğü iki katına her vardığında bir kez olur.

Gruplu tablolar, ilişkiler ve dizinler

Bazı sorular çok sık sorulur: "bu konumdaki nüfus birimleri hangileri?", "bu ülkenin vassalları kimler?", "bu konumun komşuları?". Bunlar için deponun üç aracı var:

  • Gruplu tablolar: Bir tablonun satırları bir anahtara göre sıralı tutulur; örneğin aynı konumdaki nüfus birimleri yan yana durur. "Bu konumun nüfusu" tek bir aralığı okumaktır. Satırlar yer değiştirdiğinde gün sonunda yeniden sıralanır; az satır kaydıysa yalnız kayanlar yerine yerleştirilir, çoksa hepsi bir kerede sıralanır. İki yol da aynı baytları üretir.
  • İlişkiler: "Kim kimin vassalı" gibi iki varlık arasındaki bağlar kendi tablolarında durur ve iki uçtan da aranabilir. Harita komşulukları gibi oyun boyunca değişmeyen ilişkiler yüklemede bir kez, sıkıştırılmış bir listeyle kurulur.
  • Dizinler: "Bu orduya kimler bağlı" gibi tersine sorular için dizin tutulur. Dizin dünyanın bir parçası sayılmaz: kayda yazılmaz, yüklemede sütunlardan yeniden kurulur. Hata ayıklama derlemesi her adımın sonunda dizini baştan kurulmuş haliyle karşılaştırır.

Betik değişkenleri

Oyunun ve modların kendi küçük bilgileri de vardır: "bu ülke haçlı seferi ilan etti mi", "bu karakterin rakipleri". Her varlık için önceden sütun açmak bellek israfı olurdu; bu yüzden bu değişkenler sütunların dışında, sahibine göre sıralı ayrı bir depoda tutulur. Sayı, bayrak, varlık, varlık listesi ve anahtar-değer eşlemesi olabilirler. Boşalan bir liste silinir; yani depodaki durum, oraya hangi sırayla yazıldığına bağlı değildir. Bir varlık silinince yalnız kendi değişkenleri silinir.

Gün içinde paralel erişim

Bir oyun günü birden çok iş parçacığına bölünür. Bunun güvenli olması için her sistem hangi tabloları okuyup hangilerine yazacağını oyun yüklenirken bir kez bildirir. Motor, aynı aşamada çalışacak sistemlerin birbirinin yazdığını okumayacağını yüklemede denetler; gün içinde hiçbir denetim ve bellek ayırma yapılmaz.

Gün içinde şerit parçalara bölünür ve her parçayı tek bir iş parçacığı alır; o iş parçacığı yalnız kendi parçasının satırlarına yazar. Bir sistem başka bir satıra bir şey söylemek isterse (örneğin "bu konuma 10 asker gönder") bir mesaj bırakır; mesajlar aşamanın sonunda satır sırasıyla uygulanır. Kaç iş parçacığı olursa olsun bu sıra değişmez.

Sınırlar

  • Sabit boylu veri: Bölgeye yalnız sabit boylu sayılar ve kimlikler girer. Metin ve değişken boylu veri başka yerde durur.
  • Büyüme ender ama bedava değil: Dinamik bir tablo kapasitesini aşınca kapasite ikiye katlanır ve o an bellek ayrılır. Ölçtüğümüz sıfır ayırma, sabit boyutlu dünyadadır.
  • Yazmak bildirmek demek: Bir sistem bildirmediği bir tabloya yazamaz. Bu, mod yazanlar için ek bir kural demektir.
  • Silinen kimliği taşıyan değişken: Betik değişkeni silinmiş bir varlığı gösteriyorsa kimlik kalır ama hiçbir şeye çıkmaz; bunu kontrol etmek içeriğin işidir.
  • Ölçümün kapsamı: Düzen karşılaştırması basit bir ölçümdür ve tek bir hesabı gösterir; gerçek bir oyun gününün maliyeti yerine geçmez.

Sık sorulanlar

Struct of arrays (SoA) nedir?

Her nesnenin bütün alanlarını bir arada tutmak yerine, her alanı bütün nesneler için ayrı bir dizide tutmak. Aynı alanı çok sayıda nesne için okuyan işlerde işlemci önbelleğini çok daha iyi kullanır.

Bu bir ECS (varlık bileşen sistemi) mi?

Benzer bir fikir ama birebir aynı değil. MKA Engine'de veriler oyunun yükleme anında bildirdiği tablolarda ve sütunlarda durur; sistemler hangi tabloları okuyup yazacaklarını önceden bildirir ve motor paralel çalışmayı yüklemede denetler.

Silinen bir karakterin kimliği yeni karaktere karışır mı?

Hayır. Kimlik koltuk ve kuşak numarasından oluşur. Aynı koltuğa oturan yeni karakterin kuşağı farklıdır; eski kimliği tutan kayıtlar yeni karaktere değil hiç kimseye çıkar.

Oyun sırasında bellek ayrılıyor mu?

Sabit boyutlu bir dünyada bir oyun günü bellek ayırmaz. Dinamik bir tablo kapasitesini aşınca kapasite ikiye katlanır; bu ender anlarda bellek ayrılır.

Kaynaklar ve kayıtlar

Dış kaynaklar:

İç kayıtlarımız (motorun deposu kapalı olduğu için bağlantı veremiyoruz):

  • Varlık modeli kararı: koltuk ve kuşak kimliği, silme kuralları, arşiv, betik değişkenleri, gün sonunda uygulama.
  • Sütun deposu kararı: tek bitişik bölge, değişti bitleri, yuva tabloları, değişken deposu, dizinler.
  • Düzen ölçümü: 15 Ekim 2026, 2 milyon birim, üç düzen; bilgisayar çok yüklüyken alındı.
  • 600 yıllık koşu: 14 Ekim 2026, ölçek dünyası, tepe bellek /usr/bin/time -v ile.

Bir sonraki günlükte bu deponun üzerinde dönen çarkı anlatacağız: bir oyun günü nasıl ilerler ve iş çekirdeklere nasıl dağıtılır?

Bu günlük hakkında sorularınız ya da önerileriniz varsa bize iletişim sayfasından yazabilirsiniz. Motorun genel tanıtımı için MKA Engine sayfasına, bütün günlükler için Geliştirici Günlükleri listesine bakabilirsiniz.

Kaynaklar ve ilgili belgeler

Bunlar da ilginizi çekebilir