İşlemci ile hafıza erişim hızları arasındaki fark arttıkça donanım mimarları da, oluşan performans kayıplarını önlemek amacıyla çeşitli teknikler geliştirmişler. Bunların başında elbette işlemci ve hafıza arasına yerleştirilen ve erişim hızı hafızaya nazaran daha fazla olan “cache” yapıları geliyor. Çeşitli mimarilerde, amacı aynı olan fakat davranış itibari ile birbirlerinden farklı olan bir çok “cache” yapısı geliştirilmiş. Herbir yapının da, sistem programlama açısından etkileri farklı olduğu için, bu yapılar hakkında da bahsedilen davranışların anlaşılması açısından geliştiricinin bilgi sahibi olması gerekiyor. Lkml arşivlerinde farklı cache yapılarının çekirdeğin çeşitli kısımlarındaki etkilerini anlatan David Miller’a ait güzel bir yazı var.

Konumuz elbette ki cache yapıları değil, “hafıza bariyerleri”. Ancak bu bariyerlerin de çıkış yeri yine “cache” yapılarında olduğu gibi düşük hafıza erişim hızları. Ve donanım mimarilerinde, bu düşük hızlardan minimum etkilenecek şekilde çalışmalarını sağlamak amacıyla yapılan optimizasyonlar.

İşlemci içerisinde bulunan “komut işlem odacıkları”‘nın daha verimli şekilde kullanılmasını sağlayan “pipeline” teknolojisinde, aşılması gereken çeşitli riskler mevcut. Pipeline‘ın performansını düşüren‘da hatalı sonuçlara sebebiyet verdiren bu riskleri işlemci’nin aşmasının en basit yolu da “baloncuk” yöntemi. Ancak bu yöntem pipeline’ın verimsiz kullanımına sebep oluyor.

Bu risklerin, programın derlendiği esnada derleyici tarafından tespit edilip olası baloncukların minimuma indirilmesi mümkün. Bu tekniğe “static scheduling” ismi veriliyor. Bir önceki bölümde anlatılan derleyici bariyerlerine duyulan ihtiyaç da işte buradan kaynaklanıyor.

Modern işlemcilerde, bir adım öteye gidilerek, baloncukları işlemci üzerinde dinamik olarak tespit edip önleme amacıyla çeşitli teknikler geliştirilmiş. Pipeline’da karşılaşılan “verisel” ve “yapısal” risklerde baloncuk kullanılmadan, komutların bir şekilde “program sırası” dışında çalıştırılması yöntemi ile pipeline’ın “bekleme yapmadan” çalışması sağlanabiliyor. Bu yönteme ise “dynamic scheduling” deniliyor. Komutların program sırası dışında çalıştırılması, söz konusu yalnızca tek bir işlemci olduğunda, sorun oluşturmuyor. Çünkü, her ne kadar bu teknik komutların program sırası dışında işlenmesini sağlasa da, tek işlemci açısından bakıldığında; sonuçlar, komutların tek tek ve sırasıyla çalıştırılmasından farksız şekilde neticelenecek. Daha anlaşılır bir deyişle program yapması gereken işi yapacak. Zaten aksi olması da düşünelemezdi.

Ancak işin içine, ortak paylaşılan bir hafıza ve bu kaynağı ortak kullanan birden fazla işlemci ile birlikte bilgisayar içerisinde bulunan ve DMA özelliği bulunan herhangi bir “memory-mapped” bir donanım girdiğinde işler değişiyor. Çünkü tek bir işlemcinin bakış açısına ek olarak, bu işlemcinin yaptıklarının sonuçlarının dışardaki gözlemciye (donanım ve diğer işlemciler) nasıl yansıdığının önemi ortaya çıkmış olacak.

Eğer komutlar, “program sırası” dışında işleniyorsa(”out-of-order”); bunun sebebi hafıza erişimlerinin sırasının(”memory ordering”) değiştirilmesidir. Eğer komut işleme sırası sıkı sıkıya program sırasına uyuyor ise, buna “sıkı sıkıya sıralı”, eğer bu sıra belli kurallar ölçüsünde değişiyorsa “az sıralı” olarak isimlendirilir. Bu kurallar bahsedilen iki uç nokta arasında platform’dan platform’a değişiklik gösterir. Mimaride alınan kararlar ve kullanılan optimizasyon yöntemleri bu kuralları belirler. Mimari içindeki bu kurallar bütününe “hafıza modeli” ismi veriliyor. Sistem programcısının üzerinde çalıştığı platformdaki bu kuralları bilmesi gerekli. Bu kurallar her işlemcinin ilgili datasheet’inde “Memory Ordering”, “Memory Model” ve ya “Memory Consistency Model” başlıkları altından bulunabilir.

Şimdi bu kurallardan biraz bahsedelim. Hafıza erişimleri program sırası dışında işlenip sonuçlanabilir. Sonuçlanma “SparcV9 Architecture Manual” içerisinde bölüm D.3.1′de şu şekilde tanımlanıyor: Yazma işlemi, işlemcinin yeni değerin yazılacağı adres ve yazılacak değere bağlıdır. Bu yeni değer sistemdeki tüm işlemciler tarafından görüldüğü zaman, yazma işlemi sonuçlandı demektir. Okuma işlemi, işlemcinin belli bir adresten bir değeri alma isteğidir. Okuma işlemi, işlemciye dönen değer, başka bir işlemci tarafından değiştirilemediği zaman sonuçlandı demektir.

Erişim sırasını değiştirmekteki yegane amaç erişimlerdeki gecikmeleri engellemektir:

Hafıza yazımı(store) gecikmesi için işlemciler içerisinde bir “write-buffer” bulunur. Yazma işlemi çıktısı, hemen bu buffer’a yazılır. Böylece hafızaya yazma beklenmeden hemen ardından gelen komutlar işlenebilir. Yapıya göre de “write-buffer”‘dan bir sonraki hafıza hiyerarşisi konumuna belli şartlarda/zamanlarda asıl yazma işlemi gerçekleşir. Örneğin bu, bir sonraki yazma için buffer’da yer kalmadığında olabilir. Buffer boşaltılabilir. Buffer’da verinin beklemesine sebep bir sonraki hiyeraşi konum satırı (cache satırı) o an meşgul olabilir. Diyelim ki, cache yapısının bu satırında daha önce meydana gelen bir “cache-miss” sonuçlandırılmaya çalışılıyor olabilir. O zaman meşgul olmayan ve uygun olan başka bir yazma işlemi sonuçlandırılabilir. Bu sıra program her çalıştığında farklı olabilir. Tabi bu örnek “Yazma işlemleri program sırasında sonuçlanmaz” gibi bir kural oluşturdu.

Hafıza okunma gecikmesini engellemek daha zor. Bunun için en basit yöntem bu okuma’ya ihtiyaç duyan komuta gelene kadar, bu okumayı pas geçmek olabilir. Oldukça ilkel bir yöntem. Okuma gecikmelerini engelleme teknikleri üzerine yazılan “Hiding Memory Latency using Dynamic Scheduling in Shared-Memory Multiprocessors” isimli bir makale mevcut.

Kural sınıflandırması, ardarda gelen okuma ve yazma işlemlerine göre yapılır. Yazma-Okuma(RAW), yazma işlemi ardından okuma gelmiş. Yazma daha “sonuçlanmadan” ardındaki okuma işlemi başlıyor mesela. Okumalar, yazma işleminin sonuçlanmasını beklemiyor ve başlayarak, onu “geçiyor”. Yazma-yazma(WAW), yazma işlemleri birbirlerinin sonuçlanmasını beklemeden başlayabilir, belki sonra gelen önce sonuçlanabilir. Okuma-okuma(RAR), okuma işlemi’nin ardındaki okuma işlemi, ilk okuma sonuçlanmadan başlayabilir. Okuma-Yazma(WAR), okuma işlemi sonuçlanmadan yazma başlayabilir, sonuçlanabilir.

İşte bu bahsi geçen genel kuralların uygulaması mimariden mimariye değişim gösterir. Mimarilerin “Hafıza Model”leri hakkındaki en doğru kaynak yine ilgili işlemci datasheetleridir.

Bu kadar ayrıntıdan sonra geldik hafıza bariyerlerine. Aslında derleyeci bariyerlerine çok benzer şekilde çalışıyorlar. İlgili platformlar “Hafıza Modelleri”‘nin “kontrollü” bir şekilde kullanılabilmesi ve programların yazılımcının beklediği şekilde sonuçlanabilmesi için yazılımcının “dışardan sıralama” yapmasına imkan sağlayan özel komutlara sahipler. Tabi ki bu komutlar mimarinin “sıkı sıkıya sıralı” ve ya “az sıralı” olmasına göre çeşitlilik gösteriyor. (strongly ordered - weakly ordered)

Okuma bariyerleri(rmb), bariyer öncesinde başlanmış ancak bekleyen tüm okuma işlemlerinin sonuçlanmasını sağlıyor. Yazma bariyerleri(wmb), yazma öncesinde başlanmış ancak bekleyen tüm yazma işlemlerinin sonuçlanmasını sağlıyor. “Tam bariyer”(mb), öncesinde başlamış ve bekleyen tüm yazma ve okuma işlemlerinin sonuçlanmasını sağlıyor. Sparc ve Alpha gibi bazı örnek mimarilerde daha ayrıntılı bariyerleri de bulmak mümkün.

Linux çekirdeği “en az sıralı” hafıza modelinden “en sıkı sıralı” hafıza modeline kadar tüm işlemcileri ortak kullandığı fonksiyonlar ile destekliyor. Belli kuralları içermeyen platform için çekirdekte ilgili fonksiyonlar hiç bir iş yapmıyor. Çekirdekte bahsi geçen fonksiyon isimleri mb(), wmb() ve rmb(), smp_mb(), smp_wmb(), smp_rmb(). Yazının başında hafızaya kendi açılardan bakan bir donanım ve ya farklı işlemciler olabilir demiştik. smp_ ile başlayan bariyer fonksiyonları birden fazla işlemci üzerinde çalışacak kodlar için, smp_ ile başlamayan fonksiyonlar da sistemde ortak hafıza erişimi (mmio) bir donanım ile yapılıyorsa yine gerekli olacak bariyerler. Bu fonksiyonlar incelendiğinde tümünün aynı zamanda derleyici bariyeri vazifesi gördüğü de görülüyor. Yine incelendiğinde bazı platformlarda fonksiyonların bir diğerini kapsadığı, yine smp_’siz bazı fonksiyonların sadece derleyici bariyeri olduğu görülüyor. Bunu da yine platformdan platform’a değişen “hafıza modelleri”‘nin ihtiyaçları belirliyor.

Son olarak çok kısa bir konuya daha deyinelim. Hafıza hiyerarşisi bir çok katman içeriyor. Örneğin bir hafıza işlemi, donanım hafızasına ulaşana kadar birden fazla köprü ve bus geçebilir. Bu köprülerde ve ya ara devrelerde işlemcinin yaptığı optimizasyonlara benzer şekilde yazma işlemleri cachelenip bekletilebilir. İşlemler bekletilir ki, örneğin fırsat olduğunda bir sonraki gelecek yazma işlemi ile birleştirilebilsin. Bu durumda senkronizasyon sağlamak için ise yazma işlemi ardından cihaz üzerinde bir okuma işlemi yapılması yeterli olacaktır. Bu okuma işlemi köprüde bekleyen tüm yazma işlemlerini “flush” eder. Belki daha sonra bu kısmı daha ayrıntılandırabiliriz.

Hafıza modelleri ve yapıları üzerine oldukça fazla kaynak bulmak mümkün. Bu yazıda anlatılanların yeterli başlangıç bilgisini sağlayacağını, en azından konu hakkında bir fikir vereceğini düşünüyorum. Bahsettiğim kaynaklara bir kaç örnek vermek gerekirse: “Hafıza Modelleri” üzerine yazılmış çok kapsamlı bir rapor. Linux Journal dergisinde yayınlanan “Memory Ordering” makaleleri. Yazı içerisinde geçen ilgili diğer linkler. Aynı zamanda çekirdek içerisinde kaynak kodları içerisinde yer alan Documentation/memory-barriers.txt dosyası. Bu dosya çekirdeğe özel olarak oldukça kapsamlı bir şekilde bu konuyu işliyor. İçerisinde anlatılan yapı “en az sıralı” sanal bir işlemci örnek alınarak kurulmuş; ki bu yapı’nın içine tüm modeller girebiliyor. Aynı dosya’nın sonunda yer alan, kullanılan kaynaklar kısmından da yararlanmak mümkün. memory-barriers.txt içerisinde anlatılan model bu kaynaklardan ortaya çıkan “abstract” bir mimarı. Ya da bu “abstract” mimariye acaba Alpha mı desek? :)

devam edecek… :)