31 Bellek Yönetimi

31 Bellek Yönetimi

Yinelemeli-iniş ayrıştırıcının(recursive descent parser) C yığınında  tahsis ettiği birkaç dizi hariç, Lua tüm veri yapılarını dinamik olarak tahsis eder. Tüm bu yapılar gerektiğinde büyür , küçülür veya ıskarta olur nihayetinde.

Lua belleğinin kullanımı üzerinde sıkı kontrol tutar. Bir Lua durumunu kapattığımızda Lua istisnasız tüm belleğini serbest bırakır. Dahası, Lua'daki tüm nesneler çöp toplama işlemine tabidir: sadece tablolar ve stringler değil aynı zamanda fonksiyonlar, threadlar ve modüller de (aslında tablodur bunlar). Büyük bir Lua modülü yükler ve sonra ona yapılan tüm başvuruları kaldırırsanız Lua akabinde bu modül tarafından kullanılan tüm belleği serbest bırakır.

Lua'nın bellek yönetim yöntemi çoğu uygulama için yeterlidir. Bununla birlikte bazı özel uygulamaların örneğin kısıtlı bellek ortamlarında çalışmanın gerektiği veya çöp toplama duraklamalarını en aza indirmenin gerektiği uyarlamalarına ihtiyaç olabilir. Lua bu uyarlamalara iki düzeyde imkan verir. Düşük seviyede, Lua tarafından kullanılan tahsis fonksiyonunda ayarlamalar çekebiliriz. Yüksek seviyede, çöp toplayıcısını kontrol eden bazı parametrelerde ayarlamalar çekebiliriz veya toplayıcı üzerinde doğrudan kontrol sahibi olabiliriz. Bu bölümde bu imkanları ele alacağız.

31.1 Tahsisat Fonksiyonu

Lua 5.1 çekirdeğinin belleğin nasıl tahsis edileceği ilgili herhangi varsayılanı yoktur. Bellek tahsisatı için ne malloc ne de realloc çağırır. Bunun yerine kullanıcının bir Lua durumu oluştururken sağlaması gereken tek bir tahsisat fonksiyonuyla tüm bellek tahsisat ve tasfiyesini yapar.

Durumları oluşturmak için kullandığımız luaL_newstate fonksiyonu varsayılan tahsis fonksiyonuyla Lua durumu oluşturan yardımcı bir fonksiyondur. Bu varsayılan tahsisat fonksiyonu normal uygulamalar için yeterince iyi olan(olabilecek) C standart kütüphanesinden standart malloc-relloc-free fonksiyonlarını kullanır . Tabi, temel lua_newstate ile kendi durumunuzu oluşturarak Lua tahsisatı üzerinde tam kontrol sahibi olmak oldukça kolaydır:

lua_State *lua_newstate (lua_Alloc f, void *ud);

Bu fonksiyon iki argüman alır: tahsisat fonksiyonu ve kullanıcı verisi. Bu yöntemle yaratılan bir durum tüm bellek  tahsisat ve tasfiyelerini f 'yi çağırarak yapar. (lua_State yapısının kendisi bile f ile tahsisatı yapılır.)

Tahsis fonksiyonunun lua_Alloc tipi şu şekilde tanımlıdır:

typedef void * (*lua_Alloc) (void *ud,
                             void *ptr,
                             size_t osize,
                             size_t nsize);

Daima ilk parametre lua_newstate 'e geçtiğimiz kullanıcı verisi; ikinci parametre tahsis ya da tasfiye edilen bloğun adresi; üçüncü parametre orijinal blok büyüklüğü; ve son parametre istenen blok büyüklüğüdür.

Lua, ptr NULL değilse öncelikli osize boyutlu tahsisat sağlar. Lua  sıfır boyutlu blokları NULL ile tanımlar: ptr, NULL ise (ve sadece o zaman) osize sıfırdır. Lua, her ikisi sıfır olsa bile osizenin nsize'den farklılaşmasını sağlamaz; Bu şartlar altında tahsis fonksiyonunun  ptr 'i (her ikisi sıfır olduğunda NULL olur)  basitçe döndürmesi mümkün.

Lua, tahsisat fonksiyonunun da sıfır büyüklüklü blokları NULL ile tanımlamasını bekler. nsize sıfır olduğunda tahsisat fonksiyonu ptr ile gösterilen bloğu serbest bırakmalı ve istenen büyüklüklü (sıfır) bloğa karşılık gelen NULL değerini döndürmeli. osize sıfır olduğunda (ve dolayısıyla ptr NULL) fonksiyon verilen boyutta bir blok ayırmalı ve döndürmeli; Verilen bloğu ayıramazsa NULL döndürmeli. (Hem osize hem de nsize sıfırsa önceki iki açıklama aynen geçerlidir: net sonuç tahsisat fonksiyonunun hiçbir şey yapmaması ve NULL döndürmesidir.) Son olarak, hem osize hem de nsize sıfır olmadığında tahsisat fonksiyonu, realloc gibi, bloğu yeniden konumlandırmalıdır. ve yeni adresi (orijinal ile aynı olsun veya olmasın) döndürür. Yine, hata durumunda NULL döndürmelidir. Lua, yeni boyut eskisine eşit veya daha küçük olması durumunda tahsisat fonksiyonunun kesinlikli hataya düşmeyeceğini varsayar. (Lua çöp toplama işlemi sırasında bazı yapıları daraltır ve oradaki hatalardan kurtaramaz.)

luaL_newstate tarafından kullanılan standart ayırma fonksiyonu aşağıdaki tanımlamaya sahiptir (doğrudan lauxlib.c dosyasından alıntıdır):

void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
  if (nsize == 0)
  {
    free(ptr);
    return NULL;
  }
  else
    return realloc(ptr, nsize);
}

free(NULL) hiçbir şey yapmaz ve realloc(NULL,size) malloc(size) eşdeğerdir. ANSI C standardı her iki davranışı da garanti eder.

lua_getallocf çağrısıyla Lua durumunun bellek ayırıcısını elde edebilirsiniz:

lua_Alloc lua_getallocf (lua_State *L, void **ud);

ud NULL değilse, fonksiyon,  bu ayırıcı için kullanıcı verisinin değeri ile *ud 'i set eder. lua_setallocf çağrısıyla bir Lua durumunun bellek ayırıcısını değiştirebilirsiniz:

void lua_setallocf (lua_State *L, lua_Alloc f, void *ud);

Herhangi bir yeni ayırıcının bir önceki tarafından ayrılan blokları serbest bırakmadan sorumlu olması gerektiğini unutmayın. ekseriye,  örneğin ayırmaları izlemek veya heap erişimlerini sekronize etmek için eskisinin bir sarmalayıcıdır yeni fonksiyon.

İçsel olarak, Lua yeniden kullanım için boş bellek bloklarını önbelleklemez(cache etmez). Tahsisat fonksiyonunun bunu yaptığını varsayar. İyi ayırıcılar yapar. Lua da bölünmeyi en aza indirmeye çalışmaz.Yine, çalışmalar, bölünmenin , program davranışından ziyade kötü tahsisat stratejilerinin sonucu olduğunu göstermektedir.

İyi uygulanan bir ayırıcıyı yenmek zordur, ancak bazen deneyebilirsiniz. Örneğin, Lua size serbest bıraktığı veya yeniden tahsis ettiği herhangi bir bloğun eski boyutunu verir; bu boyutu geleneksel free'den alamazsınız. Bu nedenle, özel bir ayırıcının, her blok için bellek yükünü azaltarak blok boyutu hakkında bilgi sahibi olması gerekmez.

Bellek tahsisini geliştirebileceğiniz başka bir durum çoklu iş parçacıklı(multithreading) sistemlerde bulunur. Bu tür sistemler genellikle global bir kaynak (bellek) kullandıkları için bellek ayırma fonksiyonları için sekronizasyon gerektirir. Bununla birlikte, bir Lua durumuna erişim de sekronize edilmelidir —ya da daha iyisi, bölüm 30'da lproc uygulamamızdaki gibi 1 iş parçacığıyla sınırlandırılmalıdır. Bu nedenle, her Lua durumu özel bir havuzdan bellek ayırırsa, ayırıcı ekstra senkronizasyon maliyetlerini önleyebilir.

31.2 Çöp Toplayıcı

ilk versiyondan 5.0 sürümüne kadar , Lua her zaman basit bir işaretle-ve-süpür çöp toplayıcı kullandı. Bu toplayıcı bazen “dünyayı-durduran” toplayıcı olarak adlandırılır. Bu, zaman zaman, Lua tüm çöp toplama döngüsünü gerçekleştirmek için ana programı yorumlamayı durdurduğu anlamına gelir. Her döngü dört aşamadan oluşur: işaretleme, arındırma, süpürme ve sonlandırma.

Lua, işaretleme aşamasını kök seti olarak başlatır, Lua'nın doğrudan erişimi olan nesneler: kayıt defteri (registry) ve ana iş parçacığı(thread). Canlı bir nesnede depolanan herhangi bir nesne program tarafından erişilebilir ve bu nedenle de canlı olarak işaretlenir. Tüm ulaşılabilir nesneler canlı olarak işaretlendiğinde işaretleme aşaması sona erer.

Süpürme aşamasına başlamadan önce, Lua, finalizörler ve zayıf tablolar ile ilgili arındırma aşamasını gerçekleştirir. İlk olarak, bir __gc metamethod ile işaretlenmemiş kullanıcı verisi bulmak için tüm kullanıcı verisini dolaşılır; bu kullanıcı verisi canlı olarak işaretlenir ve finalizasyon aşamasında kullanılmak üzere ayrı bir listeye konur. İkincisi, Lua zayıf tablolarını dolaşır ve anahtar veya değer olarak işaretlenmemiş tüm girdileri kaldırır.

Süpürme aşaması tüm Lua nesnelerini dolaşır. (Bu dolaşmaya imkan vermek için, Lua bir bağlı listede oluşturduğu tüm nesneleri tutar.) Bir nesne canlı olarak işaretlenmezse, Lua toplar. Aksi takdirde, Lua bir sonraki döngüye hazırlanırken işaretini temizler.

Son aşama, sonlandırma, ayıklama aşamasında ayrılmış kullanıcı verisi finalizerlerini çağırır. Bu hata işlemeyi basitleştirmek için diğer aşamalardan sonra yapılır. Yanlış bir finalizer bir hata yükseltebilir ancak çöp toplayıcısı, Lua'yı tutarsız bir durumda bırakma riski altında bir toplamanın diğer aşamalarında duramaz.
Bununla birlikte, bu son aşamada durursa sorun yoktur: bir sonraki döngü listede kalan kullanıcı verisinin finalizerlerini çağırır.

Sürüm 5.1 ile Lua gelişmiş bir toplayıcıya sahip. Bu yeni gelişmiş toplayıcı, eskiyle aynı adımları gerçekleştirir, ancak çalışırken dünyayı durdurmaya gerek yoktur. Bunun yerine, yorumlayıcı ile içiçe çalışır. yorumlayıcı sabit miktarda bellek ayırdığında, toplayıcı küçük bir adım çalıştırır. Bu, toplayıcı çalışırken, yorumlayıcı bir nesnenin ulaşılabilirliğini değiştirebileceği anlamına gelir. Toplayıcının doğruluğunu sağlamak için, yorumlayıcıdaki bazı işlemler tehlikeli değişiklikleri tespit eden ve ilgili nesnelerin işaretlerini düzelten engellere sahiptir.

Atomik işlemler

Çok fazla karmaşıklıktan kaçınmak için gelişmiş toplayıcı atomik olarak bazı işlemleri gerçekleştirir; yani, bu işlemleri gerçekleştirirken duramaz. Başka bir deyişle, Lua atomik bir operasyon sırasında hala “dünyayı durdurur". Atomik bir işlem tamamlamak için çok uzun sürerse, programınızın zamanlamasını engelleyebilir.

Ana atomik işlemler tablo dolaşma ve arındırma aşamasıdır. Tablo dolaşma atomikliği, bir tabloyu dolaşma sırasında toplayıcının asla durmadığı anlamına gelir. Bu, ancak programınızın gerçekten büyük bir tablosu varsa bir sorun olabilir. Bu tür bir sorununuz varsa, tabloyu daha küçük parçalara ayırmalısınız. (Çöp toplayıcı ile ilgili sorun yaşamasanız bile bu iyi bir fikir olabilir.) Tipik bir yeniden düzenleme, ilgili girdileri alt tablolara gruplayarak hiyerarşik olarak tabloyu bölümlemektir. Önemli olan, her girdinin boyutu değil, tablodaki girişlerin sayısıdır.

Arındırma fazının atomikliği, toplayıcının tüm kullanıcı verisini kesinleştirmesi ve tüm zayıf tabloları bir adımda arındırması için topladığını ima eder. Programınız çok miktarda kullanıcı verisi veya zayıf tablolarda çok sayıda girişe sahipse (birkaç büyük zayıf tabloda veya sayısız zayıf tabloda) bu bir sorun olabilir.

Her iki problem de pratikte ortaya çıkmıyor gibi görünüyor ancak emin olmak için yeni toplayıcıyla daha fazla deneyime ihtiyacımız var.

Çöp toplayıcı API


Lua,  çöp toplayıcı üzerinde bazı kontroller sağlamamıza imkan veren API sunuyor. C'den lua_gc kullanıyoruz:


int lua_gc (lua_State *L, int what, int data);

Lua'dan collectgarbage fonksiyonu kullanıyoz:

collectgarbage(what [, data])

Her ikisi de aynı işlevselliği sunar. what argümanı ne yapılacağını belirtir (C enum değer , Lua'da string ) . Seçenekler şunlar:

LUA_GCSTOP (“stop”):  “restart", "collect" veya "step" seçeneği  ile collectgarbage(veya lua_gc) 'a başka bir çağrıya kadar toplayıcıyı durdurur.

LUA_GCRESTART (“restart”):toplayıcıyı yeniden başlatır.

LUA_GCCOLLECT (“collect”):tam bir çöp toplama döngüsü gerçekleştirir böylece  tüm erişimi kalkmış nesneler toplanır ve sonuçlandırılır. Bu collectgarbage için varsayılan seçenektir.

LUA_GCSTEP (“step”):kısım kısım çöp toplama çalışmaları gerçekleştirilir. Çalışma miktarı, belirtilmemiş şekilde data'nın değeri ile verilir (daha büyük değerler daha fazla çalışma anlamına gelir).

LUA_GCCOUNT (“count”):mevcutta Lua tarafından kullanılan bellek kilobayt miktarını döndürür. miktar henüz toplanmamış ölü nesneleri içerir.

LUA_GCCOUNTB (mevcut değil):Lua tarafından mevcutta kullanılan bellek kilobayt miktarının kesirini döndürür. C'de, toplam bayt miktarı bir sonraki ifadeyle hesaplanabilir (bir int'e fitlendiği varsayarsak):

lua_gc(L, LUA_GCCOUNT, 0)*1024 + lua_gc(L, LUA_GCCOUNTB, 0)


lua'da, collectgarbage ("count") sonucu kayan noktalı bir sayıdır ve toplam bayt miktarı aşağıdaki gibi hesaplanabilir:

collectgarbage("count") * 1024

Yani, collectgarbage bu seçeneğe eşdeğer değildir.

LUA_GCSETPAUSE (“setpause”): toplayıcının pause parametresini ayarlar. Değer yüzde cinsinden data ile verilir: veri 100 olduğunda parametre 1  olarak ayarlanır(%100).

LUA_GCSETSTEPMUL (“setstepmul”): toplayıcının stepmul parametresini ayarlar. Değer, yüzde cinsinden data ile verilir.

İki parametre pause ve stepmul, toplayıcının karakteri üzerinde bazı kontrollere imkan verir. Her ikisi de hala deneysel çünkü hala bir programın genel performansını nasıl etkilediğine dair net bir donemiz yok.

pause parametresi toplayıcının bir toplamanın bitirilip yenisinin başlatılması arasında ne kadar süre beklenileceğini kontrol eder. Lua, bir toplamayı başlatmak için uyarlanabilir bir algoritma kullanır: Lua bir toplama sona erdiğinde m kilobayt kullandı dersek yeni bir toplama başlatmak için m * pause kilobayt kullanılıncaya kadar beklenir. Velhasıl %100 pause önceki bittiği anda yeni toplamayı başlatır. %200'lük pause toplayıcı başlamadan bellek kullanımını iki katına çıkarır; bu varsayılan değerdir. Daha düşük bellek kullanımı için daha fazla CPU zamanı feda etmeyi istiyorsanız daha düşük bir pause ayarlayabilirsiniz. Tipik olarak bu değeri %100 ile %300 arasında tutmalısınız.

stepmul parametresi toplayıcının ayrılan belleğin her kilobaytı için ne kadar çalışılacağını kontrol eder. Bu değer ne kadar yüksekse toplayıcı o kadar az yatar. % 100000000 gibi büyük bir değer toplayıcının soluksuz bir toplayıcı gibi çalışmasını saglar. Varsayılan değer% 200'dür. % 100'den daha düşük değerler toplayıcıyı o kadar yavaş yapar ki toplama hiç bitmeyebilir.

lua_gc için diğer seçenekler toplayıcı üzerinde size daha net kontrol sağlar.  Oyunlar bu tür kontrolün tipik müşterileridir. Örneğin bazı periyotlarda herhangi bir çöp toplama çalışması istemiyorsanız  collectgarbage("stop") çağrısıyla ile onu durdurabilir ve sonra collectgarbage("restart") ile yeniden başlatabilirsiniz. Periyodik boşta kalma evrelerine sahip olduğunuz sistemlerde toplayıcıyı durdurup boşta kalma süresinde collectgarbage("step", n) 'i çağırabilirsiniz. her boş bölümdeki çalışma miktarını ayarlamak için ya deneysel olarak n için uygun bir değer seçebilir ya da  n'e sıfır set ederek(küçük adım anlamında) bir döngü içinde periyot dolana kadar collectgarbage'i çağırabilirsiniz.

1 yorum: