ÖNEMLİ NOT
Tüm bu seri boyunca okuyucunun C/C++ bildiği varsayılacak ve
teknik terimlerin açıklamaları düzenli bir şekilde verilecektir. Tüm kod
örnekleri Courier fontu ile yazılacak ve aksi belirtilmedikçe C++ olacaktır.
Okuyucunun Visual C++ 6 (ya da üzeri / uyumlu bir C++ derleyicisi) kullandığı
varsayılacaktır.
Bölüm
1: Sistem Altyapısı
A-
SIMD mimarisi hakkında ön bilgi
Bu
yazı grafik programlama ile ilgili olmasına rağmen, ilk önce elimizdeki
sistemi tanıyarak ve bazı kurallar koyarak başlayacağız.
Bilgisayarlar
1983'den bu yana oldukça fazla gelişmelerine rağmen aynı taban üzerinde
ayakta kalmaktalar. Süreklilik gerektiren işlerde oldukça yetenekliler, fakat
düzensiz işlemlerde, örneğin değişken bir üç boyutlu sahneyi işlemekte,
oldukça yavaşlar. Oyunlarda sıkça yapılan şeylerden birisi bu şekildeki
verinin işlenmesi oldugu için, bu seride ilk yapacağımız şey,
bilgisayarların ne olduğunu (ve olmadığını) öğrenmek ve bu kısıtlamanın
üstesinden nasıl gelineceğini öğrenmek olacak.
Bir
günümüz bilgisayarı:
1- Doğrusal ve uzun bloklar halindeki işlemleri daha hızlı
yapabilir.
2- Ne kadar az hafıza erişimi yaparsa o kadar hızlı çalışır.
3- Elimizdeki "düzensiz" veri miktarı arttıkça
adresleme hataları da o denli artar.
Tüm
bunlar, Intel'in ve diğer firmaların şu anda kullandığı SIMD (Single
Instruction Multiple Data = Tek komutta birden çok veri) tarzı işletim ile
neredeyse tamamen örtüşen bir durum oluşturuyor. İlk kısıtlamamiz, bize
uzun blokların tek seferde daha hızlı işlendiğini söylüyor, bu da tam
olarak SIMD'nin yaptığı şey, uzun blokları tek seferde işlemek. Diğer iki
kısıtlama bize adres çözme işleminin problemlerini, yani "cache
miss" ve "misalign" sorunlarını anlatıyor. "Cache
miss" ile kastedilen, bir adres çözme işlemi sonucu erişilecek verinin
kaşe bellekte bulunmadığının anlaşılıp yeniden bir adres çözme işlemine
yol açması. "Misalign" ise çözülen adresin 8 (ya da şimdilerde
16 byte)'ın katlarına denk gelmemesi, örneğin hafızanın 3. byte'ında
bulunması sonucu gereksiz miktarda verinin birden çok kez okunmasına yol açmasına
verilen isim.
Şimdi
bu ufak tablodaki kısıtlamalardan kurtulmak için bu seri boyunca uyacağımız
kurallara bir göz atalım:
1- SIMD ve/veya SIMD2 işlemlerine ağırlık vermek ya da bunlara dönüşebilir
şekilde kod yazmak.
2- Veriyi daima "hizalanmış" hafıza adreslerinden başlatmak.
3- Veriyi daima 16 byte'ın katları olarak tutmaya çalışmak, mümkünse
{X,Y,Z} gibi sırasız veri akışını {X[],Y[],Z[]} haline getirmek.
Üçüncü
madde biraz karışık gelebilir. Tipik bir programda sakladığımız veri eğer
class nokta
{
float x, y,
z;
};
nokta benimNoktalarım[1024];
ise,
bu SIMD için daha uygun olan:
class yogunNokta
{
float x[1024], y[1024],
z[1024];
};
yogunNokta benimNoktalarim;
şeklinde
yeniden düzenlenmelidir. Tabii bu şekilde statik veri ayrılması pek önerilen
birşey olmadığı için bunu sadece konuyu açıklamak için verdiğimiz bir
örnek olarak yorumlamanız gerekiyor, pratikte:
class yogunNokta
{
float *x,* y,
*z;
};
yogunNokta benimNoktalarim;
x=(float*) aligned_malloc( 1024 * sizeof(float),
16);
y=(float*) aligned_malloc( 1024 * sizeof(float),
16);
z=(float*) aligned_malloc( 1024 * sizeof(float),
16);
daha
doğru bir yaklaşım. Buradaki aligned_malloc
komutu 16 byte'ın katı olan bir adresten başlayacak şekilde bellek ayırmamızı
sağlıyor.
B- Optimizasyon
Kullandığımız sistem, herşeyden önce, saf C/C++
kodunun daima en basit hedef işlemciye göre (zorlanmadıkça) derlendiği bir
sistemdir. Yani yazdığımız aşağıdakine benzer bir satır, eğer biz
derleyiciyi buna zorlamazsak, 80386'da hızlı çalışacak şekilde derlenir:
float
fArray[16] = {1.0f, 4.6f, 12.0f, 43.0f, 61.0f, 52.0f, 2.0f, 1.0f, 3.0f, 4.0f,
6.0f, 13.0f, 421.0f, 54.0f, 654.0f, 1.0f};
for(unsigned long i=0;
i<16; i++)
{
fArray[i] *= 2.49f;
}