Ana içeriğe geç
Kesit Python
İleri · Eşzamanlılık ve performans

Profil ve optimizasyon

timeit ve cProfile ile ölçme, Big-O sezgisi, set/dict ile sabit zamanlı arama, string birleştirme, üreteçle bellek, lru_cache ve numpy'a geçiş.

Uygulamada aç, 20 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Önce ölç, sonra düzelt

Ilgaz Bank'ın gece raporu 40 dakika sürüyor ve ekip "Python yavaş" diyor. Çoğu zaman yavaş olan Python değil, bir listede milyon kez in aramak ya da döngüde string biriktirmektir. Optimizasyonun ilk kuralı: tahmin etme, ölç. İkinci kuralı: en çok zaman geçen yüzde 5'lik kodu düzelt, gerisine dokunma.

timeit: küçük parçayı ölç

timeit bir ifadeyi çok kez çalıştırıp toplam süreyi verir; çöp toplayıcıyı kapatır, en küçük gürültüyle ölçer. İki alternatifi karşılaştırmak için idealdir.

import timeit

musteriler = list(range(50_000))
kume = set(musteriler)
aranan = [49_999, 25_000, -1, 10]

t_liste = timeit.timeit(lambda: [x in musteriler for x in aranan], number=50)
t_kume = timeit.timeit(lambda: [x in kume for x in aranan], number=50)
print(f"liste in: {t_liste:.4f} s   set in: {t_kume:.6f} s")
print(f"set yaklaşık {t_liste / t_kume:.0f} kat hızlı")

cProfile: büyük programda nerede?

Bütün betikte hangi fonksiyonun süre yediğini cProfile gösterir. Bellek içinde çalıştırıp pstats ile en pahalı birkaç fonksiyonu sıralarsın. tottime fonksiyonun kendi süresi, cumtime çağırdıklarıyla birlikte toplam.

import cProfile, pstats, io

def musteri_bul(musteri_id, liste):
    return musteri_id in liste                  # O(n)

def rapor(n):
    liste = list(range(n))
    bulunan = 0
    for i in range(0, n, 7):
        if musteri_bul(i, liste):
            bulunan += 1
    return bulunan

pr = cProfile.Profile()
pr.enable()
rapor(20_000)
pr.disable()

cikti = io.StringIO()
pstats.Stats(pr, stream=cikti).sort_stats("cumulative").print_stats(4)
satirlar = [s for s in cikti.getvalue().splitlines() if "musteri_bul" in s or "rapor" in s]
print("\n".join(satirlar))

Çıktıda musteri_bul binlerce kez çağrılıyor ve toplam sürenin neredeyse tamamını alıyor. Düzeltme fonksiyonu hızlandırmak değil, veri yapısını değiştirmek: listeyi bir kez set'e çevir.

Big-O sezgisi

Big-O, girdi n büyüyünce sürenin nasıl büyüdüğünü söyler. Sabitleri değil eğilimi önemser; 10 bin satırda fark etmeyen şey 10 milyonda saatlere dönüşür.

İşlemKarmaşıklıkÖrnek
x in set, d[k], lst[i], lst.appendO(1)Hash ile sabit zaman
x in list, lst.index, min/max/sumO(n)Baştan sona tarar
lst.insert(0, x), lst.pop(0)O(n)Her elemanı kaydırır (deque kullan)
sorted, lst.sortO(n log n)Timsort
İç içe iki döngü (her sipariş × her müşteri)O(n·m)Sözlükle O(n+m)'e iner
Sıralı listede bisectO(log n)İkili arama
import timeit

siparisler = [(i, i % 1000) for i in range(20_000)]          # (siparis, musteri_id)
musteriler = [(m, f"Müşteri{m}") for m in range(1000)]

def birlestir_yavas():                     # O(n·m)
    return [(s, ad) for s, m in siparisler for mid, ad in musteriler if mid == m]

def birlestir_hizli():                     # O(n+m)
    ad = dict(musteriler)
    return [(s, ad[m]) for s, m in siparisler]

assert birlestir_yavas()[:3] == birlestir_hizli()[:3]
print(f"yavaş: {timeit.timeit(birlestir_yavas, number=1):.3f} s")
print(f"hızlı: {timeit.timeit(birlestir_hizli, number=1):.4f} s")

Sık kazanımlar

String birleştirme

Döngüde s += parça her seferinde yeni string üretir. Parçaları listede biriktirip "".join(...) ya da f-string kullan.

Üreteçle bellek

[f(x) for x in range(10**7)] tüm listeyi belleğe koyar; (f(x) for x in ...) ya da yield tek seferde bir eleman üretir. sum, max, any gibi tüketiciler üreteçle (generator) çalışır. Büyük dosyayı satır satır okumak da aynı ilkedir.

lru_cache

Aynı girdiyle tekrar tekrar çağrılan saf (yan etkisiz) fonksiyonu functools.lru_cache ile ezberle. Özyinelemeli hesaplar ve pahalı sorgular için çarpıcıdır.

import timeit, sys
from functools import lru_cache

def birlestir_arti(n):
    s = ""
    for i in range(n):
        s += f"{i},"
    return s

def birlestir_join(n):
    return ",".join(str(i) for i in range(n)) + ","

assert birlestir_arti(100) == birlestir_join(100)
print(f"+=: {timeit.timeit(lambda: birlestir_arti(20_000), number=5):.3f} s  "
      f"join: {timeit.timeit(lambda: birlestir_join(20_000), number=5):.3f} s")

liste = [x * x for x in range(100_000)]
uretec = (x * x for x in range(100_000))
print("liste:", sys.getsizeof(liste), "bayt | üreteç:", sys.getsizeof(uretec), "bayt")

@lru_cache(maxsize=None)
def taksit_katsayi(ay: int) -> float:
    return 1.0 if ay == 0 else taksit_katsayi(ay - 1) * 1.02

t0 = timeit.default_timer()
print(round(taksit_katsayi(400), 4), f"{timeit.default_timer() - t0:.5f} s")
print(taksit_katsayi.cache_info())

numpy'a geçiş

Sayısal bir döngüyü ne kadar iyileştirsen de yorumlayıcı maliyeti kalır. Veri tek tip sayı ve işlem eleman eleman ise numpy'a geç: döngü C'de döner, bellek yan yana olur. Kural: "for ile milyon sayı" gördüğünde numpy/pandas düşün.

import timeit, numpy as np

tutarlar = [float(i % 500) for i in range(300_000)]
dizi = np.array(tutarlar)

def kdv_python():
    return sum(t * 1.2 for t in tutarlar if t > 100)

def kdv_numpy():
    return float((dizi[dizi > 100] * 1.2).sum())

assert abs(kdv_python() - kdv_numpy()) < 1e-3
print(f"python: {timeit.timeit(kdv_python, number=3):.3f} s  numpy: {timeit.timeit(kdv_numpy, number=3):.4f} s")
Erken optimizasyonOkunur kodu "belki hızlanır" diye bozma. Önce doğru çalışan kod, sonra ölçüm, sonra yalnızca ölçümün gösterdiği yer. Çoğu rapor için sorun bir tane O(n·m) döngüdür.
Derinleş

Ölçüm araç kutusu: hangi soruya hangi araç?

Derste timeit ve cProfile ile tanıştın. Gerçek işte soru her zaman "ne kadar sürdü?" değildir; bazen "hangi satır?", bazen "çalışan bir sunucu neden takıldı?", bazen "bellek nereye gitti?" diye sorarsın. Her soru için ayrı bir araç vardır.

SoruAraçNerede çalışır?
İki kısa alternatiften hangisi hızlı?timeit (repeat ile)Standart kütüphane, tarayıcıda da
Bir iş bloğu kaç saniye sürdü?time.perf_counter()Standart kütüphane
Hangi fonksiyon zaman yiyor?cProfile + pstatsStandart kütüphane
Fonksiyonun hangi satırı?line_profiler (kernprof)Kendi bilgisayarında, pip install line_profiler
Çalışan süreç neden yavaş? (kodu değiştirmeden)py-spyKendi bilgisayarında, pip install py-spy
CPU + bellek birlikte, satır satırscaleneKendi bilgisayarında, pip install scalene
Hangi satır ne kadar bellek ayırdı?tracemallocStandart kütüphane

perf_counter, time ve process_time

time.time() duvar saatidir; sistem saati senkronize edilince geri bile gidebilir. Süre ölçmek için tekdüze (monotonic) ve yüksek çözünürlüklü time.perf_counter() kullan. time.process_time() yalnızca işlemcinin senin sürecin için harcadığı süreyi verir; ağ ya da disk beklerken artmaz. Fark büyükse kodun hesap yapmıyor, bekliyordur: çözüm algoritma değil, eşzamanlılıktır (bunu eszamanlilik ve async derslerinde gördün).

import time
from functools import wraps

def zamanla(fn):
    @wraps(fn)
    def sarmal(*args, **kwargs):
        t0, c0 = time.perf_counter(), time.process_time()
        try:
            return fn(*args, **kwargs)
        finally:
            duvar = time.perf_counter() - t0
            cpu = time.process_time() - c0
            print(f"{fn.__name__}: duvar={duvar:.3f}s cpu={cpu:.3f}s")
    return sarmal

@zamanla
def hesapla():
    return sum(i * i for i in range(300_000))

@zamanla
def bekle():
    time.sleep(0.2)          # ağ/disk beklemesinin taklidi

hesapla()
bekle()                      # duvar ~0.2 s, cpu ~0: kod bekliyor, hesaplamıyor

timeit.repeat: gürültüye karşı en küçük değer

Tek bir ölçüm arka plandaki işlere, önbelleğe, işlemci frekansına göre oynar. timeit.repeat(..., repeat=5, number=N) ölçümü beş kez tekrarlar; ortalamayı değil en küçüğü al. En küçük değer "kodun gerçekte ne kadar sürdüğüne" en yakın olandır; daha büyük değerler kodun değil makinenin gürültüsüdür.

import timeit

veri = list(range(10_000))
sonuclar = timeit.repeat(lambda: sorted(veri, reverse=True), repeat=5, number=200)
print("ölçümler:", [round(s, 4) for s in sonuclar])
print(f"en iyi: {min(sonuclar) / 200 * 1e6:.1f} µs / çağrı")

Komut satırından profil: kurulum ve sık hatalar

Aşağıdaki araçlar tarayıcıda çalışmaz; kendi bilgisayarında, projenin sanal ortamı (virtual environment) etkinken dene.

# 1) cProfile'ı kod değiştirmeden çalıştır, kümülatif süreye göre sırala
python -m cProfile -s cumtime gece_raporu.py | head -25

# 2) Sonucu dosyaya kaydedip sonra incele
python -m cProfile -o profil.out gece_raporu.py
python -m pstats profil.out        # açılan kabukta: sort cumtime / stats 15

# 3) Satır satır: fonksiyonun üstüne @profile yaz, kernprof ile çalıştır
pip install line_profiler
kernprof -l -v gece_raporu.py

# 4) Çalışan sürece dışarıdan bak (sunucuyu durdurmadan)
pip install py-spy
py-spy top --pid 12345
py-spy record -o profil.svg -- python gece_raporu.py   # alev grafiği (flame graph)
BelirtiNedenÇözüm
NameError: name 'profile' is not defined@profile dekoratörünü (decorator) yalnızca kernprof tanımlar; betiği düz python ile çalıştırdınkernprof -l -v dosya.py kullan ya da işin bitince dekoratörü sil
py-spy: Permission deniedmacOS ve Linux başka sürece bağlanmayı kısıtlarmacOS'ta sudo py-spy ...; Linux'ta sudo ya da kendi başlattığın süreci record -- python ... ile izle
kernprof: command not foundPaket başka bir ortama kuruldu ya da Scripts/bin klasörü PATH'te değilSanal ortamı etkinleştir; python -m kernprof -l -v dosya.py
cProfile çıktısında yüzlerce {built-in method ...} satırıSıralama yapılmadı-s cumtime ekle, ilk 20-30 satıra bak
Profilci de yavaşlatırcProfile her fonksiyon çağrısını sayar; çok küçük fonksiyonları çok sık çağıran kod profil altında 2-3 kat yavaş görünebilir. Mutlak süreye değil, oranlara bak: "toplam sürenin yüzde 80'i şu fonksiyonda" bilgisi doğrudur.

Bellek profili: tracemalloc ile anlık görüntü karşılaştırma

tracemalloc Python'ın ayırdığı belleği izler. İki anlık görüntü (snapshot) alıp farkını satır satır listelersen "bellek nerede büyüdü?" sorusunu yanıtlarsın. get_traced_memory() ise (şu an, tepe) çiftini verir; bir fonksiyonun en yüksek bellek kullanımını ölçmenin en kolay yoludur.

import tracemalloc

def liste_ile(n):
    return sum([i * 2 for i in range(n)])

def uretec_ile(n):
    return sum(i * 2 for i in range(n))

for fn in (liste_ile, uretec_ile):
    tracemalloc.start()
    fn(200_000)
    simdi, tepe = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    print(f"{fn.__name__:11}: tepe {tepe / 1024:8.1f} KB")

Standart kütüphanenin hızlı yolları

Çoğu "hızlandırma" yeni bir şey yazmak değil, C ile yazılmış hazır aracı bulmaktır.

Yavaş kalıpHızlı karşılığıNeden
sorted(x, reverse=True)[:10]heapq.nlargest(10, x)Tümünü sıralamaz: O(n log k)
Sıralı listede for ile eşik aramabisect.bisect_rightİkili arama: O(log n)
Elle sayaç sözlüğücollections.CounterSayım döngüsü C'de
Döngüde sonuc.append(f(x))Liste kapsamı ya da mapMetot araması her turda tekrarlanmaz
Döngüde math.sqrt, self.oranDöngü öncesi yerel ada bağlaYerel ad araması nitelik aramasından ucuzdur
import heapq, bisect
from collections import Counter

tutarlar = [120, 5400, 80, 990, 15000, 300, 7200]
print("en büyük 3:", heapq.nlargest(3, tutarlar))

# Kademeli komisyon: 0-999 %3, 1000-4999 %2, 5000+ %1
esikler = [1000, 5000]
oranlar = [0.03, 0.02, 0.01]
for t in (500, 1000, 7200):
    print(t, "->", oranlar[bisect.bisect_right(esikler, t)])

sehirler = ["İzmir", "Ankara", "İzmir", "Bursa", "İzmir", "Ankara"]
print(Counter(sehirler).most_common(2))

Python yetmediğinde: seçenekler

Algoritmayı düzelttin, numpy'a geçtin, hâlâ yavaş mı? Sırayla şu seçeneklere bakılır: multiprocessing (CPU işini çekirdeklere dağıtır, eszamanlilik dersinde), Numba (pip install numba, sayısal fonksiyonu @njit ile makine koduna derler), Cython (Python'a C tipleri ekleyip derler), PyPy (JIT'li alternatif yorumlayıcı; saf Python döngülerinde çok hızlı, bazı C eklentileriyle sorunlu). Python 3.13 ile deneysel JIT ve GIL'siz (free-threaded) sürüm de geldi; üretimde kullanmadan önce bağımlılıklarının desteklediğini doğrula.

Önce soruyu küçültEn büyük kazanç çoğu zaman daha az veri işlemektir: SQL'de filtreyi veritabanına yaptır, yalnız gereken sütunları oku, dünün sonucunu önbellekten al. Hiç çalışmayan kod en hızlı koddur.
Gerçek iştePerformans düzeltmesini bir pull request'e koyarken "hızlandı" yazmak yetmez. Ölçümü de koy: hangi veri boyutuyla, kaç tekrarla, önce ve sonra kaç saniye. Ekip arkadaşın aynı komutu çalıştırıp sonucu doğrulayabilmeli. Kritik raporlar için basit bir süre testi (ör. "10 bin satır 2 saniyenin altında") CI'da gerilemeyi yakalar.
Uzmanlaş

Optimizasyon sırası

  1. Ölç: cProfile ile en pahalı fonksiyonları bul, timeit ile alternatifi doğrula.
  2. Algoritma ve veri yapısı: O(n²)'yi O(n)'e indir; list → set/dict, pop(0) → deque, sıralı arama → bisect.
  3. Python düzeyi: üreteç, join, lru_cache, yerel değişken, gereksiz kopyadan kaçınma.
  4. Kütüphaneye devret: numpy/pandas, sqlite/veritabanı sorgusu, yerleşik fonksiyonlar (sum, sorted) C'de çalışır.
  5. Paralellik: I/O için thread/asyncio, CPU için süreçler. En son adım, çünkü karmaşıklık getirir.

Bellek de ölçülür

tracemalloc hangi satırın ne kadar bellek ayırdığını gösterir; sys.getsizeof tek nesnenin boyutudur (içindekileri saymaz). Büyük veri için pandas'ta küçük dtype ve category, saf Python'da __slots__ ve üreteç.

Ölçüm hataları

İlk çalıştırma önbellek yüzünden yavaştır; number ve repeat ile birkaç kez ölç, en küçüğü al. Dizüstünde pil modu, arka plan işleri ölçümü oynatır; karşılaştırmayı aynı oturumda ve arka arkaya yap.

Alıştırmalara geç (20)