Ali Çehreli
| C ve C++'ta da olmadığı gibi, D'de de çok boyutlu dizi yoktur. (Yani, dilin kendisinde böyle bir kavram yok.) Elemanların türü dizi olunca, yani "dizi dizisi" olunca zaten çok boyutlu dizi haline geliyor:
import std.stdio;
// İki boyutlu olarak kullanılan bir int dizisi döndürür
int[][] diziDizisi(size_t satırAdedi, size_t satırUzunluğu) {
int[][] sonuç;
foreach (i; 0 .. satırAdedi) {
int[] satır;
satır.length = satırUzunluğu;
// Yukarıdaki iki satır yerine şu da olur:
// int[] satır = new int[satırUzunluğu];
sonuç ~= satır;
}
return sonuç;
}
void main() {
auto dizi = diziDizisi(3, 10);
writeln(dizi);
}
Çıktısı, üç tane dizi elemanı olan bir dizidir:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
Önce altını çiziyorum: Aşağıdaki anlatacaklarımın %99.99 programı etkilemez. :) Kodumuzu yukarıdaki gibi yazarız ve program hiçbir hız sorunu göstermeden mutlu mutlu çalışır. :)
Yine de, yukarıdaki kodun bazı sakıncaları vardır ve bunlar bazı durumlarda ölçülebilir hız kaybına neden olabilir:
1) Hem satırları tutan dizi, hem de satırlar dinamik bellekten ayrılıyorlar. Yani, yukarıdaki kodda en az (satırAdedi + 1) kere bellek ayrılır. (Ayrılan belleğin büyüklüğünden bağımsız olarak, salt bellek ayırma işlemi bile ölçülebilir derecede yavaş olabilir.)
2) Bellekler farklı adımlarla ayrıldıklarından, yukarıdaki dizinin bütün elemanları bellekte birbirlerinden uzak yerlerde duruyor olabilirler. Bu da modern mikroişlemcilerin belleği hızlı kullanma olanaklarından yararlanma şansını azaltır.
Durum gerçekten böyle olduğunda, çok eskiden beri bilinen bir yöntem, belleği tek parçada ayırmak ama elemanlar iki boyutlu olarak erişmektir:
import std.stdio;
// Bu sefer tek seferde ayrılmış olan bellek döndürür.
// Yani, dönüş değeri tek boyutlu dizidir.
int[] diziDizisi(size_t satırAdedi, size_t satırUzunluğu) {
int[] sonuç = new int[satırAdedi * satırUzunluğu];
return sonuç;
}
// Bunu bir sonraki programda kolaylaştıracağız.
// Durum bu haliyle çok kötü çünkü dizinin satır uzunluğunu da
// her seferinde vermek zorunda kalıyoruz.
ref eleman(int[] dizi, size_t satırUzunluğu, size_t satır, size_t sütun) {
size_t buSatırınBaşı = satır * satırUzunluğu;
size_t elemanınYeri = buSatırınBaşı + sütun;
return dizi[elemanınYeri];
}
void main() {
size_t satırAdedi = 3;
size_t satırUzunluğu = 10;
auto dizi = diziDizisi(satırAdedi, satırUzunluğu);
foreach (satır; 0 .. satırAdedi) {
foreach (sütun; 0 .. satırUzunluğu) {
import std.conv;
int değer = (satır * 1000 + sütun).to!int;
eleman(dizi, satırUzunluğu, satır, sütun) = değer;
}
}
writeln(dizi);
}
Programda da belirtildiği gibi, böyle bir diziyi kendisiyle çok ilgili olan satırUzunluğu kavramında ayrı tutmak, hem kullanışsızdır hem de hataya açıktır. İşte böyle durumlarda aklımıza struct geliyor. Hatta, diziDizisi() gibi bir "kurma" işlevi yerine struct'ın kurucu işlevini (this()) kullanabiliriz:
import std.stdio;
struct Dizi {
int[] elemanlar;
size_t satırUzunluğu;
this(size_t satırAdedi, size_t satırUzunluğu) {
this.elemanlar = new int[satırAdedi * satırUzunluğu];
this.satırUzunluğu = satırUzunluğu;
}
// Önceki 'elemanlar' ve 'satırUzunluğu' değişkenleri
// zaten artık üye değişkenler...
ref eleman(size_t satır, size_t sütun) {
size_t buSatırınBaşı = satır * satırUzunluğu;
size_t elemanınYeri = buSatırınBaşı + sütun;
return elemanlar[elemanınYeri];
}
}
void main() {
size_t satırAdedi = 3;
size_t satırUzunluğu = 10;
auto dizi = Dizi(satırAdedi, satırUzunluğu);
foreach (satır; 0 .. satırAdedi) {
foreach (sütun; 0 .. satırUzunluğu) {
import std.conv;
int değer = (satır * 1000 + sütun).to!int;
dizi.eleman(satır, sütun) = değer;
}
}
writeln(dizi);
}
Tabii, eleman() gibi bir işlev yerine opIndex() ile işleç yükleme olanağından da yararlanabilirdik. (Öğretmen bu konuyu ödev olarak bırakıyor. :p)
Konu burada bitmiyor.
Yukarıdaki program, dizi satır satır işlenecekmiş gibi yazılmış. Yani, tek boyutlu dizinin elemanları "önce ilk satır, arkasından ikinci satır, vs." olarak yerleştiriliyor. Mikroişlemcinin belleği önceden hızlıca okuyan düzeneği (prefetch), biz satırdaki elemanlara art arda erişirken yararlı olacaktır. (O düzenek bellekte ileriye doğru ilerlendiğini algılar ve sonraki baytları da okur.) Güzel...
Ancak, eğer elemanlara "önce ilk sütun, sonra ikinci sütun, vs." diye erişeceksek mikroişlemcinin düzeneğinin hiçbir yararı olmaz. Hmmm. Bu durumda belki de tek boyutlu diziyi şöyle tasarlamalıymışız: önce ilk sütun değerleri yan yana, sonra ikinci sütun değerleri, vs. Bu da çok kolay:
1) satırUzunluğu yerine satırAdedi değişkenini tutmak
2) eleman() işlevini şöyle değiştirmek:
ref eleman(size_t satır, size_t sütun) {
size_t buSatırınBaşı = sütun * satırAdedi;
size_t elemanınYeri = buSatırınBaşı + satır;
return elemanlar[elemanınYeri];
}
Bu son değindiğim konu, tek boyutlu dizinin "row-major" mı yoksa "column-major" mı olduğudur. (mir belgelerinde de geçiyor.)
Ali
|