logo

Murat Demirten

header-nav
text

{ ubi dubium ibi libertas }

mobile-nav-trigger

Derleyici Optimizasyonları - Volatile Kullanımı

23 Mayıs 2016, Pazartesi

Günümüzde işlemcilerle birlikte derleyici uygulamaları da oldukça gelişmiş durumda. Derleyiciler tarafından hem ilgili işlemci mimarisi hem de genel çalışma biçimine yönelik pek çok optimizasyon yapılıyor. Optimizasyon sürecinden elde edilen kazanımlar o kadar önemli ki, herhangi bir sebeple optimizasyonlardan vazgeçmek seçenekler arasında yer almıyor. Bununla birlikte derleyici optimizasyonlarının bazı durumlarda yaratabileceği tehlikelerin programcılar tarafından yeterince bilinmediğini de görmekteyiz. Bu yazımızda bir örnek üzerinden konuya değinmeye ve C dilinde bir miktar gözden uzak kalmış volatile anahtar deyimi üzerinde durmaya çalışacağız.

Nasıl bir sorundan bahsediyoruz?

Aslında her şey derleyicilerin devasa boyutta yazılımlara dönüşmesi ile başladı dersek yanlış olmayacaktır. Örnek vermek gerekirse, gcc derleyicisinin güncel 6.X sürümü kaynak kodlarının açık hali, tam olarak 815 MB yer kaplamaktadır!

Derleme zamanında gcc tarafından yapılan bir çok olağanüstü analiz ve kod optimizasyonları bulunuyor. Ancak bazı özel durumlarda, optimizasyon sürecinde üretilen obje kodları hatalı olabilmektedir.

Konuya açıklık getirebilmek için aşağıdaki tipik bir busy-loop bekleme örneğini inceleyiniz. Ana uygulama control değişkeni 0 olduğu müddetçe bekliyor, ayrı bir thread ise kendi görevini tamamladıktan sonra control değerini 1'e eşitliyor:

#include <pthread.h>

unsigned int control = 0;

void * worker (void *arg)
{
        control = 1;
        return NULL;
}

int main (void)
{
        pthread_t thread;
        pthread_create(&thread, NULL, worker, NULL);

        while (control == 0);

        pthread_join(thread, 0);
        return 0;
}

Şimdi uygulamamızı gcc'nin güvenli optimizasyon seviyesi olan -O2 ile aşağıdaki gibi derleyip çalıştıralım:

$ gcc -O2 -o test test.c -lpthread
$ ./test

Uygulamayı çalıştırdığımızda hemen sonlanmasını bekliyoruz değil mi? Thread başlar başlamaz control değeri 1 olacak ve hem thread hem de ana uygulama hızlıca sona erecek, kaynak koda baktığımızda bu açıkça görülmekte. Bununla birlikte, uygulamayı çalıştırdığınızda beklediğiniz gibi davranmadığını gözlemleyeceksiniz.

Şimdi aynı uygulamayı gcc'nin -O0 parametresini kullanarak optimizasyonları devre dışı bırakmak suretiyle yeniden derleyelim ve çalıştıralım:

$ gcc -O0 -o test test.c -lpthread
$ ./test

Uygulama bu defa beklendiği gibi çalıştı ve sona erdi. Peki sorun neydi?

Problemi anlayabilmek için ilk örneğimizdeki derleme işlemini gcc'ye -save-temps parametresini eklereyek yeniden yapalım. Bu parametre verildiğinde derleme sürecindeki assembly çıktıları bulunduğunuz dizinde .s ile biten dosyalarda saklanacaktır:

$ gcc -O2 -save-temps -o test test.c -lpthread
$ cat test.s

Ana uygulamadaki while döngümüzün bulunduğu bölgede nasıl bir kod üretildiğine bakalım:

...
	call	pthread_create
	movl	control(%rip), %eax
	testl	%eax, %eax
	jne	.L6
.L5:
	jmp	.L5
...

Yukarıdaki ilgili assembly kod bloğunun C ile yazılmış versiyonu aşağıdaki gibi olurdu:

if (!control) {
    for (;;);
}

Buradaki problem, gcc'nin control değişkeninin global olarak tanımlanmış olduğunu, dolayısıyla kullanıldığı kontrol döngüsünün dışında da değerinin değişebileceğini gözardı etmesinden kaynaklanıyor. Eğer yerel bir değişken olsaydı, yapılan optimizasyon da doğru olacaktı.

Problemin basit çözümlerinden biri, muhtemelen sizin de şu an farkettiğiniz üzere, gcc'nin global olarak tanımlanmış değişkenler üzerinde optimizasyon işlemleri yapmasının engellenmesidir. Dolayısıyla bunun bir gcc bug'ı olduğunu düşünebilir ve mail listelerinde kendinize taraftar toplamaya çalışabilirsiniz ancak pek bulamazsınız. Zira optimizasyon işlemleri gibi binlerce farklı senaryoya sahip bir süreçte, rutin senaryolardan birinde hatalı sonuç üretiliyor diye akla ilk gelen çözümü uygulamak, başka senaryolarda çok daha kötü sonuçlara yol açabilir. Konunun uzmanları sizi bu konuda ikna edecektir.

Peki optimizasyonları tümden kapatmak yerine, bu sorunu nasıl çözebiliriz?

Volatile Kullanımı

Volatile, genellikle pek önemsenmeyen ve çoğu uygulama geliştirici tarafından hiç kullanılmayan bir deyimdir. Bir çok C/C++ kitabında volatile deyimine ya hiç yada çok az yer verilmekte, hatta kullanımına ilişkin bazen çeviri kaynaklı hatalara da rastlanmaktadır.

Volatile deyimi ile tanımlamış olduğunuz bir değişkenle derleyici uygulamaya şunu söylemiş olursunuz: hey derleyici! hani olur ya aksini düşünecek olursun, sakladığım değer çalışma zamanında hiç ummadığın şekilde değişebilir, önceden haber veriyorum, benim ipimle kuyuya inilmez!

Vaktiyle yayınlanmış C/C++ standartlarında volatile deyimi için, memory-mapped io yöntemi ile donanıma erişilen senaryolarda, bellek üzerinde haritalanmış bir değişken alanının değeri, uygulamanın kontrolünden bağımsız şekilde donanım tarafından değiştirilmesi sözkonusu olabileceğinden, bu tür işlemlerde kullanılması önerilmiştir. Fakat multi-threaded uygulamalarda kullanımına yönelik ek bir açıklamaya yer verilmemiştir.

Güncel standartlarda ise, volatile deyiminin derleyicilere yönelik avoid aggressive optimization involving the object anlamına geldiği yani bu şekilde tanımlanmış değişkenler üzerinde derleyiciler tarafından agresif optimizasyon işlemleri yapılmaması gerekliliği belirtilmektedir.

Bu doğrultuda, optimizasyonları devre dışı bırakmadan yukarıdaki problemin üstesinden gelebilmek için değişkenimizi volatile ile tanımlamamız yeterli olacaktır:

volatile unsigned int control = 0;

Bir diğer çözüm yöntemi ise memory barrier kullanmaktır. Ancak bu yöntem hem platformlar arası taşınabilir değildir hem de yazımızın kapsamı dışında kalmaktadır.

Mutex mekanizmaları kullanarak da çözüm üretmek mümkündür. Bu şekilde kodunuz büyük oranda platformlar arası taşınabilir hale de gelir ancak bu sefer de çalışma zamanında gereksiz bir maliyet üretmiş olursunuz.

Bazı kitaplarda veya internet üzerinde yer alan kaynaklarda volatile deyiminin tümüyle gereksiz olduğunu okudu iseniz, unutun gitsin! Yazımızda bahsetmeye çalıştığımız türden problemlerin kesin (ve standartlara uygun) çözümü, volatile deyimini kullanmaktan geçmektedir.

Özetle..

Eğer geliştirdiğiniz multi-threaded uygulama anlam veremediğiniz şekilde davranıyorsa, derleyici optimizasyon seviyelerini kontrol ediniz.

Optimizasyonları kapatarak derlediğiniz uygulama beklediğiniz gibi çalışıyorsa, yazımızda bahsettiğimiz problemin tipik bir örneğini yaşıyorsunuz demektir. Sorunlu yere odaklanıp volatile deyimini olması gerektiği gibi kullandığınızda optimizasyonlardan vazgeçmeden uygulamanız tekrar düzgün şekilde çalışır hale gelecektir. 

If you can read assembly language then everything is open source!

search
Sosyal Medya