İçeriğe geç →

Python Çöp Toplayıcı (Garbage Collector)

Merhaba bu yazımda python çöp toplayıcı hakkında bir kaynak çevirisiyle sizlerleyim. Özet olarak giriş yapmak gerekirse çöp toplayıcılar yani GC mekanizması, oluşturulan değişken veri tiplerinin hafızadan silinmesini yöneten süreçtir. C/C++ gibi hafıza yönetim esnekliğinin üst düzey olduğu dillerin aksine python ihtiyaç duyulmayan değişkenleri hafızadan silme işlemini otomatik olarak gerçekleştirmektedir. GC mekanizmasını öğrenmek etkin programlar yazmanıza da yardımcı olacaktır.

Hafıza Yönetimi

Çoğu teknolojinin aksine python kontrolündeki hafıza bölgesini her durumda işletim sistemine geri vermez. Bunun yerine küçük nesneler(512 byte’dan küçük veya eşit) için kendi içerisinde gömülü nesne yöneticisi taşımaktadır ve söz konusu nesne yöneticisi daha sonraki hafıza ayırma işlemleri için hafıza yığınlarını hazır tutar. Yani alınan hafıza asla tamamen geri verilmez.

Buradan şu sonuç da çıkarılabilir: eğer bir python işlemi gittikçe daha çok RAM kullanıyorsa, bunun sebebi bellek sızıntısı olmak zorunda değildir.

Çöp Toplama Algoritmaları

Standart Cpython dağıtımında çöp toplayıcı iki bileşenden oluşmaktadır. Referans sayıcı ve çöp toplayıcı(GC Modülü).

Referans sayma algoritması oldukça etkin ve basittir fakat dairesel referansları algılayamamaktadır. Bundan dolayı python referans çevrimleri problemini çözen çöp toplama gibi yardımcı bir algoritmaya ihtiyaç duyar.

Referans sayma modülü temel bir modüldür ve etkisizleştirilemez, fakat çevrimsel çöp toplayıcı modülü seçeneğe bağlıdır ve manuel olarak da kullanılabilir.

Referans sayma

Referans sayma referans sayısı sıfıra düşen nesneleri hafızadan kaldıran basit bir tekniktir.

Pythondaki bütün değişkenler aslında bir nesneye işaretçidir yani nesnenin kendisi değildir. Örneğin; atama ifadesi ifadenin sağına sadece yeni bir referans eklemektedir.

Bütün nesneler -Integer bile- değerlerinin yanında referans sayısını da tutmaktadır ve bu referans sayısı yeni bir atama yapıldığında veya işaretçi kopyalandığında artar, işaretçi silindiğinde azalır.

Referans sayısını arttıran durumlar:

  • Atama operatörü
  • Metotlara parametre geçişi
  • Listeye eleman eklenmesi (Nesnenin referans sayısı artmaktadır)

Eğer referans sayısı sıfıra ulaşırsa, Cpython otomatik olarak nesne tipine özel hafızadan silme prosedürünü gerçekleştirir. Eğer söz konusu nesne başka nesnelere işaret eden referanslara sahipse işaret edilen nesnenin de referans sayısı değeri bir azaltılır. Böylelikle hafızadan silme prosedürü işaret edilen nesnelere de uygulanabilmektedir. Mesela bir liste(Python list()) silinirse elementlerinin her birinin referans sayıları da bir azaltılmaktadır.

Class’ların, fonksiyonların ve kod bloklarının dışında tanımlanan değişkenlere global değişkenler denir. Bu değişkenler çoğunlukla python işlemi son bulana kadar hafızadan silinmez. Bunun içindir ki global değişkenler tarafından işaret edilen değişkenlerin referans sayıları asla sıfıra düşmez.

Kod bloklarının içerisinde tanımlanan değişkenler (fonksiyonlar, değişkenler vb.) yerel kapsamdadır (local scope)(eğer söz konusu blok için yerel ise). Eğer python yorumlayıcısı bir bloktan çıkarsa içerisinde oluşturulmuş bütün değişkenleri yok eder. Bir değişkenin referans sayısını sys.getrefcount ile öğrenebilirsiniz

Aşağıda bir örnek görmektesiniz:

foo = []

# 2 referans, 1 from the foo değişkeninden ve 1 ise getrefcount'dan 
print(sys.getrefcount(foo))

def bar(a):
    # 4 referans
    # foo değişkeni, fonksiyon parametresi, getrefcount  
    # ve Python fonksiyon yığıtı
    print(sys.getrefcount(a))

bar(foo)
# 2 references, the function scope is destroyed
print(sys.getrefcount(foo))

CPython’ın referans sayma tekniğinin kullanmasının ana sebebi tarihi. Böyle bir tekniğin zayıf noktaları hakkında bir çok tartışma süregeliyor. Kimisi modern GC tekniklerini referans sayımından tamamen etkili bulmakta. Referans sayma tekniğinin iş parçacıklarını(thread) kitleme, dairesel referanslar ve hafıza/performans sorunları oluşturan bir çok açık noktası var.

Bu tekniğin asıl avantajı ise ihtiyaç duyulmayan nesnelerin hemen hafızadan silinmesi.

Jenerasyon Çöp Toplayıcı

Referans sayma tekniği varken neden bir çöp toplayıcıya ihtiyaç duymaktayız?

Maalesef klasik referans saymanın temel bir problemi var: referans sayma dairesel referansları tespit edemiyor. Dairesel referans (referans çevrimi) dediğimiz ise birbirlerine referansı olan iki nesnenin oluşturduğu durum aslında.

Şöyle bir örnek şemayla gösterilebilir:

Şemada da gördüğümüz gibi list nesnesi kendi kendini referans gösteriyor, bunun yanında, object 1 ve object 2 arasında da referans bağlantısı bulunmakta. Bu tip objelerin referans sayısı en az 1 olabilecektir.

Daha iyi anlaşılması için aşağıdaki örneği deneyebilirsiniz:

import gc

# ctypes ile hafızada erişimi olmayan nesnelere adresle erişiyoruz
class PyObject(ctypes.Structure):
    _fields_ = [("refcnt", ctypes.c_long)]


gc.disable()  # jenerasyon GC'yi kapat

lst = []
lst.append(lst)

# listenin adresini tut
lst_address = id(lst)

# lst referansını sil
del lst

object_1 = {}
object_2 = {}
object_1['obj2'] = object_2
object_2['obj1'] = object_1

obj_address = id(object_1)

# referansları sil
del object_1, object_2

# Çöp toplama işlemi yapmak için # işaretini kaldır 
# gc.collect()

# Referans sayılarını kontrol et
print(PyObject.from_address(obj_address).refcnt)
print(PyObject.from_address(lst_address).refcnt)

Yukarıdaki örnekte del ifadesi nesne referanslarını siler (yani referans sayısını bir azaltır). Python del ifadesinden sonra silinen nesneye Python kodu üzerinden erişemeyiz. Fakat nesne hâlâ hafızada yer kaplamakta, bunun sebebi nesneler birbirine referans gösteriyor ve her bir nesnenin referans sayısı 1 olarak kalıyor. objgraph modülüyle görsel olarak da inceleyebilirsiniz.

Bu sorunu çözmek için Python 1.5 ile beraber ek bir kod tespit algoritması tanıtıldı. Tanıtılan GC modülü bu tip bir probleme çözüm getirmek için tasarlandı.

Dairesel referanslar Python list, dictionary, class ve tuple vb. kapsayıcı nesnelerde görülen bir fenomendir(yani nesne içeren nesne). GC tuple haricindeki sabit nesneleri izlemez. Sabit nesnelerden oluşan tuple ve dictionary de izlenmeyen nesneler arasında yer alabilir. Böylelikle referans sayma tekniği dairesel olmayan bütün referans değişimlerini çözüme kavuşturur.

Jenerasyon Çöp Toplayıcı ne zaman çalışır?

Çöp toplayıcı referans sayma gibi gerçek zamanlı olarak çalışmaz, periyodik olarak işlem yapar. Çöp toplama komutlarının sayısını azaltmak için cpython çeşitli sezgisel yöntemler kullanır.

GC sınıflandırıcı nesneleri 3 jenerasyona ayırır. Her yeni oluşturulmuş nesne 1. jenerasyondan başlar. Eğer nesne çöp toplama aşamalarından sağ olarak çıkarsa bir üst jenerasyona geçiş yapar. Düşük jenerasyonlar yüksek jenerasyonlardan daha fazla çöp toplama işlemine tabi tutulur. Yeni nesnelerin yok olması ihtimali eski nesnelerin yok olması ihtimalinden fazla olduğu için, bu yöntem GC performansını arttırır ve beklemeleri düşürür.

GC işleminin ne zaman çalıştırılacağına karar vermek için her bir jenerasyonun kendine ait sayacı ve eşik değeri vardır. Sayaç, son GC işleminden sonraki, nesne için yer açma sayısı ve yer kapama sayısı arasındaki farkı tutar. Her kapsayıcı nesne için yer ayrıldığında, Cpython sayacın eşik değerini aşıp aşmadığına bakar. Eğer açmışsa Python toplama işlemini gerçekleştirir.

Eğer 2 veya daha fazla jenerasyon eşiği aşarsa GC en eskisini seçer. Tam da bu yüzden eski jenerasyonlar kendinden yeni olanları da toplar. Uzun yaşayan nesnelerin sebep olduğu performans düşüşünü azaltmak için 3. jenerasyonun GC için ek şartlar sağlaması gerekir.

Standart eşik değerleri sırasıyla (700, 10, 10) olarak ayarlanır fakat
gc.get_threshold komutuyla bunu her zaman kontrol edebilirsiniz.

Dairesel referansların tespiti

Dairesel referans tespitini bir kaç paragrafta anlatmak zor. Fakat temel olarak çöp toplayıcı, her bir kapsayıcı için geçici olarak referansların her birini siler. İterasyon tamamlandığında ise referans sayısı 2’den az olan bütün objeler Python tarafından ulaşılamıyor olarak değerlendirilir ve toplanır.

Çevrim bulma algoritmasını tamamen anlamak için Neil Schemenauer’ın Orijinal Önerisini ve Cpython kaynak kodundaki collect fonksiyonunu okumanızı öneriyoruz. Bunun yanında, Quora tartışması ve Çöp Toplayıcı Blog Yazısı da yararlı olacaktır.

Orijinal önerideki sonlandırıcı problemi Python 3.4’ten sonra düzeltildi. Bunun hakkında bilgi için: PEP 442

Performans Önerileri

Çevrimler gerçek hayatta her yerde başınıza gelebilir. Tipik olarak karşılaştığınız yerler graflar, bağlı listeler veya yapılar olabilir ve buralarda kodunuzdaki nesne ilişkilerine dikkat etmelisiniz. Eğer programınız ağır işlem yükü gerektiriyor ve düşük gecikmeyle işlem yapmak istiyorsanız, referans çevrimlerinden olabildiğince uzak durmalısınız.

Dairesel referanslardan uzak durmak için weakref modülünde implement edilen zayıf referansları kullanabilirsiniz. weakref.ref referansları klasik referanslar gibi referans sayısını arttırmaz ve nesne referansı silinmişse None döndürür.

Bazı durumlarda GC’yi kapatmak ve manuel olarak kullanmak da çözüm olacaktır. Otomatik çöp toplama gc.disable() komutuyla kapatılabilir. Manuel olarak çöp toplama işlemi başlatmak için gc.collect() komutunu kullanmalısınız.

Dairesel referansları bulma ve hata ayıklama

Dairesel referansları bulmak, üçüncü parti kütüphaneler kullandığınızda oldukça sinir bozucu olabilir. Standart gc modülü hata ayıklama için bir çok araç sağlamakta. Eğer hata ayıklama flaglerini DEBUG_SAVEALL olarak ayarlarsanız, erişilemeyen bütün nesneler gc.garbage listesine eklenecektir.

gc.set_debug(gc.DEBUG_SAVEALL)

print(gc.get_count())
lst = []
lst.append(lst)
list_id = id(lst)
del lst
gc.collect()
for item in gc.garbage:
    print(item)
    assert list_id == id(item)

Kodunuzdaki problem noktasını anladığınızda objgraph müdülüyle görsel olarak da inceleyebilirsiniz.

Sonuç

Çöp toplamanın büyük bir kısmı referans sayma algoritmasıyla yapılmaktadır ve bu algoritmanın parametrelerini pek de ayarlayamıyoruz. İmplementasyon yöntemlerinin farkında olmak önemli fakat potansiyel GC problemleri hakkında da endişelenmeye gerek duymayın.

İlk yazı olduğu için hataları görmezden geleceğinizi umuyorum.

Orijinal kaynak için: https://rushter.com/blog/python-garbage-collector/

Kategori: Gelişmiş Python

Tek Yorum

  1. Emre Emre

    Tebrikler güzel bir çalışma

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir