C Dilinde Volatile Nedir
Volatile anahtar kelimesi derleyicinin volatile olarak tanımlanan alana (değişken, bellek alanı, struct alanı vb.) yapılan yazma ve okumaları optimize etmemesinin istendiğini belirtir. Optimizasyon kod boyutu ve hız üzerinde önemli bir etkiye sahip olduğu için hata ayıklama süreci dışında her zaman istenilen bir özelliktir. Optimizasyon tekniklerinden bir tanesi, derleyicinin değişkenler ve alanlara yapılan yazma ve okumalardan gereksiz olanlar için kod üretmemesi ve bu işlemleri uygun gördüğü gibi sonucu değiştirmeyecek şekilde yeniden sıralamasıdır (mesela pipeline'ı verimli kullanmak için). Örneğin bir değişkenin değeri bir register'ın içerisine okunduysa programda aynı değişkenin tekrar değerini okumayı gerektiren bir durumda okuma işlemi yeniden yapılmaz ve register'daki hazır değer kullanılır. Yazma durumu da derleyicinin uygun gördüğü bir ana kadar ertelenebilir veya tekrar aynı değerin yazıldığı tespit edilebiliyorsa yazma işlemi bir kez yapılacak şekilde kod üretebilir. Normal durumlarda bu hızlandırma tekniklerini istesek de aşağıda belirilen amaçlarla bazı alanların programda belirtildiği anlarda ve sırada yeniden okuma/yazma yapılmasına gerek duyarız.
Volatile Nerelerde Kullanılır
Gelenekse olarak volatile'ın kullanılmasını gerektiren durumları aşağıda listeledim. Neden volatile'ın modern mimarilerde yetersiz kaldığını öğrenmek için ise okumaya devam edin.
- Interrupt servis rutinleri içerisinde değiştirilen bir alan kesme dışındaki kodda okunuyorsa (ya da tam tersi) söz konusu alanın değeri programın çalışması süresince her an değişebilir. Derleyici bundan haberdar olmadığı için bir kere değeri okuyup/yazdıktan sonra tekrar aynı işlemi yapmayabilir. Bu tip alanların bu yüzden volatile olarak tanımlanması gerekir (ve haliyle atomik olarak okunup/yazılabilmesi gerekir).
- Unix/POSIX signal handlerlar interruptlara benzerler: keyfi bir anda akışı kesebilirler. Bu yüzden handler içinden çağırdığınız fonksiyonların en azından reenterant olması gerekir ve handler içerisinde blocking bir çağrı yapamazsınız. Interrupt servis rutinlerinde olduğu gibi handler içerisinde ve kodda paylaşılan alanların volatile olarak tanımlanması gerekir.
- Memory mapped IO için söz konusu hafıza alanının volatile olarak tanımlanması gerekir. Hafıza olarak görülen alan aslında donanım registerına veya örneğin donanım üzerindeki belleğe karşılık geldiği için okuma ve yazma her zaman programda istenildiği yerde ve istenen sayıda yapılmalıdır.
- Multithread programlamada doğrudan paylaşılan (örneğin mutex ile kritik bölge içerisinden erişilmeyip doğal haliyle atomik olarak olarak okunup yazılan) bir alanın da volatile olarak tanımlanması gerekir.
- C standart kütüphanesindeki setjmp/longjmp komutlarının kullandıldığı durumlarda setjmp çağrıldıktan sonra değiştirilen alanların longjmp çağrıldıktan sonra değerleri undefined olacaktır. Volatile olarak tanımlanmaları halinde son değerlerini korurlar.
Örnek
Aşağıdaki C kodunu "gcc -O -S volatile.c" komutu ile derleyeceğiz:
volatile int g_val = 42;
int main(void)
{
int f;
f = g_val;
f = g_val;
f = g_val;
return 0;
}
Kodda dikkat çeken nokta, global değişkeni okuyarak değeri f değişkenine yazıyoruz, ancak f değişkenindeki değer hiç kullanılmıyor. Volatile anahtar kelimesini silersek derleyicinin main fonksiyonu için ürettiği kod şu:
main:
mov r0, #0
bx lr
Yani sadece 0 değerini döndüren (return 0) ve başka bir iş yapmayan bir fonksiyon. Derleyici optimizasyon yaptı ve hiç bir işe yaramadığını tespit ettiği işlemler için komut üretmedi.
Şimdi de volatile anahtar kelimesi varken derleyelim:
main:
ldr r3, g_val
ldr r2, [r3, #0]
ldr r2, [r3, #0]
ldr r3, [r3, #0]
mov r0, #0
bx lr
Bu kez volatile ile optimizasyonu engellediğimiz için üç ldr komutu ile üç kez g_val'ın gösterdiği adresten okuma yapıldı.
Bu durumda okuma işlemleri hiç bir işe yaramıyor görünüyor ama g_val'ın bir donanım registerını gösterdiğini düşünelim. Donanım register'ları normal hafızadan farklı davranışlar gösterebilirler. Örneğin sadece okuma işlemi bile donanımın durumunda bir değişiklik yaratabilir.
Volatile'ın Modern Mimarilerdeki Yetersizliği
Yazdığımız programın modern çok işlemcili mimarilerde optimizasyon ve mimari nedeniyle tam olarak yazdığımız gibi çalışmamasının ana nedenleri şunlar. 1. Derleyici komutların sıralarını ve okuma yazma işlemlerini değiştirebilir 2. işlemci komutların çalışma sıralarını değiştirebilir 3. Bir işlemcinin hafızaya yazdığı değer önbelleğinde kaldığı için o anda farklı bir işlemci tarafından görülemez ve eski değer okunur (visibility). Volatile kelimesi sadece 1 numaralı optimizasyonu engeller ancak standart diğer durumlardan bahsetmez. gcc bu yüzden kendisinden istenilenden fazlasını yapmaz ve volatile kullanılsa da 2 ve 3 numaraları sorunlar devam eder.
Çözüm işlemcilerin "memory barrier" olarak isimlendirilen komutlarını kullanmaktır. Bu komutlar ön belleği boşaltır ve okuma yazma işlemlerini sıralar. Ancak C kodu içerisinde bunları çağırmanın doğrudan bir yolu yoktur ve bu güzel bir çözüm de olmaz (çok düşük seviyeli). Not: Java dilindeki volatile kelimesi Java 5.0'dan beri memory barrier komutlarını kullanır.
Uygulama seviyesinde sorunun çözümünü pthread kütüphanesi sağlar. Örneğin paylaşılan bir alana mutex ile koruduğumuz bir kritik bölge içinden eriştiğimizi düşünelim. Mutex'i kilitleyip açtığımız pthread çağrıları hem derleyici seviyesinde hem de işlemci seviyesinde (memory barrier komutları ile) gerekeni yapacaktır. Bu yüzden multithread programlama yaparken paylaşılan alanları volatile yapmak gerekmez ve yetmez. Bunun yerine alana erişimi gerekli pthread ya da kullanılan daha üst seviyeli kütüphane çağrıları ile kontrol etmeliyiz.
Memory barrier'lerle ilgili güzel bir belge Linux kernel dokümantasyonu içerisinde geliyor. Bu dokümana göre desteklenen mimariler içerisinde bu konuda baz alınan mimari agresif optimizasyonları nedeniyle Alpha mimarisiymiş.