Bellek yönetimi kavramı temel olarak belleğe veri ekleme(allocation) ve çıkarma(deallocation) işlemlerini içerir. Geliştirme sürecinde, genellikle Python’un otomatik bellek yönetimi özelliği sayesinde bu konuya ekstra bir çaba göstermemiz gerekmez. Ancak, bellek yönetimi konseptlerine hakim olmak ve çalışma mantığını anlamak, daha verimli ve optimize edilmiş kodlar yazmamıza olanak tanır.
Python’da bellek yönetimi özel bir heap bölgesinde gerçekleştirilir. Heap, nesnelerin depolandığı ve yönetildiği bir bellek bölgesidir. Bellek yöneticisi yeni bir obje için bellekten yer ayırabilir veya kullanılmayan nesneleri bellekten silebilir, bu sayede C, Rust gibi dillerde olduğu gibi manuel bir işleme ve ekstra bir efor harcamaya gerek kalmaz.
Python, bellekteki nesneleri takip etmek için reference counting adı verilen bir teknik kullanır. Python’daki her nesnenin kendisiyle ilişkili bir referans sayısı vardır ve bu sayı nesneye yeni bir referans oluşturulduğunda artırılır ve nesneye bir referans silindiğinde veya kapsam dışına çıktığında azaltılır. Bir nesnenin referans sayısı sıfıra ulaştığında, nesneye daha fazla referans olmadığı anlamına gelir ve nesne tarafından işgal edilen bellek serbest bırakılabilir. Reference counting konusu temelde basit ve etkili bir yöntem gibi gözüksede nesnelerin birbirine dairesel olarak referans verdiği durumları tespit edemez, bu dairesel referans durumlarında ise garbage collection tekniğini kullanır. (Dairesel referans, birbirini refere eden nesneler olarak düşünebiliriz. Örneğin, bir nesne içinde next_node olarak başka bir nesne yer alıyorsa, bu durumda dairesel referans oluşmuş olabilir. Diyelim ki A objesini oluşturduk ve next_node’a B objesini atadık. Aynı şekilde, B objesinin next_node’una da A objesini verdik. Bu durumda, A ve B nesneleri arasında bir döngü oluşur ve bu döngüyü dairesel referans olarak tanımlayabiliriz. Bu durumu, birbirini gösteren bir Spider-Man resmi gibi düşünebiliriz.)
Kod üzerinden dairesel referanse örnek verecek olursak:
import sys
class Node:
def __init__(self, data):
self.data = data
self.next = None
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.next = node2
node2.next = node3
node3.next = node1
current_node = node1
for i in range(5):
if current_node.next is None:
continue
print(f"current node -> {current_node} - data: {current_node.data} -> reference count {sys.getrefcount(current_node)}")
current_node = current_node.next
----------------------------------------------------------------
Output:
current node -> <__main__.Node object at 0x1020ffb50> - 1 -> reference count 4
current node -> <__main__.Node object at 0x102263520> - 2 -> reference count 4
current node -> <__main__.Node object at 0x1022634c0> - 3 -> reference count 4
current node -> <__main__.Node object at 0x1020ffb50> - 1 -> reference count 4
current node -> <__main__.Node object at 0x102263520> - 2 -> reference count 4
Garbage collector, mark and sweep tekniğini kullanarak görevini yerine getirir, bu teknikte nesne grafiğindeki kök noktadan(root, parent class) erişilen nesneler periyodik olarak yapılan kontrolle işaretlenir ve daha sonrasında kökten erişilmeyen işaretsiz nesneler bellekten kaldırılarak ilgili alanlar serbest bırakılır. Aslında, ilgili nesneler hemen bellekten kaldırılmaz; önce bu nesneler listeye eklenir ve boş bir alanda tutulur. Bu sayede, gelecekte bu nesneler için bir atama yapıldığında, oluşacak kaldırma ve yer ayırma maliyetini atlayarak tekrar aynı nesneyi kullanmak mümkün olur.)
Python otomatize olarak ilerleyen bu çöp toplama mekanizmasına manuel olarakta müdahale etmemize izin vermektedir. Bu sayede örneğin normalde periyodik olarak yapılan collection işini kod akışının istediğimiz yerinde çalışmasını sağlayabiliriz veya garbage collectionu manuel olarak başlatabiliriz ya da garbage collectionu devre dışı bırakabiliriz. (ayrtıntı için bkz: https://docs.python.org/3/library/gc.html)
Memory Leak (Bellek Sızıntısı)
Memory leak, bir programın çalışması sırasında kullanılan belleğin doğru şekilde serbest bırakılamaması veya yönetilememesi durumunda ortaya çıkan durumdur. Bu durum sonucunda programın bellek tüketimini artar ve yeterince verimli yönetilemez, dolayısıyla performansını olumsuz etkilenir ve kaynakların tükenmesine yol açabilir.
Bu durumla karşılaşabileceğimiz bir kaç örnek senaryo verecek olursak;
- Bellek alanlarının kullanımdan kaldırmaması veya serbest bırakmaması,
- Dinamik bellek tahsis eden işlevler çağırırken, geri dönüş değerlerini düzgün bir şekilde ele almaması veya kaydetmemesi sonucu. (örneğin bir while loop ile bir işlevi/fonksiyonu çağırarak sonucu doğru şekilde ele almazsak veya aynı iterasyon adımında tahsis edilen bellek alanını serbest bırakmazsak)
- Döngüsel referansların oluşması durumunda
gibi durumlarda memory leak oluşabilir, genellikle monitoring ve profiling (örn: cProfile) yöntemleriyle bu gibi durumları tespit edip optimize edebiliriz. Memory leakin önüne geçebilmek için python tarafında ve genel olarak yapabileceğimiz bir kaç temel optimizasyon yer alır bunlar:
1- Veri Yapılarının Doğru Şekilde Kullanılması
En temel ve genellikle en iyi optimizasyon yöntemi veri yapılarını doğru kullanmaktır. Veriyi depolamak için doğru veri yapısının seçimi günün sonunda belleğinde en verimli şekilde kullanmamızı sağlar, örneğin 0 ile 100.000.000 arasındaki tek sayıları tutmak için list yerine array kullanmak daha verimli olacaktır.
2- Generator Kullanımı
Büyük verileri itere ederken looplar yerine generatorleri kullanmak bellek optimizasyonu açısından faydalı ve gereklidir, çünkü for/while gibi looplarda tüm veri belleğe alınarak her iterasyonda üzerinde istenen işlem yapılır ancak generatorlerde her seferinde ilgili/ihtiyaç durulan kısmı ele alınır ve üzerinde işlem yapılır.
3- Context Manager Kullanımı
Özellikle I/O işlemlerde bir dosyadan veri okurken veya yazarken sıkça kullandığımız context managerler, resourceu gerektiği zaman/işlem bitince kapattığı için bellek optimizasyonunda önemli rol oynar.
4- Weakref Kullanımı
pythonda weak referencing weakref modülüyle sağlanır, genellikle dairesel referans durumlarında kullanılabilir, weakref proxy gibi yöntemlerle ana nesneye proxy yapan bir zayıf referanslı nesler oluşturulabilir, ana nesne silindiğinde proxy objeside otomatik olarak None olacaktır.
5- Profiling ve Monitoring
cProfile, memory_profiler, tracemalloc (3.4 ve sonrasında standart modül haline gelmiştir.) veya heapy gibi araçlar işlevlerimizin bellek kullanımını izlemeyip analiz etmek ve sakıncalı/uygun olmayan durumları saptayıp aksiyon alabilmemiz için önemli rol oynamaktadır.
6- Memory ve Object Pooling
Bir nesneye ihtiyaç olduğunda yeniden bellek üzerinde yer ayırmak ve yerleştirmek doğal bir işlemdir. Ancak sürekli nesnelerin üretilip kullanılması gerektiği durumlarda maliyetli olacaktır. Bunun yerine oluşturduğumuz nesneleri bir havuzda tutarak ihtiyaç duyduğumuzda bu havuzdan alıp, kullanmak ihtiyacımız sona erdiğinde havuza geri koymak çok daha az maliyetli olacaktır. Object pooling yöntemini ağ bağlantıları gibi kaynakları yönetmek için sıkça başvurulmaktadır. (bkz: multiprocessing)
Memory pooling ise genellikle sabit tipte, boyutta ve küçük ancak fazla sayıda veriyle çalışırken kullanılabilecek bir yöntemdir, bellekteki ayrılan alanların boyutu benzer/aynı olacağından boyutlandırma ve fragmentation tarafında fayda sağlayacaktır.
Temel bellek yönetimi ve python tarafında işlerin nasıl gerçekleştiğine değinmiş olduk, verimli ve optimize programlar yazmak için bellek yönetimi en önemli konulardan biridir ancak unutulmamamız gerekirki, kaynakları verimli kullanmak amacıyla overengiinering yaparak kodu daha kompleks hale getirmek gelecekte uygulamanın bakımını ve yönetimini zorlaştıracaktır.
Be First to Comment