DERLEME BEKLEMEYE SON!
Retro bilgisayarIntro ya da demo hazırlarken, özellikle ürünün sonlarına doğru programcının uyguladığı genel yordam, bitirmeye çalıştığı partı izlemek, küpler yanlış dönüyorsa kodu değiştirip derleyip bir daha izlemek, renkler olmadı gibiyse kodu değiştirip derleyip bir daha izlemek, gfx partı olması gerekenden daha erken girdiyse kodu değiştirip bir daha derleyip yine izlemektir… Ta ki ürün mükemmelleşene ya da daha büyük bir ihtimalle compo deadline gelene kadar bu böyle devam eder. Buradaki en sorunlu durum tahmin edeceğiniz gibi eliniz kolunuz bağlı beklediğiniz derleme sürecidir. Dergide bir yerlerde olması gereken seri üretim yazısını okuyup oradaki bir kısım yöntemleri uygulayanlar için, ufak değişiklikler sebebiyle tekrar derleme olayı tarihe karışacaktır. Bu yazı “değiştirme-derleme-izleme” sürecini tercih eden diğer okuyucuların hayatını oldukça kolaylaştırmasını umduğum birkaç programlama yönteminden bahsedecek.
Derleme Profili
Hedefimizi derleme süresini düşürmek olarak belirlediğimize göre önce derleme süresine etki eden olayların analizini yapalım. Bu sayede derleme işini en yavaşlatan işi bulup onu hızlandırmak, mümkünse ortadan kaldırmak istiyoruz.
Analize sondan başlayalım. Linking (birleştirme) işlemi okunup derlenmiş kaynak dosyalarınızdan hazırlanan obje dosyalarının (.obj uzantılı) içindeki kodların tüm uygulamayı oluşturacak şekilde tek dosyada toplanmasıdır. Bu işlem büyük çoğunlukla obje kodun assembly koduna birebir çevrilmesinden ibaret olduğu için tüm derleme olayının belki de en hızlı aşamasıdır. Bu yüzden bunu atlıyoruz.
Bir önceki aşama derleyicinizin okuyup belli iç yapılara çevirdiği kaynak dosyasının koda çevrilmesi işlemidir. Ancak bu da C/C++ gibi kısmen alt düzey diller için büyük çoğunlukla birebir çevirmedir. Bu da gerçekten çok hızlı geçen bir aşamadır.
Bir önceki ve ilk aşama ise kaynak dosyanızın içindeki yazıların ayrıştırılması (parse edilmesi) ve obje koduna çevrimin kolaylaşacağı basit yapılara haline getirilmesidir. Bu aşama if, while vs. ile yazdığınız dil yapılarının çözülmesi, girdiğiniz değişken, fonksiyon isimlerinin kontrol edilmesi gibi gerçek hayatta devlet memuru olarak bile yapmak istemeyeceğiniz bir sürü amele iş barındırır. C/C++ dillerinin esnekliği (serbest sayıda boşluk vs.) de göz önünde bulundurulursa, bu aşamada tüm derleme zamanının büyük kısmının harcandığı anlaşılabilir.
Headers are Evil!
Hemen celallenmeyin, bir okuyun :)
Peki, bu süreyi azaltmak için ne yapacağız? Kodlarımızı daha mı kısa tutalım? Gerek yok. Çok kısa değişken ve fonksiyon isimleri mi kullanalım? Asla! Bunlara kesinlikle ne gerek var ne de bunları yapmanız size hız kazandırır. Zaten biraz incelerseniz, kaynak kodunuzun ne kadar uzun olursa olsun bazılarının hep aynı sürede derlendiğini göreceksiniz. Derlerken alt alta sıralanan dosya isimleri, hepsi birer birer geliyor, her birinde yaklaşık 3 saniyelik bir bekleme…
Bu dosyaların tepesine bakın, ne görüyorsunuz? Tabii ki include satırları. Eklenen header (başlık) dosyalarını inceleyin. Onların eklediklerini inceleyin. Aralarında gl.h, windows.h gibi cicantik dosyalar göreceksiniz. İster inanın ister inanmayın ama derleme sürenizin yarısından fazlası bu gibi dosyaların parse edilmesi ile geçiyor. Hem de bunları kullanan her kaynak dosyası için birer kez! Derleyicinizin headerlarının olduğu dizine gidin ve windows.h’a bakın. Belki tüm projenizden çok daha uzun bir kod! Öyle değilse de düzinelerce başka header ekliyor olmalı.
Compiler Firewall Yöntemi
Bu gibi header dosyalarının işlenme sürelerinin azaltılması için zaten çok bilindik yöntemler mevcut. Bunların en temeli compiler firewall adı verilen ön derleme (preprocessing) komutları. Bunların amacı aynı kaynak kodunu derlerken bir header dosyasının birden çok kez işlenmemesi. Bunun için, ilk işleme ile bir makro tanımlayıp sadece bu makro tanımlı iken headerı işlemek bir yöntem, “#pragma once” satırı bir başka yöntem.
Ama bunlar windows.h gibi dosyalarda zaten var (bendekinde ilk yöntem kullanılıyor). Bu ancak kendi headerlarınız için kullanabileceğiniz (ve kullanmanız gereken) ve çok ufak çapta hızlandırmalar sağlayan bir yöntem. Zaten sorunumuz bu headerların tüm kaynak dosyalarınız için tekrar tekrar işlenmesi idi.
Bir kaynak dosyasının windows.h’ın yeniden işlememesinin tek bir yolu var: windows.h’ı eklememesi! Ama nasıl olur? Onu kullanarak bellek yönetimi yapıyoruz, zamanlama yapıyoruz, DirectX hatta ve hatta OpenGL kullanıyoruz. Bir çırpıda nasıl atarız? Bir çırpıda atamayacağız zaten. Bunun için DirectX, OpenGL, WinAPI gibi dev kütüphanelerin sadece kullanacağınız kısımlarını ayırıp bir bir wrap etmeniz gerekiyor.
SubAPI Yöntemi
OpenGL ile bir demo yaptığınızı ele alalım. Demonuzun her yerinde üçgenlerden oluşan objeler çiziyor, etrafta çizgiler uçuşturuyor, dikdörtgenler basarak resimler gösteriyorsunuz. Gayet iyi, gayet hoş. Lakin OpenGL, Bézier eğrileri de hesaplayabiliyor, ufak çapta görüntü işleme de yapabiliyor, isterseniz ekranda çizilen cisimlerin çetelesini de tutuyor. Çok daha kötüsü OpenGL, Windows altında windows.h’ı ekliyor!
Her kaynak dosyanızda gl.h eklemek, her kaynak dosyası için birkaç saniye beklemeniz demek. 30 kaynak dosyalı bir projede, 30 kaynak dosyanızın da kullandığı ve gl.h’ı çağıran bir headerı sürekli değiştirmenizin gerekmesi demek, yakında çıldıracaksınız demek. Bu yüzden OpenGL gibi dev bir kütüphanenin tüm proje çapında görülmemesi lazım.
Bunu engellemek için OpenGL’in kullandığımız kısımlarını projenin kalan kısmına sunan ufak çaplı bir API (application programming interface) hazırlayabilirsiniz. Bu API, örneğin C++’ta Renderer adı verilen bir namespace altındaki ya da C’de render öneki ile başlayan global fonksiyonlardan ibaret olabilir.
Renderer::DrawSprite2D /* ya da renderDrawSprite2D */ float left, float right, float top, float bottom
Renderer::Translate /* ya da renderTranslate */ float dx, float dy, float dz
Evet, glTranslate’in on bin farklı versiyonunu da kullanmayacaksınız. Kullanacağınız tek bir versiyonu (glTranslate3f) kullanan başka bir fonksiyon yazın. Her yere glBegin, glEnd koymak yerine, üçgen, dikdörtgen çizen bir iki fonksiyon hazırlayın. Anahtar nokta şu ki; bu fonksiyonların hepsini tek bir kaynak dosyasında tanımlayın ve gl.h’ı sadece buradan ekleyin. Artık demonun bütün kaynak dosyaları için derleme bekleme derdi kalmadı.
Aslında bu sayede derleme süresini azaltmanın yanında önemli bir iş daha başarmış olduk. OpenGL’i demonun kodlarından büyük ölçüde soyutladık. İleride DirectX’e ya da software bir motora, hatta textmode motora geçmek isterseniz işinizin ne kadar kolay olacağını bir düşünün. Tüm kodları baştan yazmak yerine sadece bu fonksiyonların bulunduğu kaynak dosyası hedefiniz olacak. Kısa sürede farklı bir çizim motoruyla demo yapmaya başlayabileceksiniz. Nested’tan Caret’e geçiş aynen böyle olmuştur. Hatta tecrübeli okuyucuların fark edeceği gibi, eski motorun kodunu yeni motorda çalıştırabilirsiniz, hem de Renderer.cpp haricinde hiçbir kaynağı derlemeden!
WinAPI’yi kullanırken de benzer bir yöntem izleyebilirsiniz. En rahatı tüm kütüphaneyi çevrelemek yerine; bellekle uğraşan, disk işlemleri ile uğraşan, pencere işlemleri ile uğraşan farklı farklı subAPI’ler hazırlamak. Bu şekilde kod yönetimi kolaylaşacaktır. Tüm projenin bellek ihtiyaçlarını karşılayan tek bir ara yüz hazırlamak, sık karşılaşılan bellek sorunlarının çözülmesine de yardım edecektir (her yerde new ve delete kullanmayla kıyaslandığında).
—Hepsi
çok iyi, hepsi çok güzel anesoğlan. Ancak bu API’ler bana yetmiyor. Deli gönlüm sınıflar oluşturmak, hiyerarşiler yaratmak istiyor. Lakin sınıf tanımlarını headerlarda yapıyoruz ve sen de takdir edersin ki bunların kullandığı tipleri gizli de olsa headerı ekleyene göstermek icap eder. Var mıdır bu derde de bir dermanın ha evlat?
Private Parts are Evil!
Çok güzel bir noktaya parmak bastınız. Global fonksiyonlar için sorunumuz yok ama bir sınıfın dışarıya gözükmeyen kısımlarının tanımını bile headerda yapmak zorundayız; çünkü sınıfı parçalayıp tanımlayamıyoruz. Haliyle bu kısımların ihtiyacı olan headerlar, sınıfın kullanıcısı tarafından ihtiyaç duyulmasa da eklenmek zorunda kalıyor.
Sorunumuzu bir örnekle anlatalım. Zamanlama için bir timer sınıfı hazırlıyoruz, bu sınıfa ait objeleri bir kronometre gibi başlatacağız, durduracağız, aritmetik işlemlerde gönlümüzce kullanacağız. Ama WinAPI’ye özgü yüksek hassaslıkta zamanlayıcıları kullanmazsak da ölürüz. Mevzu yaklaşık şöyle:
class Timer { public:
Timer() ;
~Timer() ;
void Start() ;
void Stop() ;
double GetTime() const ;
private:
bool counting_ ;
LARGE_INTEGER frequency_ ;
LARGE_INTEGER zero_ ;
Timerımız için Windows’un yüksek hassaslıklı zamanlayıcılarını kullanacağız. Bunun için işlemciye özgü bir saatin frekansını (frequency_) ve başla dediğimiz anın zamanını (zero_) tutmamız gerekiyor. GetTime dediğimizde de kronometre çalışıyorsa (counting_) timerımız o anki süreye bakıp, başlangıç süresini çıkarıp, frekansı da kullanarak saniye cinsinden kronometre değerini dönüyor. Kronometre durmuşsa da en son durulan uzaklığı zero_‘ya atmış olmamıza güvenip bu değeri veriyor.
(Not: Burada basit bir yapı olacak, kronometre her zaman sıfırdan başlayacak. Olayı kavradıktan sonra pause ve hız ayarı gibi özellikler ekleyebilirsiniz.)
Tabii bu timer'ı kullanmak isteyenlerin LARGE_INTEGER'ın ne olduğunu bilmeleri, bu yüzden windows.h'ı kullanmaları gerekiyor. Bir diğer seçenek de Timer.h'ın tepesinde bu işi kendimiz yaparak sınıf kullanıcılarını bu dertten kurtarmamız. Her iki durumda da istenmeyen header'lardan kurtulamıyoruz.
Çözüm yöntemlerimiz neler olabilir?
LARGE_INTEGER'ın 64 bitlik tamsayı olduğunu varsayıp yerine favori 64 bitlik tipimizi koymak ve gereken her yerde tip geçişi yapmak? Sakın! Asla diğer tiplerin yapıları hakkında varsayımlarda bulunmayın. LARGE_INTEGER'ın ya da onu oluşturan parçaların en ufak değişikliğinde kendinizi milyonlarca satır düzeltmeye çalışırken buluyor olabilirsiniz.
Hassas saatler yerine standart kütüphanenin clock'unu kullanmak? Belki. Hassaslıkta epey bir kaybınız olacak ama en azından daha kolay port edilebilir bir kod elde edeceksiniz. Eksi olarak ise, kullanıcı istemeden ctime'ı eklemiş olacaksınız ve bu mantıkla devam ederseniz platforma özgü çok iyi özelliklerden mahrum kalacaksınız.
Benim istediğim ise hiçbir header eklemeye gerek bırakmayan, üzerinde çalıştığı platformun en iyi çözümünü kullanan ve bunu yaparken kullanıcıya herhangi bir şey yansıtmayan bir yöntem.
Opak Pointer Yöntemi
Sorunumuz LARGE_INTEGER'ı kullanabilmek için tanımına ihtiyacımız olması idi. Bu yöntem soruna çok basit yaklaşım sergiliyor. Tanıma ihtiyacımız var çünkü Timer objeleri oluşturabilmek için Timer objelerinin boyunu bilmeliyiz. Timer objelerinin boyunu hesaplamak için de LARGE_INTEGER'ların boyunu bilmeliyiz. Ama anahtar şu ki, kullanıcı asla zero_ ya da frequency_'yi kullanmayacak. Dolayısıyla onun bunların boyunu başka bir amaçla bilmesine gerek yok. Timer bu elemanların boyunu kullanıcıya göstermeden onları nasıl kullanabilir?
Tabii ki pointer ile.
Bunları private alanda birer pointer ile tutsam, constructor'da oluşturup destructor'da silsem sorunum kalmayacak.
class Timer { public:
Timer() ;
~Timer() ;
Timer(Timer const &timer) ;
Timer &operator =(Timer const &timer) ;
void Start() ;
void Stop() ;
double GetTime() const ;
private:
bool counting_ ;
union LARGE_INTEGER ;
LARGE_INTEGER *frequency_ ;
LARGE_INTEGER *zero_ ;
(Not: Dinamik bellek kullanımına geçtiğimiz için varsayılan kopyalama işlemleri (element element kopyalama) isteğimizi karşılamayacak. Bu yüzden kendi kopyalama rutinlerimizi yazmamız gerekti.)
Timer sınıfını görüp Timer objeleri oluşturmak isteyen bir kaynak dosyasının artık windows.h’ı eklemesine gerek kalmadı. Çünkü gizli alandaki LARGE_INTEGER’ları kullanmayacak ve boylarını bilmesine gerek yok. Ve çünkü bütün pointerlar aynı boydadır! Tek yapmamız gereken bunun ne tür bir değişken olduğunu ön bildirim olarak derleyiciye vermek (struct).
Aslında daha rahat bir kullanım tüm private kısmı özel bir sınıf içine alıp tek bir pointer kullanmak.
class Timer { public:
Timer() ;
~Timer() ;
Timer(Timer const &timer) ;
Timer &operator =(Timer const &timer) ;
void Start() ;
void Stop() ;
double GetTime() const ;
private:
class Implementation ;
Implementation *pimpl_ ;
Timer.cpp’yi hazırlarken private datalara bu pointer üzerinden ulaşmamız gerekecek. Timer.cpp içinde önce bu yeni sınıfı tanımlamamız gerekiyor.
#include <windows.h>
#include "Timer.h"
class Timer::Implementation { friend class Timer ;
private:
bool counting_ ;
LARGE_INTEGER frequency_ ;
LARGE_INTEGER zero_ ;
Tüm datayı private yapıp sadece Timer sınıfına erişim vermek bu sınıfın normal kullanım için olmadığını, sadece Timer’ın arka plandaki işlerini yürütmek için gerektiğini rahatça anlatıyor (ufak bir comment satırının yerini tutamaz tabii).
Bu yönteme opak pointer dedik, çünkü kullanıcı bu pointerın arkasında neler döndüğünü göremiyor, haliyle de bilemiyor. (Zaten bilmemeli?!)
Timer constructor'ımızda bu implementasyon objemizi oluşturup destructor'da yok etmemiz gerekiyor.
Timer::Timer()
pimpl_(new Implementation)
// Başlangıçta durmuş halde.
pimpl_->counting_ = false ;
// İşlemci saati frekansını al.
pimpl_->QueryPerformanceFrequency(&pimpl_->frequency_) ;
// Baslangic degeri sifir.
pimpl_->zero_.QuadPart = 0ll ;
Timer::~Timer()
delete pimpl ;
Kopyalama işlerimiz de gerekli ama gayet kolaylar.
Timer::Timer(Timer const &timer)
pimpl_(new Implementation(*timer.pimpl_))
Timer &Timer::operator =(Timer const &timer)
*pimpl_ = *timer.pimpl_ ;
return *this ;
Kronometre işleri…
void Timer::Start()
if(!pimpl_->counting_)
// Başlama saatini al.
QueryPerformanceCounter(&pimpl_->zero_) ;
pimpl_->counting_ = true ;
return ;
void Timer::Stop()
if(pimpl_->counting_)
// Şimdiki saati al.
LARGE_INTEGER now ;
QueryPerformanceCounter(&now) ;
// Başlama saatinden çıkar.
pimpl_->zero_.QuadPart = now.QuadPart - pimpl_->zero_.QuadPart ;
pimpl_->counting_ = false ;
return ;
Son olarak da saatimizin okunması…
double Timer::GetTime() const
// Sayıyorsak if(pimpl_->counting_)
// Şimdiki saati al.
LARGE_INTEGER now ;
QueryPerformanceCounter(&now) ;
// Başlama saatinden çıkar.
now_.QuadPart = now.QuadPart - pimpl_->zero_.QuadPart ;
// Süreyi saniyeye çevirip dön.
return double(now.QuadPart) / double(pimpl_->frequency_) ;
// Saymıyorsak else
// Süreyi saniyeye çevirip dön.
return double(pimpl_->zero_.QuadPart) / double(pimpl_->frequency_) ;
Kapanış
Gördüğümüz derleme süresi azaltma yöntemlerini özetleyelim…
Compiler firewall yönteminde header'larımızın başına özel satırlar ekleyip bir kaynak dosyası için iki kez okunmamasını sağlıyoruz. Her ne kadar ufak kazançlar sağlasa da alışkanlık edinmemiz gereken bir yöntem.
SubAPI yönteminde büyük kütüphanelerin sadece kullandığımız kısımlarını sunan ufak kütüphaneler hazırlıyoruz. Bu sayede devasa header'lar sadece tek kaynak dosyası tarafından görülmüş oluyor. Bu yöntemle portability'mizi de artırmış oluyoruz. Ve çok daha iyisi, aynı işi yapan farklı alternatifler arasında geçişimiz çok rahat oluyor. Örneğin:
#ifdef OPENGL
/* Burada OpenGL kullanan SubAPI */
#endif
#ifdef DIRECTX9
/* Burada da DirectX kullanan */
#endif
Bu ayrımı kodumuzun çizim yapan her satırına da uygulayabilirdik. Ama bu kadar temiz olmayacaktı tabii ki.
Opak Pointer (ya da pimpl idiom) ile SubAPI'lerde elde ettiğimiz gücü sınıflarda buluyorduk. Bu yöntem derleme sürelerini oldukça düşürmesinin yanında, arayüz ve implementasyon ayrıntılarını istisnasız şekilde birbirinden ayırdığı için de projenin tamamının yönetimini kolaylaştıracaktır.
Örnek vermek gerekirse, ana işlemleri tamamen GPU üzerinde çalışan bir matris kütüphanesi hazırlamıştım. Ama bu kütüphaneyi kullanmak için hiçbir grafik kütüphanesi header'ı eklemeye gerek yoktu!
Bir başka örnek olarak da şu anda tam olarak API bağımsız (OpenGL, DirectX) bir doku sınıfı kullandığımı ve sonuçlardan çok memnun olduğumu söylemeliyim. Tam olarak'tan kastım ise, iki API arasındaki farklara bakmak için geçiş yaptığımda, sadece linkleme yapılıyor olması.
Datalara bir pointer ile ulaşmak ufak da olsa işleri yavaşlatacaktır ama asıl yapılan işle (rendering) kıyaslandığında bu eksiye devede kulak denebilir.
Bu yöntemler üzerinde kafa yormaktan, hatta uygulayıp sonuçlarını denemekten çekinmeyin. Sonuçlar karşısında şaşkına dönüp benimle mutluluğunuzu paylaşmak istediğinizde, ben atıma binmiş başka tutorial'lara doğru yol alıyor olacağım.