Bir Python kodu yavaşladığında insanın ilk refleksi hep aynı: suçlu bir döngü aramak. Hani gözle görülen en bariz yere abanıyoruz ya… Geçen yıl, 2024 Nisan’ında İstanbul’da çalışan küçük bir fintech ekibinde buna benzer bir şeye takıldım; herkes “şu liste taraması” diyordu, (ciddiyim). Ölçünce asıl masrafın veritabanı çağrılarında olduğu çıktı. Açık konuşayım: performans işi çoğu zaman tahmin değil, sayılar oyunu.
İşin aslı şu ki, Python’da darboğaz bulmak biraz doktorluk gibi — önce teşhis koyuyorsun, sonra ilaç veriyorsun. Rastgele refaktör yapmak ise bayağı pahalı bir hobiye dönüşüyor. Bir fonksiyonu hızlandırdığını sanıp başka yerde daha büyük yavaşlık yaratmak çok kolay, hem de farkında olmadan. Bu yüzden “hissiyat” yerine ölçüm şart. Kesinlikle.
Çok konuştum, örnekle göstereyim.
Benim editör masasında bu konuyu ilk gördüğümde aklıma 2023 sonbaharında Ankara’daki bir SaaS projesi geldi. Takım lideri bana “kodda her şey temiz ama uygulama sürünüyor” demişti (buna dikkat edin). Profil çıkarınca gördük ki sorun ne matematikte ne de string işlemlerindeydi; asıl bela ağ gecikmesiydi. Yani mesele bazen o kadar da dramatik değil… sadece yanlış yere bakıyorsun.
Önce Ne Olduğunu Anla: CPU mu I/O mu?
Tuhaf ama, Şöyle bir şey var. Performans sorunlarını iki ana kutuya ayırmak iyi bir başlangıç noktası: CPU-bound ve I/O-bound. CPU-bound tarafta işlemci yanıyor; hesaplama, döngü, veri dönüştürme, sıkı sıkıya çalışan algoritmalar, bunların hepsi burada. I/O-bound tarafta ise program bekliyor — diskten dosya okumak, API çağrısı yapmak, veritabanından sonuç beklemek gibi durumlar,. Aslında “neden bu kadar uzun sürüyor ki” diye şikayet ettiğin şeylerin büyük kısmı buradan geliyor (yanlış duymadınız)
Eh, Bu ayrımı yapmadan çözüm üretmek, lastiği patlamış arabaya spoiler takmaya benziyor. Mesela ağır matematik yapan bir fonksiyona asyncio eklemek çoğu zaman hiçbir fayda sağlamıyor; sadece event loop üstüne ekstra yük bindiriyor ve işleri daha da girift hale getiriyor. Tam tersi de geçerli: ağ çağrısı bekleyen bir sistemi daha fazla çekirdekle şişirmek genelde bütçeyi eritiyor. İşe yaramaz.
Ben kendi testlerimde şunu çok gördüm: sorunun tipi doğru anlaşılınca çözümün yarısı zaten cebinde oluyor. Bazen tek yapılacak şey cache eklemek değil; sorguyu sadeleştirmek ya da toplu işlem yapmak. E tabi bu kadar basit olmayan durumlar da var — özellikle eski kod tabanlarında her yer birbirine girmişse, orada iş çetrefilleşiyor biraz.
Zaman Ölçmeden Konuşmak Boş İş
Hızlı hissettiren kod ile gerçekten hızlı olan kod aynı şey değil. Bunun için önce time.perf_counter() ile kaba bir bakış atılır, sonra iş ciddileşince timeit devreye girer. timeit, küçük dalgalanmaları törpüleyip daha dürüst bir ortalama veriyor — bu fark küçük gibi görünse de karar verirken hayat kurtarıyor.
Evet, doğru duydunuz. Daha fazla bilgi için Kod Karo’da Video Arama: Canlı Kod Editörüne Yeni Soluk yazımıza bakabilirsiniz.
Geçen martta İzmir’deki bir e-ticaret ekibine danışırken tam bunu yaşadık. Bir geliştirici tek seferlik (belki yanılıyorum ama) süre ölçüp “tamam düzeldi” demişti; halbuki aynı kod farklı yükte bambaşka davranıyordu. Tek koşumluk süre neredeyse her zaman kandırıcıdır, çünkü OS planlayıcısı, önbellek etkisi, arka plandaki süreçler… hepsi işin içine giriyor ve sonucu bozuyor.
| Yöntem | Neye iyi gelir? | Zayıf tarafı |
|---|---|---|
perf_counter() |
Kaba kontrol, hızlı doğrulama | Tek ölçüm yanıltabilir |
timeit |
Mikro benchmark ve tekrar edilebilirlik | Gerçek sistem yükünü birebir yansıtmaz |
cProfile |
Bottleneck’i bulmak | Cumtime yüzünden başlangıçta kafa karıştırabilir |
Kısacası önce kısa mesafe koşusu yapma; maraton koşacaksan ritmi ona göre ölç. Hele bir de temsil gücü düşük küçük örnekler tehlikeli — L1 cache’e sığan minik veriyle aldığın sonuçlar prod ortamda çöpe dönebilir. Dönüyor da zaten.
cProfile: Suçu Kime Atacağını Öğrenmek
Sorun yavaş ama nedenini bilmiyorsan sıradaki durak genelde cProfile. Burada amaç süslü rapor almak değil; hangi fonksiyonun gerçekten vakit yediğini görmek. İlk bakacağın metrik çoğu zaman tottime olmalı,. O değer fonksiyonun kendi içinde harcadığı zamanı gösteriyor — başka şeylere yapılan çağrılar dahil değil, saf maliyet.
Durun, bir saniye.
Cumtime ise biraz aldatıcı olabiliyor; üst seviye orkestratör fonksiyonları şişkin gösteriyor ve sizi yanlış yere yönlendirebiliyor. Ben buna birkaç kez düşpek çok doğrusu. 2022’de Berlin merkezli bir uzaktan çalışma projesinde ekip önce en üst fonksiyonu optimize etmeye girişti — sonuç? Hiçbir şey değişmedi. Saatler gitti.
import cProfile
import pstats
def run():
heavy_task()
cProfile.run('run()', 'profile.out')
stats = pstats.Stats('profile.out')
stats.sort_stats('tottime').print_stats(10)
Aynen böyle yani: önce ölçüyorsun, sonra ayıklıyorsun, sonra tekrar ölçüyorsun. Bu döngü yoksa optimize etme dediğin şey makyajdan öteye geçmiyor. Nokta.
Klasik Python Yavaşlıkları: Aynı Tuzaklara Defalarca Düşmeyin
Peki en çok nerede kaybediyoruz? Liste üyelik kontrolleri burada baş belası. if item in my_list ifadesi — kendi adıma konuşayım — liste büyüdükçe O(n) maliyetle can sıkıyor; döngünün içinde kullanırsan iş çabucak O(n²)’ye kayıyor ve farkında bile olmuyorsun. Buna karşılık set veya dict kullandığında tablo değişiyor — erişim pratikte çok daha hızlı hale geliyor, bu kadar basit aslında. Bu konuyla ilgili Avrupa’nın Veri Güvensizliği: ABD ve Çin Neden Aynı Kaderi Paylaşıyor? yazımıza da göz atmanızı tavsiye ederim.
Dize birleştirme tarafında da aynı hikaye. Döngü içinde sürekli += ile string büyütmek yeni nesneler üretiyor ve belleği yoruyor. Önerim net: mümkünse parçaları bir listeye topla ve sonunda "".join() kullan. Küçük değişiklik, ciddi fark. FERPA Uyumlu RAG: Kurumsal Sistemler Nerede Çuvallıyor? yazımızda bu konuya da değinmiştik.
- Liste araması: Küçük listede idare eder, büyük listede pahalıya patlar.
.apply(axis=1): Pandas dünyasında sık görülen gizli yavaşlık kaynaklarından biri.- Küresel değişken erişimi: Local değişken kadar ucuz değil; küçük farklar milyon iterasyonda büyüyor.
- Ağ çağrıları: Çoğu zaman gerçek suçlu burası oluyor ama ilk bakışta saklanıyor. — bunu es geçmeyin
Pandas cephesinde de küçük bir itirazım var: .apply bazen pratik görünüyor. Satır satır Python turuna dönüştüğü için hayal kırıklığı yaşatabiliyor. Vektörize işlemler daha derli toplu çalışıyor ve çoğu durumda ciddi fark yaratıyor. Her şeyi apply ile halletmeye çalışmak kısa vadede rahat ettirir ama uzun vadede can sıkar — söz veriyorum. Next.js ve PostgreSQL ile Ölçeklenen SaaS Kurmak yazımızda da bu konuya değinmiştik. LangChain Ajanlarını Üretimde İzlemek: Gerçek Zamanlı Rehber yazımızda da bu konuya değinmiştik.
Küçük Startup vs Kurumsal Sistem
Küçük startup’larda sorun genelde tek sunucuda yaşanır ve hızlı teşhis avantaj sağlar. Ama orada da disiplin yoksa işler dağılıyor; biri cache eklerken diğeri sorguyu değiştiriyor, üçüncüsü log seviyesini açıp sistemi boğuyor. Kaos.
Enterprise tarafında ise problem daha sessiz — mikro servislerin arasında kaybolur, izleme yoksa olayın kökünü bulmak gerçekten zorlaşır. Açık söyleyeyim: kurumsal projelerde performans problemi çoğu zaman “tek büyük hata” değil, onlarca küçük ihmalin birleşimi oluyor. Birkaç milisaniyelik gecikme tek başına önemsiz görünür ama zincirleme etkisi fena vurur. Küçük ekiplerde ise tersine, kötü yazılmış tek döngü bütün deneyimi çökertmeye yeter.
Daha Hızlı Olmanın Mantıklı Yolları Var mı?
Var. Ama sihir yok. Öncelikle veriyi küçültmek gerekebilir; gereksiz kolonları taşımayın, boş yere JSON şişirmeyin, aynı işi defalarca yapmayın. Sonra cache düşünülür —. Cache’i “her derde deva” sanmak da ayrı derttir çünkü invalidation kısmı can yakar, bunu yaşayan bilir (ciddiyim)
Şöyle ki, Bazen de çözüm algoritmayı değiştirmektir. Mesela lineer aramayı hash tabanlı yapıya çevirmek kulağa basit gelir ama etkisi muazzam olabiliyor (şaşırtıcı ama gerçek). Bir arkadaşım Londra’daki ürününde tam bunu yaptı ve üç haftalık uğraşı tek öğleden sonra toparladı — tabii sistemin geri kalanı hazırsa bu mümkün oluyor, aksi halde ufak domino etkileri çıkabiliyor. Neyse, çok dağıttım.
Ölçmeden optimize etmek çoğu zaman sadece kodu başka yere taşımaktır; hız kazancı sandığınız şey bazen yalnızca karmaşıklığın yer değiştirmesidir.
Neyse uzatmayayım, pratik tarafta şu alışkanlıklar baya işe yarıyor:
- Sorunu önce daraltın, sonra benchmark alın. (bence en önemlisi)
- Tek koşuma güvenmeyin; tekrar edin. — bunu es geçmeyin
- Tottime‘a bakın, cumtime’a körü körüne bağlanmayın.
- Döngü içindeki üyelik kontrollerini gözden geçirin.
- Pandas’ta satır bazlı mantığı mümkünse vektörel hale getirin.
Sahada İşe Yarayan Küçük İpuçları
Son olarak birkaç şey daha söyleyeyim, bunlar kitaplarda pek geçmiyor ama gerçekten işe yarıyor. Profil almayı “sorun çıkınca” değil, düzenli aralıklarla alışkanlık haline getirmek büyük fark yaratıyor. Sorun çıkmadan önce neyin normal olduğunu bilirsen, sorun çıkınca nereye bakacağını da bilirsin.
Hani, Bir de şu var: iyileştirme yapacaksan önce testi yaz (evet, doğru duydunuz). Çünkü “hızlandırdım” derken davranışı bozduğun çok oluyor — özellikle köşe case’lerde. Hız kazandım ama doğruluk kaybettim, bu hiç iyi bir takas değil. Maalesef.
Şunu söyleyeyim, Son bir not: bazen en iyi optimizasyon o özelliği hiç yazmamak oluyor. Kullanılmayan, gereksiz yere çalışan kod hep var büyük projelerde — kaldırdığında sistem anında nefes alıyor. Bunu da gördüm sahada, birden fazla kez.
Bu içerik işinize yaradı mı?
Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.



