> Yapılarıda Hizalama (Alignment in Struct): C'de derleyiciler yapı elemanlarına erişimi hızlandırmak için elemanların arasında belli bir kurala göre boşluklar bırakabilmektedir. Bu duruma "yapı elemanlarının hizalanması (struct member alignment)" ya da kısaca "hizalama (alignment)" denilmektedir. Örneğin: struct SAMPLE { short a; int b; short c; int d; }; struct SAMPLE s; Burada short türünün 2 byte int türünün 4 byte olduğu bir sistemde ilk bakışta s nesnesinin sizeof değeri 12 olacakmış gibi gözükmektedir. Ancak muhtemelen bu sizeof değeri yazdırıldığında 16 olduğu görülecektir. Çünkü derleyici a ile b arasında ve s ile d arasında 2 byte'lık boşluklar bırakacaktır. Yani s nesnesinin bellekteki organizasyonu şöyle olacaktır: a (2) Boşluk (2) b (4) c (2) Boşluk (2) d (4) Derleyicinin bıraktığı bu boşluklara İnglizce "padding" denilmektedir. Yapı elemanlarına erişim sırasında derleyici hangi elemanlar arasında ne kadar boşluk bıraktığını bildiği için bir sorun oluşmamaktadır. Örneğin struct SAMPLE türünden ps isimli bir gösterici olsun. Biz de ps->c ifadesiyle ps adresinden başlayan yapının c elemanına erişmek ieteyelim. Derleyci boşlukları nerelerde bıraktığını bildiği için c elemanının ps adresinden 8 byte ileride olduğunu hesaplayabilmektedir. C standartlarına göre yapı elemanları ilk eleman düşük adreste olacak biçimde sırasıyla dizilmektedir. Yapı elemanları arasında boşlukların bırakılabileceği standartlarda belirtilmiştir. Başka bir deyişle elemanlar arasında bırakılan boşluklar (paddings) elemen ardışıllığını aslında bozmamaktadır. Pekiyi derleyici yapı elemanları arasında neden boşluklar bırakabilmektedir? Bunun en önemli gerekçesi "hız kazancı" sağlamaktır. Hız kazancının nasıl sağlandığı işlemci ile RAM arasındaki bağlantı biçi ile açıklanabilir. Örneğin 32 bit işlemcilerde işlemci RAM ile bilgileri dörder byte'lık bloklar biçiminde transfer etmektedir. Yani işlemci bellekten 2 byte'lık bir bilgiyi 2 byte olarak okumaz. Onun içinde bulunduğu 4 byte'ın tamamını okuyup onun içerisindeki 2 byte'ı ayrıştırır. Bu sistemlerde RAM'in işlemciye göre görüntüsü şöyledir: xxxx xxxx xxxx xxxx .... xxxx xxxx xxxx xxxx Burada her satır dördün katlarındadır. Şimdi işlemcinin aşağıdaki gibi 4 byte'lık int bir nesneye erişmek istediğini düşünelim: xxxx xxxx xxxx xxxx .... xxxx xxii iixx xxxx Burada erişelecek int nesne 4'ün katında değildir. İşte işlemci iki bus ahareketi yaparak int nesnenin parçalarını ayrı ayrı 4 byte'ı okuyup kendi içinde birleştirmektedir. Tabii bu erişim makine komutuna yansımamaktadır. Makine komutunu uygulayan programcı yine tek bir komut olarak onu yazmıştır. Ancak bu komut kendi içerisinde iki kere RAM erişimiş yaptığı için nano düzeyde daha yavaş çalışacaktır. Pekiyi aynı int nesne aşağıdakiş gibi 4'ün katlarında bulunsaydı ne olurdu? xxxx xxxx xxxx xxxx .... xxxx xxxx iiii xxxx Burada işlemci tek bir RAM erişimi ile int nesneyi tek hamlede elde edebilecekti. Buradan çıkan basit sonuç şudur: 32 bit işlemcilerde 4 byte'lık nesnelere (örneğin int bir nesne) hızlı erişilebilmesi için bu nesnelerin 4'ün katlarında bulunması gerekir. Pekiyi 2 byte'lık (örneğin short türden) bir nesne için bu böyle bir hizalamaya gerek var mıdır? İşte 2 byte'lık nesnenin 4 byte'ın neresinden başladığına göre bu durum değişebilir. Örneğin: xxxx xxxx xxxx xxxx .... xxxx xxxx xxss xxxx Burada iki byte'lık short nesneye erişimde bir yavaşlık söz konusu olmayacaktır. Ancak örneğin: xxxx xxxx xxxx xxxx .... xxxx xxxx xxxs sxxx Burada 2 byte'lık short nesneye erişim diğer duruma göre nano düzeyde yavaş olacaktır. Pekiyi 1 byte'lık char gibi bir nesne için hizalama önemli midir? Bunun yanıtı 1 byte'lık nesneler için hizalamanın önemli olmadığıdır. Bu 1 byte'lık nesneler 4 byte'tın neresinde olursa olsun işlemci tarafından tek hamlede alınabilmektedir. Örneğin: cxxx xxxx xcxx xxxx .... xxxc xxxx xxcx xxxx Burada tüm 1 byte'lık char nesnelere aynı hızda erişilecektir. Pekiyi bu durumda derleyicinin nasıl bir strateji izlemesi anlamlı olur? Aslında 32 bit bir işlemci için izlenecek strateji basittir: Tüm nesnelerin kendi uzunluklarının katlarına yerleştirilmesi hızlı erişim için yeterli olmaktadır. Yani örneğin int bir nesne 4'ün katlarına, short bir nesne 2'nin katlarına char bir nesne 1'in katlarına yerleştirilmelidir. Pekiyi 8 byte'lık double türü kaçın katlarına yerleştirilmelidir? İşte matematik işlemci bağlantısı da dikkate alındığında bu nesnelrin de 8'in katlarında olması en iyi durumdur. Şimdi yapı elemanları arasında derleyicinin nedne ne nasıl boşluk bıraktığı artık anlaşılabilir. Pekiyi hizalama yalnızca yapı elemanları için mi önemlidir. Yerel değişkenler de benzer biçimde hizalanmakta mıdır? Derleyici aslında hizalamayı tüm nesneler için yapmaktadır. Ancak zaten C ve C++ standartları yerel değişkenlerin yerleşimi hakkında bir şey söylememektedir. Örneğin: void foo(void) { char a; int b; ... } Burada bu iki yerel değişkenin ardışıl olmasının bir garantisi yoktur. İlk bildirilen değişkenin stack'te düşük adreste olmasının da bir garantisi yoktur. Dolayısıyla genellikle bu durum programcıyı ilgilendirmemektedir. Ancak dizi elemanları her zaman ardışıldır. Yani bir elemanın bittiği yerde boşluk olmaksızın diğeri başlamıdır. Örneğin: short a[10]; Derleyici bu diziyi 2'nin katlarına yerleştirirse zaten dizinin tüm elemanları 2'nin katlarında olacaktır. Örneğin: int b[10]; Burada da derleyici b dizisini 4'ün katına yerleştirirse zaten dizinin tüm elemanları 4'ün katlarında olur. Pekiyi dinamik bellek fonksiyonlarında durum nasıldır? Çünkü biz malloc gibi bir fonksiyonun geri döndürdüğü değeri herhangi biçimde kullanabiliriz. Örneğin: struct SAMPLE { char a; int b; char c; int d; }; ... struct SAMPLE *ps; ps = malloc(sizeof(struct SAMPLE)); Burada malloc alan tahsis ederken zaten oranın herhangi bir türden olabileceği fikriyle uygun değerin katlarında olan bir adres verecektir. Örneğin burada malloc fonksiyonun 4'ün katlarında bir adres vermesi gerekir. Tabii malloc fonksiyonu bizim bu adresi nasıl kullanıcığımızı bilmemektedir. Bu nedenle en kötü olasılığa göre bir hizalama uygulayacaktır. Hizalama konusunda şu noktalara dikkat ediniz: >> Derleyiciler genel olarak beş farklı hizalama stratejisi izleyebilmektedir: -> 1 Byte Hizalama (Byte Alignment) -> 2 Byte Hizalama (Word Alignment) -> 4 Byte Hizalama (Double Word Alignment) -> 8 Byte Hizalama (Quad Word Alignemnet) -> 16 Byte Hizalama (Double Quad Word Alignment) N byte hizalama şu anlama gelmektedir: "Nesnenin uzunluğu ve N değerinin hangisi küçükse nesne o değerin katlarına hizalanır". Nesnenin tamamı da N'in katlarına hizalanmaktadır. Örneğin 4 byte hizalama söz konusu olsun. Bu durumda 1 byte'lık bir nesne (örneğin char bir nesne) 1'in katlarına, 2 byte'lık bir nesne (örneğin short bir nesne) 2'nin katlarına, 4 byte'lık bir nesne (örneğin int bir nesne) 4'ün katlarına ve 8 byte'lık bir nesne 4'ün katlarına yerleştirilir. Örneğin 8 byte (Quad Word alignment) söz konusu olsun. Bu durumda 1 byte'lık nesne 1'in katlarına, 2 byte'lık bir nesne 2'nin katlarına, dört byte'lık bir nesne 4'ün katlarına, 8 byte'lık bir nesne 8'in katlarına hizalanır. "1 byte hizalamanın aslında hizalama yapmamakla" aynı anlama geldiğine dikkat ediniz. Dolayısıyla programcı hizalamayı kaldıracaksa derleyiciyi 1 byte hizalama ayaralayabilir. >> Derleyicinin yaptığı hizalamalar bazen programcının işine gelmeyebilir. Yani programcı çeşitli gerekçelerle derleyicinin hizalama yapmasını istemeyebilir. Hizalamanın programcı tarafından kontrolü genellikle derleyicilerin sunduğu ek özelliklerle sağlanmaktadır. Microsoft derleyicilerinde hizalama komut satırından derleme yapılırken /ZpN (buradaki N 1, 2, 4, 8, 16 olabilir) seçeneği ile ayarlanmaktadır. Hizalama Visual Studio IDE'sinde proje seçeneklerinden "C-C++/Code Generation/Struct Member Alignment" combo box seçneğinden ayarlanabilmektedir. Eğer bu ayarlamalar yapılmazsa 32 bit derleme için default durum /Zp8 (yani quad word alignment), 64 bit derleme için /Zp16 biçimindedir. Microsoft derleyicileri bir yapının en büyük elemanı neyse yapının tamamını da o en büyük elemanı referans alarak hizalamaktadır. Yani bu durumda yapı nesnenin sonunda da yapının en büyük elemanının hizalama bilgisi kadar boşluk bulundurulacaktır. gcc ve clang derleyicilerinde de default durumda Microsoft'taki gibi 32 bit derleyiciler için 8 byte hizalama, 64 bit derleyiciler için 16 byte hizalama kullanılmaktadır. Hizalamayı değiştirmek için -fpack-struct=N komut satırı seçeneği kullanılmalıdır. Örneğin, gcc -fpack-struct=1 -o sample sample.c >> Hizalama konusunda default belirlemeye neden müdahale etmek isteriz? İşte bunun en tipik örneği bir dosyanın içerisindeki bilgilerin fread ya da read gibi bir fonksiyonla bir yapı nesnesinin içerisine okunmasının istendiği durumlardır. Örneğin bir BMP dosyasının başındaki 12 byte'a BMP başlığı denilmektedir. BMP başlığı şu biçimde içeriğe sahiptir: Uzunluk İçerik 2 byte Magic Number 4 byte Dosya uzunluğu 2 byte Reserved 2 byte Reserved 4 byte Image bilgilerinin bulunduğu offset Şimdi biz bu dosyanın başından 12 byte okuyarak okuduklarımızı bir yapı ile çakıştırmak isteyelim: struct BITMAP_HEADER { char magic[2]; /* 2 byte */ uint32_t size; /* 4 byte */ uint16_t reserved1; /* 2 byte */ uint16_t reserved2; /* 2 byte */ uint32_t dataloc; /* 4 byte */ }; İşte okudğumuz 12 byte bu elemanlarla default hizalama yüzünden çakışmayacaktır. Bu çakışmayı sağlamak için bizim hizalamayı byte hizalaması olarak ayarlamamız gerekir. Aşağıdaki örneği "test.bmp" isimli bir BMP dosyası oluşturup hep default hizalama ile hem de 1 byte hizalama ile deneyiniz. * Örnek 1, #include #include #include struct BITMAP_HEADER { char magic[2]; /* 2 byte */ uint32_t size; /* 4 byte */ uint16_t reserved1; /* 2 byte */ uint16_t reserved2; /* 2 byte */ uint32_t dataloc; /* 4 byte */ }; #pragma pack(4) int main(void) { FILE *f; struct BITMAP_HEADER bh; if ((f = fopen("test.bmp", "rb")) == NULL) { fprintf(stderr, "Cannot open file!..\n"); exit(EXIT_FAILURE); } fread(&bh, sizeof(struct BITMAP_HEADER), 1, f); printf("Magic: %c%c\n", bh.magic[0], bh.magic[1]); printf("Size: %u\n", bh.size); printf("Bitmap Data Locatiion: %u\n", bh.dataloc); fclose(f); return 0; } >> Pekiyi hizalama program kodunun içerisinde ayaralanabilir mi? İşte hizalamayı değiştirmek için hem Microsoft hem gcc hem de clang derleyicilerinde #pragma pack(N) direktifi kullanılabilmektedir. Bu direktif komut satırında belirtilen hizalama seçeneğine göre daha yüksek önceliklidir. (Yani hem komut satırında belirleme yapıp hem de £pragma pack ile belirleme yaparsak #pragma pack belirlemesi dikkate alınır.) * Örnek 1, #include #pragma pack(1) struct SAMPLE { char a; int b; char c; int d; }; int main(void) { struct SAMPLE s; printf("%zd\n", sizeof s); /* 10 */ return 0; } Buradaki #pragma pack direktifi sonraki #pragma pack direktifine kadar etkili olmaktadır. Böylece programcı isterse programın farklı yerlerinde farklı hizalamalar kullanabilir. * Örnek 1, #include #pragma pack(1) struct SAMPLE { char a; int b; char c; int d; }; #pragma pack(8) struct MAMPLE { char a; int b; char c; int d; }; int main(void) { struct SAMPLE s; struct MAMPLE m; printf("%zd\n", sizeof s); /* 10 */ printf("%zd\n", sizeof m); /* 16*/ return 0; } >> C11 ile birlikte bir nesnenin (bir yapının elemanı için de söz konusu olabilir) hizalaması _Alignas(N) belirleyicisi ile değiştirilebilmektedir. * Örnek 1, #include struct SAMPLE { int a; _Alignas(8) int b; }; int main(void) { struct SAMPLE s; printf("%zd\n", sizeof s); /* 16 */ return 0; } Burada yapının b elemanının önüne _Alignas(8) belirleyicisi getirilmiştir. Bu belirleyici aslında dördün katlarına yerleştirilecek int nesnesnin 8'in katlarına yerleştirilmesini sağlamaktadır. Ancak C11 standartlarına göre _Alignas(N) belirleyicisi ile yüksek bir hizalama gereksinimi düşük bir hizalamaya çevrielemez. Örneğin: struct SAMPLE { int a; _Alignas(1) int b; /* geçersiz! */ }; _Alignas ile belli nesnelerin ya da belli yapı elemanlarının hizalama gereksinimlerinin değiştirilebildiğine dikkat ediniz. Ayrıca C11 ile birlikte _Alignof(tür_ismi) isminde bir operatör de dile eklenmiştir. Bu operatör o anda o tür için derleyicinin uyguladığı hizalamayı bize vermektedir. Örneğin: #include int main(void) { printf("%zd\n", _Alignof(int)); /* 4 */ return 0; } _Alignas belirleyicisi bir tür ismiyle de kullanılabilmektedir. Örneğin: _Alignas(int) char c; _Alignas(tür_ismi) aslında _Alignas(_Alignof(tür_ismi)) anlamına gelmektedir. Yani bir _Alignas(int) dediğimizde int türünün hizalama gereksinimi neyse o sayıyı parantez içerisine yazmış gibi oluruz. >> Anımsanacağı derleyicilerin yapı elemanlarının arasında hizalama amacıyla bıraktığı boşluklara "padding" deniyordu. Aynı türden iki yapı nesnesi birbirine atandığında "padding" kısımlarının birbirine atanması konusunda bir garanti verilmemektedir. Örneğin: struct SAMPLE { char a; int b; }; ... struct SAMPLE x = {'a', 3}, y; x = y; Burada C standartları a elemanı ile b elemanı arasındaki muhtemelen 3 byte'lık padding alanının atama sırasında hedefe kopyalanaacağı konusunda bir garanti vermemektedir. Programcının bu padding alanlarını kullanmaya çalışması iyi bir teknik değildir.