"Enter"a basıp içeriğe geçin

Heap Fragmentation (Yığın Parçalanması)

Merhabalar. Bu yazım mikrodenetleyicilerde verimli ve stabil programlar üretmenin önündeki büyük engellerden heap parçalanması ile ilgili olacak. Bu konuda şuradaki yazı çok hoşuma gitti. Bu yüzden büyük oranda buradan faydalanacağım. Ayrıca linkte verilen buradaki Github reposuna da bir katkı yaptım. Şuradan katkı yaptığım haliyle görebilirsiniz.

O zaman hemen başlayalım. Mikrodenetleyicilerde kaynaklar genelde oldukça kısıtlıdır. İşlem birimi, bellek, giriş çıkış birimleri, vs. Hal böyle olunca tüm kaynakları en verimli şekilde nasıl kullanırız sorusu gündeme geliyor. Verimli kullanılması gereken en önemli kaynak da RAM bellektir. RAM belleği iyi yönetemediğimizde şu üç problemle karşılaşmamız olasıdır:

  • Bellekte çok fazla boş alan olmasına rağmen bellek ayırma işlemi başarısız oluyor.
  • Program saatlerce, günlerce hatta aylar boyunca iyi çalışıyor, fakat belli belirsiz bir anda çöküyor.
  • Program zaman geçtikçe yavaşlıyor.

Eğer yukarıda saydığım problemlerle karşılaşıyorsanız bu bir yığın parçalanma probleminiz olduğuna işaret etmektedir. Yazımızda, bunun ne anlama geldiğini ve nasıl düzeltebileceğimizi göreceğiz. Öncelikle heap alanının ne olduğundan başlayarak heap fragmentasyonunun nasıl oluştuğuna ve bu sorunu nasıl çözeceğimize bakalım.

Heap alanı nedir?

“Heap”, dinamik bellek tahsisinin gerçekleştiği RAM alanıdır. malloc() fonksiyonunu her çağırdığımızda, heap alanında bir bellek bloğu ayırmış oluyoruz.

The three areas of the RAM, with the heap highlighted
Kaynak: https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

Bununla birlikte new anahtar kelimesini her çağırdığımızda da heap alanından bir bellek bloğu ayırıyoruz. Yani, new operatörü arkaplanda malloc() fonksiyonunu çağırmaktadır. Benzer şekilde delete operatörü de free() fonksiyonunu çağırır. Bu nedenle burada malloc() ve free() için anlatacağımız herşey new ve delete operatörleri için de geçerlidir.

Programımız malloc() fonksiyonunu çağırmadan da heap alanından bellek ayırabilir. Örneğin, bir String nesnesi oluşturduğumuzda, String sınıfının yapıcı metodu, bu nesnenin tutacağı karakterleri depolamak için heap alanında bir miktar yer ayırır. Böylece bilerek veya bilmeyerek heap parçalanmasına kapı aralamış oluruz.

Heap parçalanması nedir?

Heap alanından malloc() veya new ile ayırdığımız bir bloğu serbest bırakmak için free() işlevini veya delete operatörünü çağırırız. Ancak, bunu yaptığımızda aynı zamanda bellekte kullanılmayan bir delik de yaratmış oluruz. Böylece bir süre sonra heap, Kars’ın meşhur bol delikli gravyer peynirine döner (kaynak makale İsviçre peyniri örneği vermişti).

Örneğin aşağıdaki gibi 30 baytlık bir heap alanımız olsun:

free() creates a hole in the heap
Kaynak: https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

Delikler de boş bellek olarak sayıldığından görünürde burada 20 baytlık boş yerimiz var. Ancak bu 20 baytlık alan ardışık olmadığından, 20 baytlık bellek alanı ayırmak istediğimizde yapamayacağız. Bu durum “yığın parçalanması” olarak adlandırılmaktadır ve RAM’in verimsiz kullanılmasının bir sonucudur. Bu da mikrodenetleyicinin kapasitesinin tam olarak kullanılmasını engeller.

Parçalanma ne zaman olur?

Bir bellek bloğunu serbest bıraktığımızı ve bundan dolayı yığında bir delik oluşturduğumuzu varsayalım.

A hole in the heap
Kaynak: https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

Bu her zaman sorun olur mu bakalım. Burada üç ihtimal var:

Birinci ihtimal: Aynı boyutta başka bir blok tahsis edebiliriz. Böylece yeni blok, eskisinin bıraktığı yeri alır. Delik de kalmaz.

A block of the same size fills the hole
Kaynak: https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

İkinci ihtimal: Varolan delikten daha küçük bir blok tahsis edebiliriz. Burada yeni blok deliğe sığar ama deliği doldurmaz. Delik kapanmasa da geride nispeten daha küçük bir delik kalır.

A smaller block fits in the hole
Kaynak: https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

Üçüncü ihtimal: Varolan delikten daha büyük bir blok tahsis edebiliriz. Yeni blok deliğe sığamaz, bu nedenle yığında daha fazla alan tahsis edilir. Geride büyük bir delik kalır.

A larger block doesn't fit in the hole
Kaynak: https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

Buraya kadar bir programın farklı boyuttaki blokları tahsis edip, serbest bırakarak yığın parçalanmasını nasıl arttırdığını gördük. Şimdi de bir örnekle bu durumu somutlaştıralım.

Örnek

Hava durumu tahminlerini bir web sunucusundan alan bir loop() fonksiyonumuz olduğunu düşünelim. Bu fonksiyon, öncelikle sunucudan gelen yanıtı büyük bir String‘e kaydeder; ardından da tarihi, şehri, sıcaklığı, nemi ve hava durumunu çeşitli boyutlardaki beş ayrı String‘e atar.

String serverResponse;
String date, city, temperature, humidity, description;
void loop() {
   serverResponse = downloadFromServer();
   date = extractDate(serverResponse);
   city = extractCity(serverResponse);
   temperature = extractTemperature(serverResponse);
   description = extractDescription(serverResponse);
}

loop() fonksiyonunun ilk döngüsünde sorun yok. String‘leri heap içinde tahsis eder, ancak onları serbest bırakmaz, bu nedenle parçalanma da olmaz.

Ardından, her yinelemede yeni String‘ler oluşturularak eskileri ile değiştirilir. Yeni String‘ler heap alanından yeni bloklar tahsis eder ve eski String‘ler eski blokları serbest bırakır.

İşte sorun da tam burada baş gösterir: Sunucunun verdiği her farklı yanıtta, blokların boyutları da değişir. Daha önce gördüğümüz gibi, değişen boyutlarda bellek tahsisatı yığında delikler oluşturarak parçalanmayı artırır.

Parçalanmayı ölçmek

Parçalanma için birkaç formal tanım var. Ancak, bu yazıda, aşağıdaki basit tanım kullanılacak:

Fragmentation's formula

Bu formülde bazı sayıları deneyelim. Örneğin 1KB boş RAM’imiz olduğunu varsayalım.

  • %0’da (parçalanma yok), tek seferde 1KB alan tahsis edebilirsiniz.
  • %25’te tek seferde 750B alan tahsis edebilirsiniz.
  • %50’de, tek seferde sadece 500B alan tahsis edebilirsiniz.
  • %75’te tek seferde sadece 250B alan ayırabilirsiniz.

%50 veya daha fazla bir değer yüksek kabul edilir ve göreceğimiz gibi programınızın çalışmasını ciddi şekilde engelleyebilir.

Parçalanma zamanla nasıl gelişir?

Artık formal bir tanımımız da olduğuna göre, parçalanmanın zaman içindeki gelişimini gösterecek bir program yazalım.

Parçalanmanın hesaplanması

Parçalanma yüzdesini hesaplamak için formülümüzü uygularız ve 100 ile çarparız:

float getFragmentation() {
  return 100 - (getLargestAvailableBlock() / getTotalAvailableMemory()) * 100.0;
}

Burada, tahmin edebileceğiniz üzere, getLargestAvailableBlock(), ayrılabilir en büyük bloğun boyutunu ve getTotalAvailableMemory() toplam boş belleği döndürür.

Bu iki işlevi yazmak, bu programın en zor kısmıdır çünkü bunlar platforma bağlıdır. Kod örneklerinde, asıl makalenin yazarı Benoît Blanchon tarafından eklenen AVR (Arduino UNO ve diğerleri) ve ESP8266 desteğinin yanında benim eklediğim ESP32 desteği de mevcut.

Bellekte delik açma

Parçalanma olgusunu üretmek için farklı boyutlarda birkaç String kullanabileceğimizi ve bunları tekrar tekrar değiştirebileceğimizi görmüştük.

Bunu yapmanın en kolay yolu, bir String dizisi oluşturmak ve dizinin her bir elemanını rastgele değerlerle değiştirmektir:

String strings[NUMBER_OF_STRINGS];

for (String &s : strings)
   s = generateRandomString();

Adından da anlaşılacağı gibi, createRandomString() fonksiyonu, her çağrıldığında uzunluğu değişen bir String döndürür:

String generateRandomString() {
  String result;
  int len = random(SMALLEST_STRING, LARGEST_STRING);
  while (len--) result += '?';
  return result;
}

Bu program kabaca yukarıda örneğini verdiğimiz hava tahmin programını simüle eder.

Sonuçlar

Uzunluğu 10 ile 50 karakter arasında değişen 20 String ile aşağıdaki grafik oluşturulmuş, test programı Arduino UNO üzerinde çalışmıştır.

A graph showing the evolution of heap fragmentation over time
Kaynak: https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

Grafikten görülebileceği üzere, program ilk başladığında, parçalanma neredeyse sıfıra yakın. Hemen ardından yaklaşık %70’te stabil olana kadar düzensiz bir şekilde artıyor.

Parçalanma üzerindeki etkisini görmek için ayarları değiştirmenizi tavsiye ederim. Özellikle, SMALLEST_STRING ve LARGEST_STRING eşit olduğunda, yani String’lerin tümü aynı boyuttaysa, parçalanma olmadığını göreceksiniz.

Heap parçalanması neden kötüdür?

Özensiz yazılan bir programda parçalanmanın nasıl arttığını gördük, şimdi yüksek parçalanma seviyesinin sonuçlarından bahsedelim.

Sonuç 1: Güvenilmez program

Tanım olarak, yüksek parçalanma seviyesi, çok fazla boş belleğiniz olduğu, ancak oluşturacağınız nesnelere yalnızca küçük blokları tahsis edebileceğiniz anlamına gelir. Programınız daha büyük bloklara ihtiyaç duyduğunda, gerekli bellek alanını alamayacak ve çalışmayı durduracaktır.

Sonuç 2: Düşük performans

Yüksek oranda parçalanmış bir yığın daha yavaştır çünkü bellek ayırıcının “en uygun” olarak adlandırılan en iyi boşluğu bulması daha fazla zaman alır.

Madem sorun bu kadar büyük, neden kimse bundan bahsetmiyor?

Yığın parçalanması, aslında çözülmüş bir problemdir, ancak mikrodenetleyiciler için gömülü yazılım geliştirenler için henüz çözülmüş değil. Peki diğer platformlarda sorun nasıl ele alınmış, nasıl çözümler uygulanmış bakalım.

1. Çözüm: Sanal bellek

Bilgisayarlarımızda çalışan programlar Sanal Bellek kullanır. Bellekte verinin yerini gösteren işaretçinin değeri, RAM’deki fiziksel bir konum değil, Sanal Bellek adresidir. CPU bu sanal adresi anında fiziksel adres karşılığına çevirir. Bu ayrım, RAM’in hiçbir şeyi hareket ettirmeden birleştirilmesine izin verir, ancak bu çözüm mikrodenetleyicilerde bulunmayan özel bir donanım gerektirir.

2. Çözüm: Optimize edilmiş heap ayırıcıları

İster standart ister bağlı kütüphanenin parçası olsun bilgisayar üzerinde çalışan C++ programlarının heap ayırıcıları mikrodenetleyicilerde bulunanlardan daha verimli çalışırlar.

Bunun için birtakım optimizasyon tekniklerini uygularlar. Burada bunun tüm detaylarına değinmeyeceğiz ancak en yaygın kullanılan optimizasyon, küçük blokları bölmelerde toplamaktır: 4 baytlık bloklar için bir bölme, 8 bayt için bir bölme vb. Bu teknik sayesinde küçük nesneler parçalanmaya katkıda bulunmaz ve parçalanmadan da etkilenmez.

3. Çözüm: Kısa string optimizasyonu

C++ standardı zorunlu kılmasa bile, std::string‘in tüm uygulamaları “Small String Optimization” veya SSO’yu destekler. std::string kısa dizeleri yerel olarak depolar ve heap alanı yalnızca uzun dizeler için kullanır.

4. Çözüm: Heap sıkıştırma

Yönetilen belleğe sahip dillerde çöp toplayıcı, küçük boşluklardan kurtulmak için bellek bloklarını hareket ettirir.

Bu tekniği C++’ta kullanamayız çünkü bir bloğu taşımak onun adresini değiştirir, bu yüzden bu bloğu gösteren tüm işaretçiler geçersiz olur.

5. Çözüm: Bellek havuzu

Bir program çok sayıda küçük blok tahsis etmek yerine sadece bir büyük blok tahsis edebilir ve onu ihtiyaç duyduğu şekilde bölebilir. Bu blok içinde, program herhangi bir tahsisat stratejisini kullanmakta serbesttir.

Örneğin, ArduinoJson kütüphanesi bu tekniği DynamicJsonBuffer ile uygular.

Bu tekniklerin hiçbiri Arduino programlarımız için geçerli değildir, bu da programımızı parçalanmayı azaltacak şekilde kodlamamız gerektiği anlamına gelir.

Peki heap parçalanmasını azaltmak için ne yapabilirim?

Strateji 1: Heap kullanmaktan kaçının (özellikle String kullanımından)

Birçok durumda dinamik bellek tahsisinden kaçınabiliriz. Yığındaki nesneleri tahsis etmek yerine, onları stack alana veya global alanlara yerleştiririz. Tasarım gereği, bu iki alan parçalı değildir.

Strateji 2: Kısa ömürlü nesneler kullanın

Kısa ömürlü nesnelerin yığın parçalanması üzerinde küçük bir etkisi vardır. Yığını aynı durumda bırakarak hızla gelip giderler.

Bununla birlikte, uzun ömürlü nesnelerin yığın parçalanması üzerinde önemli bir etkisi vardır. Odalarını ayırtırlar ve uzun bir süre burada kalırlar, heap alanı ortada hareketsiz bir blokla bırakırlar. Yani, global değişkenlerden kaçınmak için bir nedene ihtiyacınız varsa, o da budur.

Strateji 3: Sabit bellek alanı ayırın

Örneğin, 10 ile 100 karakter arasında olabilen bir string’imiz varsa, bunun için her zaman 100 karakter ayırabiliriz:

myString.reserve(100);

İlginç gelse de, gerekli olandan daha fazla bellek ayırmak, RAM’in daha verimli kullanılmasını sağlar.

Sonuç olarak

Makalede anlatılanları maddeler halinde özetleyecek olursak:

  1. Parçalanma, RAM’in verimsiz kullanımından kaynaklanır.
  2. Arduino programları parçalanmadan diğer programlardan daha fazla etkilenir.
  3. Parçalanmayı önlemek programcıların sorumluluğudur.

Hangi tür mikrodenetleyici olursa olsun program iyi tasarlanmadıysa bu durum yaşanabiliyor. Cihazda çökmeler yaşanıyor ve bir süre sonra kendini resetliyor. Programcı bu konuda tecrübeli değilse duruma anlam veremeyebiliyor. Bu çökmeler zaman zaman hatanın donanımsal olduğunu düşündürebiliyor. Bu çökmelere donanımsal veya çevresel nedenler aramadan önce yukarıda sayılan parçalanmayı önleyici tedbirleri gözden geçirmek gerekmektedir.

Kaynaklar

  • https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

İlk Yorumu Siz Yapın

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir