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 Nişan’ı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 işe 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 yaniyor; hesaplama, döngü, veri dönüştürme, sıkı sıkıya çalışan algoritmalar, bunların hepsi burada. I/O-bound tarafta işe 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 hâle 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 yanilı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 yaniltabilir |
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 işe biraz aldatıcı olabiliyor; üst seviye orkestratör fonksiyonları şişkin gösteriyor ve sızı 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ı hâle 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ı hikâye. 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 işe 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 işe 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 hâlde 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 hâle 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 hâline 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.
Sıkça Sorulan Sorular
Python’da performans sorunu olduğunu nasıl anlarım: CPU mu I/O mu?
Önce “iş neyi bekliyor?” sorusunu düşünmek işe yarar. CPU-bound işlerde işlemci çalışır (hesaplama/dönüşüm), I/O-bound işlerde işe ağ, veritabanı ya da disk gibi kaynaklar yanit vermeyi bekler. Basit bir profil/ölçümle (ör. örnekleme profilleme veya zamanlama) bu ayrımı hızlıca netleştirebilirsin; ben de birçok kez asıl suçlunun veritabanı gecikmesi çıktığını gördüm.
“Döngü yavaş” sanıp refactor yapmak doğru yaklaşım mı?
Çoğu zaman hayır; hissiyata göre yapılan refactor, gerçek darboğazı kaçırıp başka yerde maliyeti büyütebilir. Önce ölçüm yapıp zamanın nereye aktığını görmek gerekiyor. Benim tecrübemde, “kod temiz ama uygulama sürünüyor” diyen ekiplerde bile profil sonucu çoğunlukla ağ/DB beklemesi gibi daha görünmez yerlerden çıkıyordu.
time.perf_counter() ile timeit arasındaki fark ne, ne zaman hangisini kullanmalıyım?
time.perf_counter() genelde hızlı bir “kaba bakış” için kullanılır; kısa denemelerde genel gidişatı görmeye yardım eder. timeit işe küçük dalgalanmaları daha iyi bastırdığı için mikro-benchmark’larda daha güvenilir sonuç verir. Ben genelde önce perf_counter ile yön bulur, sonra timeit ile kritik parçaları ölçüp karar veririm.
asyncio eklemek her zaman performansı artırır mı?
Hayır, özellikle CPU-bound işlerde asyncio genelde mucize yaratmaz; sadece event loop üstüne ekstra karmaşıklık ekleyebilir. Eğer asıl problem bekleme (I/O) işe, doğru tasarım ve paralel/async çağrılar fayda sağlayabilir. Mantık şu: beklediğin şey ağ/DB işe async anlamlı, hesaplama yoğun işe algoritma ve sorgu optimizasyonu daha etkili ölür.
Veritabanı çağrıları yavaşsa Python tarafında neyi hedeflemeliyim?
Öncelikle sorgu sayısını ve sorgu şekillerini kontrol etmek gerekir: N+1 problemi, gereksiz alan çekme, kötü join’ler gibi durumlar çok sık çıkar. “Cache ekleyelim” tek başına her zaman çözüm değildir; çoğu zaman toplu işlem (batch), sorguyu sadeleştirme ve doğru indeksleme daha büyük kazanım sağlar. Eğer sistem büyüdüyse, ölçümle birlikte uygulama katmanında gereksiz veri kopyalamayı da elemek iyi ölür.
Kaynaklar ve İleri Okuma
Azure mimarisi: İzleme (Monitoring) en iyi uygulamaları
Azure Application Insights Profiler
Azure mimarisi: Performans için en iyi uygulamalar
Python timeit modülü (resmî dokümantasyon)
Bu içerik işinize yaradı mı?
Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.



