İngilizce Kaynaklar


Diğer




Bellek Yönetimi

Çeviren: Ali Çehreli
Tarih: 14 Temmuz 2009
İngilizcesi: Memory Management

Her ciddi program bellek ayırma ve geri verme işlemleriyle ilgilenmek zorundadır. Programların karmaşıklıkları, boyutları, ve hızları arttıkça bu işlemlerin önemi de artar. D'de bellek yönetimi için birçok seçenek bulunur.

D'de bellek ayırma konusunda 3 temel nesne türü vardır:

  1. Programın statik veri bölgesinde [static data segment] yer alan statik veriler
  2. CPU'nun program yığıtında [program stack] yer alan yığıt verileri
  3. Çöp toplamalı bellekte yer alan ve dinamik olarak ayrılan çöp toplamalı veriler

Bunların programda kullanılmaları ve bazı ileri bellek yönetimi teknikleri şunlardır:

Dizgilerde (ve Dizilerde) Yazınca Kopyalama [copy-on-write]

Bir fonksiyona bir dizinin gönderildiği bir örneği ele alalım; fonksiyon dizide duruma göre değişiklik yapıyor olsun ve sonuçta değişiklik yaptığı bu diziyi fonksiyondan döndürsün. Diziler fonksiyonlara değer olarak değil, referans olarak gönderildikleri için; dizi içeriğinin kime ait olduğu cevaplanması gereken önemli bir sorudur. Örneğin bir dizideki ASCII karakterleri büyük harfe çeviren bir fonksiyon şöyle yazılabilir:

char[] büyük_harfe_çevir(char[] s)
{
    int i;

    for (i = 0; i < s.length; i++)
    {
        char c = s[i];
        if ('a' <= c && c <= 'z')
            s[i] = c - (cast(char)'a' - 'A');
    }
    return s;
}

Dikkat edilirse, fonksiyonu çağıran taraftaki dizideki karakterlerin de değiştirildikleri görülür. Bunun istenen bir sonuç olmayabileceğinin yanı sıra, s[] yazılamayan bir bellek bölgesine bağlı bir dizi dilimi bile olabilir.

Bunun önüne geçmek için büyük_harfe_çevir() içinde s[]'nin bir kopyası alınabilir. Ancak bu da dizinin zaten bütünüyle büyük harflerden oluştuğu durumlarda tamamen gereksiz bir işlemdir.

Burada çözüm, yazınca kopyalama yöntemini kullanmaktır; dizi ancak gerçekten gerekiyorsa kopyalanır. Bazı dizgi işleme dillerinde bu işlem otomatiktir ama otomatik olmasının büyük bir bedeli de vardır: "abcdeF" gibi bir dizgi 5 kere kopyalanmak zorunda kalacaktır. Dolayısıyla, bu yöntemden en yüksek verimin alınabilmesi için bunun kod içinde açıkça yapılması gerekir. büyük_harfe_çevir() yazınca kopyalama yönteminden yararlanacak şekilde etkin olarak şöyle yazılabilir:

char[] büyük_harfe_çevir(char[] s)
{
    int değişti;
    int i;

    değişti = 0;
    for (i = 0; i < s.length; i++)
    {
        char c = s[i];
        if ('a' <= c && c <= 'z')
        {
            if (!değişti)
            {   char[] r = new char[s.length];
                r[] = s;
                s = r;
                değişti = 1;
            }
            s[i] = c - (cast(char)'a' - 'A');
        }
    }
    return s;
}

D'nin Phobos kütüphenesindeki dizi fonksiyonları yazınca kopyalama yönteminden yararlanırlar.

Gerçek Zamanlı [real time] Programlama

Gerçek zamanlı programlama, işlemlerin en fazla ne kadar gecikmeyle tamamlanacaklarının garanti edilmesidir. Bu gecikme, malloc/free'nin ve çöp toplamanın da aralarında bulunduğu çoğu bellek yönetimi işlemlerinde teorik olarak sınırsızdır. Bu durumda en güvenilir yöntem; gerekli belleğin önceden ayrılmasıdır. Böylece gecikmeye tahammülü olmayan işlem sırasında bellek ayrılmaz ve çöp toplayıcı çalışmayacağı için gecikme miktarı belirli bir sınırın altında kalır.

Kesintisiz Çalışma

Gerçek zamanlı programlamaya bağlı olarak, çöp toplayıcının belirsiz anlarda çalışmaya başlayarak programda duraksamalara neden olmasının önlenmesi gerekir. Örnek olarak bir savaş oyunu programını düşünebiliriz. Her ne kadar programda bir bozukluk olarak kabul edilmese de, oyunun rastgele duraksaması kullanıcıyı son derece rahatsız edecektir.

Duraksamaları ortadan kaldırmanın veya hiç olmazsa azaltmanın bazı yolları vardır:

Serbest Bellek Listeleri [free list]

Serbest bellek listeleri, sıklıkla ayrılıp tekrar geri verilen türlerde büyük hız kazancı sağlar. Aslında çok basit bir fikirdir: işi biten nesne geri verilmek yerine, bir serbest bellek listesine yerleştirilir. Bellek gerektiği zaman da öncelikle bu listeye bakılır.

class Foo
{
    static Foo serbestler_listesi;      // listenin başı

    static Foo ayır()
    {   Foo f;

        if (serbestler_listesi)
        {   f = serbestler_listesi;
            serbestler_listesi = f.sonraki;
        }
        else
            f = new Foo();
        return f;
    }

    static void geri_ver(Foo f)
    {
        f.sonraki = serbestler_listesi;
        serbestler_listesi = f;
    }

    Foo sonraki;         // bağlı liste için
    ...
}

void deneme()
{
    Foo f = Foo.ayır();
    ...
    Foo.geri_ver(f);
}

Bu tür listeler son derece hızlı olabilirler.

Referans Sayma

Burada fikir, nesne içinde bir sayaç barındırmaktır. Sayaç, nesneye yapılan her referansta arttırılır, sonlandırılan her referansta da azaltılır. Sayacın değeri sıfıra indiğinde artık nesne geri verilebilir demektir.

D'de referans saymaya yönelik olanaklar bulunmadığı için istendiğinde elle yapılması gerekir.

Win32 COM programlama, referans sayıları için AddRef() ve Release() fonksiyonlarını kullanır.

Nesne Ayırmanın Özel Olarak Tanımlanması

D'de her tür nesne için özel ayırma ve geri verme fonksiyonları tanımlanabilir. Nesneler normalde çöp toplamalı bellek bölgesinde yer alırlar; çöp toplayıcı çalışmaya karar verdiğinde de sonlanmış olan nesnelerin bellekleri otomatik olarak geri alınır. Ayırma ve geri verme işlemlerini bir tür için özel olarak tanımlamak için new ve delete bildirimleri kullanılır. Örneğin bu işi C kütüphanesindeki malloc ve free ile yapmak için:

import std.c.stdlib;
import std.outofmemory;
import std.gc;

class Foo
{
    new(size_t uzunluk)
    {
        void* p;

        p = std.c.stdlib.malloc(uzunluk);
        if (!p)
            throw new OutOfMemoryException();
        std.gc.addRange(p, p + uzunluk);
        return p;
    }

    delete(void* p)
    {
        if (p)
        {   std.gc.removeRange(p);
            std.c.stdlib.free(p);
        }
    }
}

new()'ün özel olarak tanımlanmasıyla ilgili önemli bazı bilgiler:

delete()'in özel olarak tanımlanmasıyla ilgili önemli bazı bilgiler:

Bir sınıf için özel bellek ayırma ve geri verme fonksiyonları yazıldığında, bellek sızıntısı ve geçersiz işaretçiler gibi sorunları önlemek için çok dikkatli olmak gerekir. Özellikle hata atma durumlarında bellek sızıntılarını önlemek için RAII yöntemini uygulamak çok önemlidir.

Bu özel fonksiyonlar struct ve union türleri için de tanımlanabilirler.

İşaretle ve Geri Ver [mark/release]

Bu yöntem program yığıtına benzer. Bellekte program yığıtı gibi kullanılacak bir yer ayrılır ve nesneler için gereken yer, bu bellekte ilerleyen bir işaretçinin değeri değiştirilerek ayrılır. Bazı noktalara işaretler koyulur ve yığıt işaretçisine tekrar o noktalardan birisi göstertilerek belleğin bir bölümü tümden geri verilmiş olur.

import std.c.stdlib;
import std.outofmemory;

class Foo
{
    static void[] bellek;
    static int indeks;
    static const int uzunluk = 100;

    static this()
    {   void *p;

        p = malloc(uzunluk);
        if (!p)
            throw new OutOfMemoryException;
        std.gc.addRange(p, p + uzunluk);
        bellek = p[0 .. uzunluk];
    }

    static ~this()
    {
        if (bellek.length)
        {
            std.gc.removeRange(bellek);
            free(bellek);
            bellek = null;
        }
    }

    new(size_t gereken_uzunluk)
    {   void *p;

        p = &bellek[indeks];
        indeks += gereken_uzunluk;
        if (indeks > bellek.length)
            throw new OutOfMemory;
        return p;
    }

    delete(void* p)
    {
        // [Çevirenin notu: Bu yöntemde Foo nesneleri teker
        // teker 'delete' ile geri verilmeyecekleri için,
        // 'delete'in yanlışlıkla çağrıldığı durumları
        // yakalamak için burada program sonlandırılıyor.]
        assert(0);
    }

    static int yerini_işaretle()
    {
        return indeks;
    }

    static void geri_ver(int i)
    {
        indeks = i;
    }
}

void test()
{
    int m = Foo.yerini_işaretle();
    Foo f1 = new Foo;          // yer ayır
    Foo f2 = new Foo;          // yer ayır
    ...
    Foo.geri_ver(m);           // f1 ve f2'yi birlikte geri ver
}

bellek[] için ayrılan yer std.gc.addRange(p, p + uzunluk); ile çöp toplayıcıya bildirildiği için, açıkça geri verilmesi gerekmez.

Kaynakları Bozucu Fonksiyonlarda Geri Verme: RAII (Resource Acquisition is Initialization)

RAII yöntemi, ayırma ve geri verme fonksiyonlarının açıkça kod içinde yapıldığı durumlarda bellek sızıntılarının önlenmesinde çok yararlıdır. Bu tür sınıflara scope niteliği eklemek bu konuda yarar sağlar.

Sınıf Nesnelerini Yığıtta Oluşturmak

Sınıf nesneleri normalde çöp toplamalı bellekte yer alırlar. Bazı durumlarda ise program yığıtında oluşturulurlar:

Çöp toplayıcının bu nesne için zaman geçirmesi gerekmeyeceği için bu etkin bir yöntemdir. Ama böyle nesneleri gösteren referansların fonksiyondan çıkıldığı anda geçersiz hale geldiklerine dikkat etmek gerekir.

class C { ... }

scope c = new C();      // c yığıttadır
scope c2 = new C(5);    // yığıttadır
scope c3 = new(5) C();  // özel ayırıcısı ile ayrılmıştır

Eğer sınıfın bir bozucusu varsa, kapsamdan (örneğin fonksiyondan) hangi nedenle olursa olsun çıkılırken, o bozucu fonksiyon çağrılır (hata atıldığında bile).

Yığıtta İlklenmemiş Diziler Oluşturmak

D'de diziler normalde otomatik olarak ilklenirler. Dolayısıyla şu kodda dizi[] elemanlarının ilk değerleri hiç kullanılmadıklarından (diziyi_doldur() tarafından üzerlerine yazıldığı varsayılırsa) bu kod daha hızlandırılabilir.

void foo()
{   byte[1024] dizi;

    diziyi_doldur(dizi);
    ...
}

Eğer programın bu kod yüzünden gerçekten hız kaybettiği kanıtlanmışsa, elemanların ilklenmelerinin istenmediği void ile belirtilir:

void foo()
{   byte[1024] dizi = void;

    diziyi_doldur(dizi);
    ...
}

Ancak, yığıttaki ilklenmemiş verilerle ilgili bazı uyarıları akılda tutmak gerekir:

Kesme Servisleri [Interrupt Service Routines] (ISR)

Çöp toplayıcı her çalıştığında yığıtlarını ve yazmaçlarını [register] taramak için bütün iş parçacıklarını [thread] durdurmak zorundadır. Eğer bir kesme servisi iş parçacığı durdurulursa, bütün program göçebilir.

Bu yüzden kesme servislerinin durdurulmamaları gereklidir. std.thread modülündeki fonksiyonlar tarafından başlatılan iş parçacıkları durdurulurlar. Ama C'nin _beginthread() ve benzeri fonksiyonları tarafından başlatılan iş parçacıkları durdurulmazlar, çünkü çöp toplayıcının bunların varlığından haberi yoktur.

Bunun başarıyla çalışabilmesi için: