Tuesday, January 16, 2007

Convert Constructor

Bircogunuz başlığa bakınca convert constructor mı o da ne diyordur herhalde, aslında convert constructor C++ da bildiğimiz içerisine parametre alan constructor a denmektedir. Peki sizce neden böyle bir ad konulma gereği duyulmuş bu parametre alan constructor tipine? sadece constructor denseydi parametre almayan constructordan nasıl ayrılacaktı... peki böyle bir ayrıma ihtiyaç var mı? peki copy constructor var bide o ne? etrafta bu kadar constructor dolanırken neden bir tanecik destructor var:) Bu soruların cevabını merak ediyorsanız bu yazımı okuyabilirsiniz, merak etmiyorsanız bir aşağıdaki yazıya alalım sizi:)

Şimdi siz aşağıda ki kodu bir inceleyin ve bu kodun çalışıp çalışmayacağını kendinize söyleyin ( Bu şekilde sorduğuma göre kodun çalışmasını bekliyorsunuzdur herhalde :) )

#include<iostream>
using namespace std;


class Test{

int var;

public:

Test( int v = 0 ) : var( v ) {}

int getVar() const { return this->var; }
};


void function( Test t1 ){

cout << t1.getVar()<< endl;

}


int main(){

function( 50 );

return 0;
}


Evet gelelim cevaba. Bu kodun çalışacağını söyleyenler malasef kaybeden tarafta yer almıycaklar:) evet çalışır bu kod. Peki;

gördüğünüz üzere function fonksyonunun giriş parametresi class Test tipinden tanımlanmıştır. Ozaman demek ki bizim function fonksiyonuna Test nesnesi vermemiz gerekir. Oysa biz napıyoruz açıkça bir int parametre veriyoruz...

İşte burada convert constructor dediğimiz olay devreye giriyor! 50 sayısı Test için yazdığımız constructora yada birden fazla varsa sırayla, verilerek Test nesnesine cast edilmeye yani 50 sayısı ile bir giriş parametresi oluşturulmaya çalışılıyor...

Burada argümanlar bire bir uyuştuğu için, Test sınıfının constructor ı bir tane int parametre alır, bu casting işi başarılı oluyor ve Yığında bir adet Test nesnesi oluşturuluyor. Bu yüzden bu constructorlar convert constructor olarak adlandırılmıştır.

Peki gelelim bu durumu engellemek için yapılabileceklere. Bazen böyle bir durumun gerçekleşmesini istemeyebiliriz. Fonksiyonun sadece bir nesneyi direk olarak vererek çalışmasını istediğimiz zamanlar olabilir. Bunu engellemek için ben 3 tane yol önerecem sizin aklına gelirse commentleyin lütfen:)

1) Fonksyionun giriş parametresini referance yapalım. Yani:
void function( Test& t1 ); şeklinde tanımlarsak, yığında nesne oluşturmak işe yaramayacaktır. Çünkü t1 nesnesi direk nesnenin kendisi olmalıdır ( kopyası değil! ) Bu yüzden bu şekilde bir fonksyon için function( 50 ); çağrısı hata verecektir.

2) Hiç convert constructor yazmayalım.. Bu mümkün bir çözüm olmasına rağmen akıldan uzaktır. 3. yöntem e bakın siz:)

3) explicit anahtar sözcüğünü kullanırsak derleyiciye function( 50 ); çağrısının mümkün olamayacağını söylemiş oluruz.
Peki nedir bu explicit? eğer bir convert constructor ı explicit olarak yazarsak, bu constructorın dışarıdan dolaylı olarak nesne üretmesini engellemiş oluruz. Yani function( 50 ); çağrısında ki 50 değeri bu constructor a giremez.

explicit Test( int v=0 ) : var( v ) {}


Evet işte convert constructlar hakkında kısaca söyleceğim şeyler bunlardı. convert constructorlardan başka bir de copy contructorlar var. copy constructor lar ise, bir nesneden bir nesne üretilceği zaman çalıştırılır. örneğin

Test t1;
Test t2 = t1; //copy constructor çalışır.

eğer sınıfınızın üyeleri arasında dinamik bellek ile ayrılmış değişkenler yoksa ( işaretçiler), sizin açıkça bir copy constructor yazmanız çoğu zaman gereksizdir ( gerekli olduğu durumlar da vardır uygulamanıza göre ). Çünkü derleyici otomatik olarak size bir adet constructor ve copy constructor sağlayacaktır. derleyicinin sağladığı copy constructor basitçe değişkenlerin değerlerini kopyalanandan kopyalayana atar. Eğer siz bir copy constructor ve constructor yazarsanız sınıfınıza, derleyici size default olarak bu fonksiyonları artık sağlamaz ve sizin yazdıklarınız geçerli olur. O yüzden eğer bilmeyerek constructor ı private alana koyarsanız, niye nesne oluşturamıyorum diye şaşırmayın:) her zaman nesne oluşturmak istiyorsanız constructorlarınız public alanda olmalı ki dışarıdan çağrılabilsin.

Constructorlarda ki bu çeşitliliğe karşılık destructor her zaman 1 tanedir. Bunun sebebi burada yapılacak işlemelerin genelde kaynakları geri iade etmek ve giriş çıkış işlemlerini tamamlamak gibi giriş parametresine bağlı olmayan değerlerdir. ve nesne yok edilirken kendiliğinden otomatik olarak çağrılmasıdır.

Bu yazımda sizlere convert constructorların bir özelliğinden bahsetmeye ve diğer constructorlar hakkında kısaca bilgi vermeye çalıştım. Bir sonraki yazımda görüşmek üzere...

Monday, January 15, 2007

Sabitler de artık değişmeye başladı...

yarın finale gireceğim için bu gece karışık bişeyler yazmamaya karar verdim, ve kısaca size C de programlamanın ne kadar çok programcıya bırakıldığını göstermeye kadar verdim. Güzel bir trick yaparak const tanımlanan değişkenleri değiştirelim ne derseniz....

Derleyici const olarak tanımlanmış bir değişkene direk olarak bir atama yaptığınız da şöyle bir hata verecektir:

#include<iostream>
using namespace std;

int main(){

const int thisIsConstant = 10;

thisIsConstant = 9;

cout << thisIsConstant << endl ;
return 0;
}

error C2166: l-value specifies const object

gördüğünüz gibi l-value ( left value ) bir const nesneye işaret ediyor dedi derleyicim. Bunun anlamı sen atama yapamazsın çünkü sol da bir const değişken var, nereye atama yapıon sen filan diyor :)

ama derleyiciye sezdirmeden bu const değişkeni değiştirebiliriz... Değiştirdikten sonra bir soru sorucam bilene benden bi çikolata:)

işte değiştiriyoruz const değişkeni:
#include<iostream>
using namespace std;

int main(){

int* ptr;
const int thisIsConstant = 10;

ptr = ( int* ) &thisIsConstant; //cast ediyoruz

*ptr = 9; //degistirdikkkk

cout << thisIsConstant << endl;
return 0;
}

Evet thisIsConstant değişkenin bellek gözünde 9 mu yazıyor dersiniz??? Benim sistemimde ekran çıktısı:
10
Press any key to continue

10 yazıyor! Bunun sebebi, derleyicinin object kodunu oluştururken thisIsConstant değişkeni gördüğü yerleri değeriyle değiştirmesidir, derleyici kodda bu değişkenin değerinin 10 olduğunu ve const tanımlandığını görünce nasolsa bu const değişken değişemez diyip değerini object koda gömdü. ( Systems programming ders1 : studying assembly language provides; deeper understanding how the compiler works! )
Bu arada merak ediyosanız, debug yaparak değişkenin içeriğine baktığınız da ( ben baktım :) ) içerisinde 9 yazdığını göreceksiniz. Ama object kodda değişkenin kullanıldığı yerde değişkenin adresi değil derlendiği anda ki içeriği yani 10 değeri olduğu için aslında
cout << thisIsConstant << endl; satırı
cout << 10 << endl;
satırı ile yer değiştirmiştir ;) Derleyicim VS 6.0, başka derleyiciler de derlediğiniz zaman bu optimize yapılmıyorsa ekran da 9 sayısını görürsünüz. Mesala ben bu denemeden sonra, cpp uzantılı dosyayı c olarak değiştirdim, tabi iostream i de stdio ile

#include<stdio.h>

int main(){

int a = 5;
int* ptr;
const int thisIsConstant = 10;

ptr = ( int* ) &thisIsConstant; //cast ediyoruz

*ptr = 9; //degistirdikkkk

printf("%d\n",thisIsConstant);

return 0;
}

işte ekran çıktısı:
9
Press any key to continue

Demek ki neymiş efendim, vs 6.0 da C ve C++ derlenirken kodun optimize edilişi farklıymış.

Aynı kodları, ssh ile itu ye bağlanip gcc ile derledim.
Sonuçlar:
C kodu için ekrana 9 değeri bastı,
C++ kodu için ekrana 10 değeri bastı.

Buradan genelleme yaparsak, C++ derleyicileri kodu optimize ederken, const değişkenlerin değerlerini object koda direk gömerler. C derleyicileri değişkenlerin adresilerini kullanmaya devam ederler. ( En azından VS 6.0 ve Gcc/G++ öle yapıo ).

Const değişkenler ve pointerlar hakkında söylenecek bayağı bir şey var aslında,
mesala;
const int* ptr ile int* const ptr nin farklı şeyler olduğunu biliyor muydunuz?

const int* ptr ; ile işaretçinin içeriğini değiştirebilirsiniz ama işaret ettiği verinin içeriği değişemez.

int x, y;
const int* ptr = &x ; //işaretçi oluşturulurken değer atanıyor OK
*ptr = 111; //işaretçinin gösterdiği yerdeki veri değişemez, HATA
ptr = &y; //işaretçi farklı bir değişkene işaret ettirildi, OK

int* const ptr; de ise işaretçinin işaret ettiği adres değişemez, ama içerdiği veri değişebilir:

int x,y;
int* const ptr = &x ; //işaretçi oluşturulurken değeri atandı, OK
*ptr = 111; //işaretçinin verisi değiştirilebilir OK
ptr = &y; //işaretçi const tanımlanmıştı, başka bir yere işaret edemez, HATA

Son olarakta const int* const ptr; ile tanımlanmış bir işaretçinin ne içerdiği veri, ne de gösterdiği yer değişebilir. ( yukarıda ki iki işaretçinin birleşimi gibi )

Bu yazımda da sizlerle birlikte const değişkenleri/işaretçileri tartışmış olduk.
Bir sonra ki yazımda C++ da convert constructor larla ilgili ilginç bir konuya değinmek istiyorum inşallah:) Görüşmek üzere...

Sunday, January 14, 2007

Struct ve Union da ne ola ki...

Neredeyse yazdığım programların hiç birinde union kullanmadım, hatta bu cümleyi okurken bazılarımız union da ne ola ki diyebilir.
struct C de yaygin olarak kullanilan, C++ da ise class olarak bilinen bir veri tipidir. C++ da ki class ile C de ki struct in farklari tabi ki vardır, orneğin default olarak class yapısında değişkenler private tanımlanır. yani;

class emreknlkCokClass{ //:))
int var;
};

emreknlkCokClass sınıfının içerisinde tanımlanmış var değişkeni private dır ve dışarıdan erişelemez. struct da ise varsayılan erişim tipi public olup dışardan değişkenlere erişilebilir.

Neyse benim bu akşam canım sıkıldı. O yüzden struct ve union ı kendime oyuncak seçtim, hadi biraz oynayalım ;)

1) Makineniz little endian mı big endian mı öğrenmek istermisiniz...

Little endian ve big endian kavramları nedir bilmeyenler için özetleyim: Verilerinizin bellekte nasıl saklandığına yönelik kavramlardır. Big endian kısmında verinin en yüksek anlamlı kısmı düşük adreste, düşük anlamlı kısmı yüksek adreste saklanır. Bu insanların da rahatça anlayabileceği bir yöntemdir. Mesala $4579 verisini 1000 no lu bellek gözüne yazmak isterseniz, 1000 nolu bellek gözünün içeriği 45 , 1001 no lu bellek gözünün içeriği 79 olacaktır.
Little endian da tahmin edeceğiniz üzere bunun tam tersi, 1000 nolu bellek gözünde 79, 1001 nolu bellek gözünde 45 yazacak...

işte union yapısı ile:
#include<stdio.h>

union WhichEndian{
int number; //4 byte
char byte; //1 byte
};


int main(){

union WhichEndian find;
int i;
char* ptr;

find.number = 0x12345678;

ptr = &find.byte;

for(i = 0; i < sizeof( int ) ; i++)
printf("Memory address: %p\tData: %x\n",ptr, (int)*(ptr++));

return 0;
}


ve benim makinemde işte size ekran çıktısı:

Memory address: 0012FF7C Data: 78
Memory address: 0012FF7D Data: 56
Memory address: 0012FF7E Data: 34
Memory address: 0012FF7F Data: 12
Press any key to continue


Gördüğünüz gibi little endian çıktı benim toşibam :))

Gelelim kodaa, union ne yapar, ne işe yarar kısaca bir özet geçeyim: union içerisinde ki bütün değişkenleri bellekte aynı offset değerinden başlayarak yerleştirir. Kodda bir integer ve bir char kullandık, bunların ikisininde başlangıç adresleri aynıdır. aslında bu kodda char byte değişkenine gerek yoktu, aslında yukardaki sonucu elde etmek için union kullanmak bile gereksizdi, ama burada dikkatinizi çekmek istediğim nokta:

ptr = &find.byte;

satırıdır. Burada ptr adresi byte değişkenin adresini içeriyorsa da aslında number değişkeni ile byte değişkeni aynı bellek gözünden başlayarak tanımlandıkları için, for döngüsü içerisinde number değişkeninin içeriğini sıra ile düzgün bastırabildik.

Eğer burada union değil de struct kullanacak olsaydık bu kod çalışmayacaktı. Çünkü byte değişkeni number ın bittiği yerden başlayacaktı...



2) Struct ile bit show

Diyelim ki elimiz de 8 tane led var. Bu ledlerin durumunu ( on/off ) kontrol etmek istiyoruz. Her bir led için 1 integer değer tutarsak 8*sizeof(int) kadar bellek bosuna gitmis olacak. Ben diyorum ki gelin her led için 1 bit kullanalım ve toplam 1 byte yer ayırarak bu işi bitirelim...

işte gerekli struct yapısı ve program kodu

#include<stdio.h>

#define OFF 0
#define ON 1

struct LedControl
{
unsigned char led1: 1;
unsigned char led2: 1;
unsigned char led3: 1;
unsigned char led4: 1;
unsigned char led5: 1;
unsigned char led6: 1;
unsigned char led7: 1;
unsigned char led8: 1;

};

int main(){

struct LedControl vecii; //LED leri vecii yaksin kapasin...

printf("Led Control %d byte yer kaplar..\n",sizeof(vecii));

vecii.led1 = OFF;

printf("1. led: %d\n",vecii.led1);

vecii.led1 = ON;

printf("1. led: %d\n",vecii.led1);

return 0;
}

Görüldüğü gibi struct içinde tanımlana led lerin her biri 1 bit yer kaplar. Bu bitlere erişim normal değişkenlere erişim gibidir. her değişken için bir bit ayırmak zorunluluğu yoktur.

Şimdi ekran çıktısını inceleyip struct için gerçekten 1 byte yer ayrıldığını görebiliriz:

Led Control 1 byte yer kaplar..
1. led: 0
1. led: 1
Press any key to continue


Peki nerelerde kullanılır bu yapı, herhalde enterprise edition lı program yazarken pek işimiz düşmez bu yapılara:) Ama sistem programlama da, driver yazmada , embeded sistemler için yazılım geliştirme de ciddi derece başvurulabilen bir metot olarak karşımıza çıkar.

Bu yazımda da size basitçe struct ve union ı tekrar hatırlatmak istedim. Basit şeyler olmasına karşın pek başvurulmadığı için zamanla fonksyonları unutulabiliyor. Diğer yazılarımda görüşmek üzere...

Saturday, January 13, 2007

Template C++

Bu postumda size belki biraz sıradışı ama ilginizi çekecek bir konuyu tanıtacağım. Yazıyı okumadan önce C++ ve template bilginizi tazeleyin derim, basit tutucam örneği ama yine de anlamayabilirsiniz:)

Bildiğiniz gibi template kavramı c++ nın güçlü ve önemli bir yanıdır. generic dediğimiz fonksyonlar sınıflar veritipleri oluşturmak için kullanırız genelde. örneğin bir absolute fonksyonu yazacaksak template kullandığımız zaman bütün veri tipleri için ( int float double ... ) ayrı ayrı yazmamıza gerek kalmaz. programcı bir kere yazar bu fonksyonu derleyici sağolsun çevirir....

Benim burada bahsetmek istediğim şey ise daha uçuk bişi:)

Önce gelin template i unutalım ve normal babadan kalma yöntemle bir fibonacci serisinin 20. terimini sadece 50 kere hesaplatalım ve ne kadar zaman alıo ölçelim:

#include < iostream >
using namespace std;

class Fibbooo{
public:
static int FindNth( int n ){
if( n == 0 )
return 0;
if( n == 1 )
return 1;
else
return FindNth( n - 1 ) + FindNth( n - 2);
}
};

int main(){

int result,i;

for( i = 0; i < 50; i++ )
result = Fibbooo::FindNth( 20 );

cout << result;
return 0;
}

Kodun çıktısında 20.terim: 6765 ve en önemlisi toplam süre:

Program Statistics ------------------
Command line at 2007 Jan 14 01:56:
"D:\ITU\c++\fib\Debug\fib"

Total time: 482,825 millisecond
Time outside of functions: 17,381 millisecond
Call depth: 21
Total functions: 1088
Total hits: 1095805
Function coverage: 26,2%
Overhead Calculated 3
Overhead Average 3

Module Statistics for fib.exe
-----------------------------
Time in module: 465,444 millisecond
Percent of time in module: 100,0%
Functions in module: 1088
Hits in module: 1095805
Module function coverage: 26,2%


Görüldüğü gibi modülün yürütülmesi 465,4 milisaniye, tüm program da 482 milisaniye de bitti.

Şimdi gelelim Template Meta Programming e, kodu daha sonra açıklayacağım. önce heycanlanmanız için kodu ve sonucu veriyim. işte kod:

#include <iostream>

template<n>
struct fibonacci
{
enum { result = fibonacci < n-1 >::result + fibonacci < n - 2 >::result};
};

template <>
struct fibonacci<0>
{
enum { result = 0 };
};



template <>
struct fibonacci<1>
{
enum { result = 1 };
};

int main() {

int result,i;

for( i = 0; i < 50; i++)
result = fibonacci<20>::result;

std::cout << result;

return 0;
}


ekran çıktısı bir önceki ile aynı. peki zamanlamalara göz atalım:
Program Statistics ------------------
Command line at 2007 Jan 14 01:59:
"D:\ITU\c++\fib\Debug\template"
Total time: 32,450 millisecond
Time outside of functions: 19,927 millisecond
Call depth: 15
Total functions: 1087
Total hits: 1255
Function coverage: 26,1%
Overhead Calculated 3
Overhead Average 3

Module Statistics for template.exe
----------------------------------
Time in module: 12,522 millisecond
Percent of time in module: 100,0%
Functions in module: 1087
Hits in module: 1255
Module function coverage: 26,1%

Evet, bazılarınız gözlerinize inanamamış olabilir, hatta benim bir yerlerde yannış yaptığımı da düşünebilir:) ama gördükleriniz doğru. program 32,5 milisaniyede ( ilk koda göre yaklaşık 15 kat daha hızlı) modül ise 12,5 milisaniyede ( yaklaşık 35 kat daha hızlı ) bir şekilde çalıştı.

Peki neden? Bunun sebebi aslında çok basit, siz execute tuşuna bastınız, yada exe ye çift tıkladınız, program loader programı belleğe aldı. Aslında tam bu noktada, yani loader programı belleğe yüklerken fibonacci serisinin 20. terimi hazırdı!!! evet hazırdı, program run time da hesaplamaz onu, compile ettiğiniz anda dosyayı 20.terim hesaplanır ve object kodunun içine yazılır. Nasıl mı? template ler compile aşamasında derleyici tarafından değiştirilerek yerine gelmesi gereken kod konulur. örneğin yukarıda bahsettiğim absolute fonksyonunu template ile yazıp, main içerisinde double için fonksyonu çağırırsanız, derlerken derleyici sizin bu fonksyonu double için çağırdığınızı görüp, template ifadelerini atıcak ve yerine sadece double için çalışan absolute fonk. nu yerleştirecektir.

Hal böyleyken, siz derlemeye başladığınız anda derleyici aşağıda ki ifadeyi görecek:
fibonacci<20>::result;
ve fibonacci<20>::result değerini hesaplamaya başlayacaktır. bunun için rekürsif olarak sürekli fibonacci < n > li struct ın değerini hesaplayacak, ne zaman ki n = 0 veya n = 1 olunca fibonacci<0> veya fibonacci<1> in değerlerini ( result değişkenlerini ) alabilecek, ozaman hesaplama bitmiş olacak...

Templateler ile programlama bir çok algoritmayı önemli derecede hızlandırabilmektedir. Uygulama da c++ programları bir run time da hesaplar yapan bölüm, bir de template meta programming dediğimiz bu şekilde kodlama ile yazılır ve compile time denilen yerde işlerin bir bölümü halledilmiş olunur.

Umarım bir miktar açıklayabilmişimdir TMP yi. daha fazlası için bknz google :) Sonraki yazılarımda görüşmek üzere...

Analiz: Nasıl kod yazabiliriz?

Anladım ki benim yazılarım genel de yaşadığım güncel olaylar üzerine çıkıyor. Şimdi size geçenler de itü bilgisayar grubumuz da konustugumuz bir konuyu hararetli bicimde aktaricam hazir olun:) Yaziyi sonuna kadar takip etmenizi oneririm sonunda şaşırabilirsiniz...
Ozgur Macit arkadasimin deyimiyle;
Efendim gecen gun grubumuza bir mail geldi bi arkadasimiz tarafindan. Mail de herkesin bildiği bir swap islemi C/C++ dilinde farkli bir sekilde yazilmisti. Buyrun inceleyelim:

#include

int main(void)
{
int a = 3, b = 5;
a = a^b;
b = a^b;
a = a^b;

printf("%d\n", a);
printf("%d\n", b);

return 0;
}

// ^ XOR islemi bu arada :)
Alinti: Onur Dolu

Goruldugu gibi 3 tane XOR islemi yapilarak a ve b değiskenlerinin yeri değiştirimiş. Xor işlemi matematikte/programlamada çok işe yarıyor daha sonra başka örnekler yazmayı düşünüyorum da biz gelelim esas konuya: Bu kodu bilinen temp değişkeni üzerinden eski değeri saklayıp, daha sonra a ve b değişkenlerinin değerini değiştiren swap koduna tercih edilebilr mi?
Bu arada yukarıda yazılmış olan kod aşağıda ki şekilde de kısaltılabiliyor:

int a = 3, b = 5;
a ^= b ^=a ^=b;

Grupta bulunan sevdiğim bir arkadaşım bu koda doğal olarak karşı çıktı. Nedenini ise şu şekilde açıkladı:

"Okuması da oldukça zor oluyor..

Kod bir kere yazılır ama defalarca okunur..

Senden sonra programda değişlik yapması gereken bir programcı aşağıdaki satırı anlıyamıyabileceğ inden veya daha kötüsü yanlış anlıyabileceğinden saatler veya günler kaydebilir.. Proje başarızlığa uğrayabilir."...

Yukarıda aynen alıntısını aldığım yazı hakkında herhalde herkes fikir birliğine varmıştır. Bu kodu okumak matematik bilgisi çok iyi olmayan bir programcı için imkansıza yakın olduğu gibi, matematik bilgisi olan bir programci için de zaman alıcı ve can sıkıcıdır.

Bunun üzerine ne yapılabilir diye düşündüm. Kod daha efektif duruyordu. Yukardaki probleme bir çözüm üretmeden önce bunu teyid etmem gerekti ( Hazırsanız birazdan kendinizi sonu gelmeyen bir kaosun içinde bulacaksınız!! ) :

Test1 ( yeni swap )
#include //stdio gereksiz bu kodda ama printf koymustum kalmis oyle:)

int main(void)
{
int a = 3, b = 5;
int i;

for( i = 0; i < 100001; i++ ){
a ^= b ^= a ^= b;
}

return 0;
}

Bu kodun çalışması sonucunda şu değerler elde edildi:
Program Statistics
------------------
Command line at 2007 Jan 13 11:28: "D:\ITU\c++\fib\Debug\swap"
Total time: 17,024 millisecond
Time outside of functions: 15,794 millisecond
Call depth: 1
Total functions: 1
Total hits: 1
Function coverage: 100,0%
Overhead Calculated 3
Overhead Average 3

Module Statistics for swap.exe
------------------------------
Time in module: 1,231 millisecond
Percent of time in module: 100,0%
Functions in module: 1
Hits in module: 1
Module function coverage: 100,0%

Func Func+Child Hit
Time % Time % Count Function
---------------------------------------------------------
1,231 100,0 1,231 100,0 1 _main (swap.obj)


Görüldüğü gibi döngü yaklaşık 1,231 milisaniye de tamamlandı.
Şimdi ikinci swap işlemini incelemeye geldi sıra. Bildiğmiz temp değişkeni üzerinden elde edilen swap....

Test1 ( klasik swap )
#include

int main(void)
{
int a = 3, b = 5;
int i, temp;

for( i = 0; i < 100001; i++ ){
temp = a;
a = b;
b = a;
}

return 0;
}

Evet heycanla bu kodu yazdım ve sonucu gerçekten çok merak ediyordum. Eğer tahminlerim doğruysa bu kodun çalışması en az 2 kat yavaş olacaktı çünkü fazladan bir temp değişkenine erişim vardı. Belleğe erişimin registerlar üzerinde xor işleminden çok daha fazla sürmesi gerekirdi. Şimdi sonuca bakalım:

Program Statistics
------------------
Command line at 2007 Jan 13 11:35: "D:\ITU\c++\fib\Debug\swap"
Total time: 2,228 millisecond
Time outside of functions: 1,538 millisecond
Call depth: 1
Total functions: 1
Total hits: 1
Function coverage: 100,0%
Overhead Calculated 3
Overhead Average 3

Module Statistics for swap.exe
------------------------------
Time in module: 0,690 millisecond
Percent of time in module: 100,0%
Functions in module: 1
Hits in module: 1
Module function coverage: 100,0%

Func Func+Child Hit
Time % Time % Count Function
---------------------------------------------------------
0,690 100,0 0,690 100,0 1 _main (swap.obj)

Evet herkes gördü sonucu, bende gördüm, gördüm sonra gözlerime inanamadım, inanamadım bir daha çalıştırdım, çalıştırdım yine aynı sonucu gördüm, gördüm gözlerime inanamadım...

Sonuç beni hem hayal kırıklığına uğratmıştı hem de heycanlandırmıştı. Bu sonucu beklemediğim açıktı. Demek ki ya ben bi yerlerde yanlış yaptım, ya da...
Evet ya da derleyici de bir sorun vardı:) Bu arada derleyici MSVS 6.0

Ve derleyiciden şüphe duydum! Daha önceki tecrübelerimden biliyordum ki derleyici assembly kodunu oluştururken arada geçici bellek atamaları yapıyordu. Kodu tekrar yazmalıydım ama bu sefer inline assembly kullanacaktım, kalp atışlarım hızlanmıştı.

Ama, evet ama önce söylediğim şeyin gerçek olduğunu size göstermeliydim. Test1 kodunun disassemblysine baktım işte size de göstereyim ( sadece for döngüsü içinde ki tek satır xor işlemleri) :
00401041 mov ecx,dword ptr [ebp-4] <- varan 1
00401044 xor ecx,dword ptr [ebp-8] <- varan 2
00401047 mov dword ptr [ebp-4],ecx <- varan 3
0040104A mov edx,dword ptr [ebp-8] ....
0040104D xor edx,dword ptr [ebp-4] ....
00401050 mov dword ptr [ebp-8],edx ....
00401053 mov eax,dword ptr [ebp-4] ....
00401056 xor eax,dword ptr [ebp-8] ....
00401059 mov dword ptr [ebp-4],eax <- varan 9

Evet kod gerçekten beni "in a horror" vaziyetine düşürmüştü! ben saf ve masum bir şekilde registerlar da xor işlemi yapiyordum. Ama derleyici her işlem sonucunu bir bellek gözünde saklıyordu!!! bu ise performansı inanılmaz bir şekilde düşürüyordu. Gerçekten vs 6.0 dan korkmaya başlamıştım...

ve test2 deki temp kullanarak gerçekleştirilen 3 atama işlemi ile swap yaptığım kodu disassembly ettim ve işte sonuc:
9: temp = a;
00401041 mov ecx,dword ptr [ebp-4]
00401044 mov dword ptr [ebp-10h],ecx
10: a = b;
00401047 mov edx,dword ptr [ebp-8]
0040104A mov dword ptr [ebp-4],edx
11: b = a;
0040104D mov eax,dword ptr [ebp-4]
00401050 mov dword ptr [ebp-8],eax

Bu kod kesinlikle belleğe daha az erişiyordu. Aslında tam da yapması beklenen işlemi yapıyordu. önce a değişkenini ecx e atıyor sonra ecx i temp bellek gözüne atıyordu ve diğer a=b ve b=a içinde aynılarını yapıyordu.

test2 nin kodu bu kadar iyi çalıştığı halde test1 neden bukadar kötüydü? Şeytan beni iyice kodu inline assembly de yazmaya itiyordu...

Ama önce son bir kez daha brain storming yapmak istedim. Düşündüm... registerlar üzerinde xor işlemi yapmak istiyordum. Ama yazdığım kod registerlar üzerinde değil bellek üzerinde, bellekten değerleri registerlar a çekerek, daha sonra registerlar da gerçekleştirdiği xor işlemlerini tekrar belleğe yazarak gerçekleştiriyordu. Evet yazarken farkediyodum herşeyi, bir emreknlk nasil bu kadar dikkatsizce davranabilirdi, bir emreknlk bu hatayi nasil yapabilirdi!

VS 6.0.. O kadar günahını aldığım derleyici. Aslında ben ne söylüyorsam onu yapıyordu! Onun hiç bir suçu yoktu tek suçlu vardı o da emreknlk! Kodu yazarken dikkat ederseniz değişkenler int a ve int b olarak tanımlamıştım. Yani ben dedim ki: Git esp den ( stack pointer) 8 çıkar! Değişkenleri yığında oluşturuyordum!!! Yani bellekte! sonra nasıl olurda bütün işlemleri registerlar üzerinde yapmasını bekleyebilirdim ki... Bellekten a ve b yi alıp tekrar a ve b yi belleğe yazması gerekiyor derleyicinin çünkü ben ona öyle yap demiştim!

Peki şimdi İnline assembly de yazmama gerek varmıydı? İnline assembly de yazarsam kodun hızlanacağını biliyordum. Çünkü asm kodunu ben gömecektim C nin içine. Ama C nin de bana sunduğu yetenekleri göz ardı edemezdim.

Evet register anahtar sözcüğü ( keyword ) değişkenleri bellekte değil, eğer mümkünse registerlar da oluşturuyordu. hemen
int a, b; ifadesini register int a, b; ifadesi olarak değiştirdim.

Sonucu merak ediyor olmalısınız. Sonuç değişmedi... Bir kaos içinde kalmıştım. Düşündüğüm hiç bir şey çalışmıyordu. Başarısızlık ard arda geliyordu. Belki de beni C ye bu kadar bağlayan şey buydu.

Araştırmaya başladım. niye çalışmadı niye sonuç aynıydı. tekrar disassebly ye baktım sonuçta yine a ve b değişkeni registerlarda değil yığında oluşturulmuştu! peki bu nasıl olabilirdi? Oysa ben açıkça registerlarda oluştur demiştim değişkenleri. Sonunda kendimi msdn de buldum. işte açıklamaları:

"The compiler does not accept user requests for register variables; instead, it makes its own register choices when global register-allocation optimization (/Oe option) is on. However, all other semantics associated with the register keyword are honored."

Evet vs 6.0 register anahtar sözcüğü için bizi sallamıyordu. belki de güvenmiyordu. Bu C nin esnekliğinin bir yerde kaybolması demekti. Düşündüm, kodu linux de derlesem çalışacaktı. Bundan şüphem yoktu. register anahtar sözcüğü ile test1 de yazdığımız xor lu kod hızlı çalışacaktı.

Ama bir sorun vardı! Birden adil davranmadığımı anladım. test2 içinde temp, a ve b değişkenlerini register anahtar sözcüğü ile tanımlasam daha o daha da hızlı çalışacaktı. 3 register da sadece 3 atama işlemi. işte hız buydu...

Peki son bir çırpınış, bu yazıma başlarken hatırlarsanız kodun okunabilirliğini sağlamak için yapılması gerekenleri anlatmak ama önce o kodun hızlı çalışması gerektiğini ispatlamak istiyordum. hızlı çalıştığını ispatlamadan nasıl olurda bu kodu kullanın diyebilirdim ki size, üstelik daha da okunabilirliği azaltırken.

Evet iki koda da adil olarak davrandığım sürece her yola başvurmalıydım. register fikri çok iyi bir fikir değildi. Büyük programlarda bu yöntem patlayabilirdi. Şimdi geriye malesef tek bir çözüm kaldı. Bu çözüm de başarısızlığa uğrarsa... düşünmek bile istemiyorum.

Inline assembly
Evet heycan dorukta. Bu sefer vs60 var bir de ben varım...

işte test 1, xor işlemleri ile yapılan swap işlemi ( Not: bu kod main içinde tanımlanmış ama swap fonksyonu içerisinen alınabilir, testleri yapmak için bu şekilde yazdım) :

#include

int main(void)
{
int a = 3, b = 5;
int i;

for( i = 0; i < 100001; i++ ){

__asm{
mov eax, [a]
mov ebx, [b]
xor eax, ebx
xor ebx, eax
xor eax, ebx
mov [a], eax
mov [b], ebx
}

}

return 0;
}

Gördüğünüz gibi asm kodu visual studio 6.0 içinde main içinde duruyor. şimdi bakalım sonuca:

Profile: Function timing, sorted by time
Date: Sat Jan 13 12:50:05 2007


Program Statistics
------------------
Command line at 2007 Jan 13 12:50: "D:\ITU\c++\fib\Debug\swap"
Total time: 1,702 millisecond
Time outside of functions: 1,112 millisecond
Call depth: 1
Total functions: 1
Total hits: 1
Function coverage: 100,0%
Overhead Calculated 3
Overhead Average 3

Module Statistics for swap.exe
------------------------------
Time in module: 0,589 millisecond
Percent of time in module: 100,0%
Functions in module: 1
Hits in module: 1
Module function coverage: 100,0%

Func Func+Child Hit
Time % Time % Count Function
---------------------------------------------------------
0,589 100,0 0,589 100,0 1 _main (swap.obj)

Evetttt, işte bu mudur? budur dedirten sonuç kendini gösterdi!!! 0.589 milisaniye şu ana kadar elde edilen en iyi süre. Şimdi biraz sevinç duyuyorum ama daha herşey bitmedi diğer kodu da bu şekilde yazıp sonuçları karşılaştırmam gerek.

işte temp kullanılarak gerçekleştirilen swap işleminin inline assembly li hali:
#include

int main(void)
{
int a = 3, b = 5;
int i,temp;

for( i = 0; i < 100001; i++ ){

__asm{
mov eax, [a]
mov [temp], eax ; a nin degeri temp de
mov ebx, [b]
mov [a], ebx ; b nin degeri a da
mov eax, [temp]
mov [b], eax ;temp in degeri b de
}

}

return 0;
}

ve sonuç geliyor:

Profile: Function timing, sorted by time
Date: Sat Jan 13 12:56:28 2007


Program Statistics
------------------
Command line at 2007 Jan 13 12:56: "D:\ITU\c++\fib\Debug\swap"
Total time: 21,834 millisecond
Time outside of functions: 21,197 millisecond
Call depth: 1
Total functions: 1
Total hits: 1
Function coverage: 100,0%
Overhead Calculated 3
Overhead Average 3

Module Statistics for swap.exe
------------------------------
Time in module: 0,637 millisecond
Percent of time in module: 100,0%
Functions in module: 1
Hits in module: 1
Module function coverage: 100,0%

Func Func+Child Hit
Time % Time % Count Function
---------------------------------------------------------
0,637 100,0 0,637 100,0 1 _main (swap.obj)


Evet en sonunda istediğim sonuç... 0.637 milisaniye . Biraz acımasız davrandım ikinci swap a kabul ediyorum ama en iyiye ulaşmak kolay olmuyor tabi :))

Dahası var...
inline assembly kullanarak temp i de atabilirz! işte 3. yol:

#include

int main(void)
{
int a = 3, b = 5;
int i;

for( i = 0; i < 100001; i++ ){

__asm{
mov eax, [a]
mov ebx, [b]
mov [a], ebx ; b nin degeri a da
mov [b], eax ; a nin degeri b de
}

}

return 0;
}

ve işte en kısası şimdi geliyorrrr :)
Profile: Function timing, sorted by time
Date: Sat Jan 13 13:00:59 2007


Program Statistics
------------------
Command line at 2007 Jan 13 13:00: "D:\ITU\c++\fib\Debug\swap"
Total time: 1,597 millisecond
Time outside of functions: 1,095 millisecond
Call depth: 1
Total functions: 1
Total hits: 1
Function coverage: 100,0%
Overhead Calculated 4
Overhead Average 4

Module Statistics for swap.exe
------------------------------
Time in module: 0,501 millisecond
Percent of time in module: 100,0%
Functions in module: 1
Hits in module: 1
Module function coverage: 100,0%

Func Func+Child Hit
Time % Time % Count Function
---------------------------------------------------------
0,501 100,0 0,501 100,0 1 _main (swap.obj)

Yani bu yazıdan çıkaracağımız pek çok sonuç var, ama en önemlisi vazgeçememek bana sorarsanız...

Sonuç olarak gelmek istediğim, asıl anlatmak istediğin noktaya gelemedim aslında, yazımın amacı kodu okunabilir yapmaktı. Bu konu hakkında bir cümleyle şunu söyleyebilirim ki: eğer elinizde daha efektif bir kod varsa, ve bu kodun okunabilirliği diğer algoritmalara göre daha kötüyse, o kodu, isminden görevinin anlaşılacağı bir fonksyon içerisine gömmek ( tek satır olsa dahi kodunuz) ve programınızı yazarken o koda ihtiyacınız olduğu yerde yazdığınız fonksyonu çağırmaktır. Fonskyonun ismini güzelce belirlerseniz, kodunuzu okuyanın fonksyon içerisinde yazdığınız kodu okuması gerekmeyecektir. Black box olara düşünülebilir fonksyonunuz.

Bir başka önemli nokta da, burada kodu zamanlama bakımından değerlendirdik. Günümüz bilgisayarlarında bellek sorunu olmamaktadır. ANCAK, gömülü sistemlerde, gerçek zaman sistemlerinde ciddi bellek kısıtlamaları vardır. Bu durumda 4 byte lık bir temp değişkenini bile kullanmak istemeyebilirsiniz. Bu durumda xor ile swap işlemi yapmak gerçekten işinize gelebilir. Kodu da büyük ihtimalle assembly ile yazacağınız için bu yöntem daha efektif olacaktır.

Yazı biraz uzadı ama ben yazarken gerçtekten keyif aldım, umarım sizde sıkılmamışsınızdır. Sonraki yazılarımda görüşmek dileğiyle...