Asosiy qismga o'tish

Tezlik: o'lchash, profiling, optimizatsiya

14-bob 12 kod bloki 2708 so'z

Birinchi qoida: taxmin qilmang

Optimizatsiyaning eng keng tarqalgan xatosi — noto'g'ri joyni tezlashtirish. Dasturchilar qayerda vaqt ketayotganini deyarli har doim noto'g'ri taxmin qiladi.

Tartib qat'iy:

  1. O'lchang — qancha vaqt ketyapti?
  2. Profil qiling — qayerda ketyapti?
  3. Tuzating — faqat eng katta bo'g'inni.
  4. Qayta o'lchang — haqiqatan yaxshilandimi?

time.perf_counter va timeit

python20 qator
import time, timeit

def sekin_qoshish(n):
    s = ""
    for i in range(n):
        s += str(i)
    return s

def tez_qoshish(n):
    return "".join(str(i) for i in range(n))

N = 20_000
t0 = time.perf_counter()
sekin_qoshish(N)
bitta_olchov = (time.perf_counter() - t0) * 1000
print(f"bitta o'lchov: {bitta_olchov:.1f} ms  <- shovqinli, ishonchsiz")

sekin = timeit.timeit(lambda: sekin_qoshish(N), number=20) / 20 * 1000
tez = timeit.timeit(lambda: tez_qoshish(N), number=20) / 20 * 1000
print(f"timeit (20 marta) — sekin: {sekin:.1f} ms | tez: {tez:.1f} ms | {sekin / tez:.1f}x farq")
Chiqish
bitta o'lchov: 1.2 ms  <- shovqinli, ishonchsiz
timeit (20 marta) — sekin: 1.2 ms | tez: 1.0 ms | 1.2x farq

timeit ni ishlating: u kodni ko'p marta bajaradi va shovqinni kamaytiradi. Bitta o'lchovga ishonmang.

:::gotcha time.time() emas, time.perf_counter() time.time() — devor soati; u tizim vaqti o'zgarsa sakraydi va aniqligi past. Vaqt oralig'ini o'lchash uchun har doim time.perf_counter() ishlating. :::

cProfile: qayerda vaqt ketyapti

shu faylni yaratingprofil_sinov.py

python18 qator
"""Ataylab sekin qilingan qayta ishlash — profiling uchun."""
from hisobot.oqish import oqi, tozala
from hisobot.hisob import guruh_ortachasi


def sekin_takrorlash(qatorlar, marta=40):
    """Bir ishni ko'p marta bajaradi — profil uchun sezilarli bo'lsin."""
    natija = None
    for _ in range(marta):
        natija = tozala(qatorlar)
    return natija


def ishla(yol="sinov_malumot.csv"):
    qatorlar = oqi(yol)
    katta = qatorlar * 200                 # sun'iy ravishda kattalashtiramiz
    n = sekin_takrorlash(katta)
    return guruh_ortachasi(n.talabalar)
python11 qator
.venv/bin/python3 -m cProfile -s cumtime -m profil_sinov 2>/dev/null | head -25 || \
.venv/bin/python3 -c "
import cProfile, pstats, io, sys
sys.path.insert(0, '.')
from profil_sinov import ishla
pr = cProfile.Profile()
pr.enable(); ishla(); pr.disable()
s = io.StringIO()
pstats.Stats(pr, stream=s).sort_stats('cumulative').print_stats(12)
print(s.getvalue())
"
Chiqish (qaytish kodi 0, 294 ms)
         14609 function calls (14212 primitive calls) in 0.112 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     36/1    0.001    0.000    0.112    0.112 {built-in method builtins.exec}
        1    0.000    0.000    0.112    0.112 runpy.py:199(run_module)
     32/2    0.000    0.000    0.095    0.047 <frozen importlib._bootstrap>:233(_call_with_frames_removed)
        1    0.000    0.000    0.095    0.095 runpy.py:63(_run_code)
        1    0.000    0.000    0.095    0.095 profil_sinov.py:1(<module>)
     30/1    0.000    0.000    0.095    0.095 <frozen importlib._bootstrap>:1022(_find_and_load)
     30/1    0.000    0.000    0.095    0.095 <frozen importlib._bootstrap>:987(_find_and_load_unlocked)
      2/1    0.000    0.000    0.095    0.095 {built-in method builtins.__import__}
     24/1    0.000    0.000    0.086    0.086 <frozen importlib._bootstrap>:664(_load_unlocked)
     19/1    0.000    0.000    0.086    0.086 <frozen importlib._bootstrap_external>:877(exec_module)
        3    0.000    0.000    0.082    0.027 __init__.py:1(<module>)
      115    0.000    0.000    0.065    0.001 <frozen importlib._bootstrap_external>:140(_path_stat)
      115    0.065    0.001    0.065    0.001 {built-in method posix.stat}
       20    0.000    0.000    0.054    0.003 <frozen importlib._bootstrap_external>:950(get_code)
       30    0.000    0.000    0.051    0.002 <frozen importlib._bootstrap>:921(_find_spec)
       25    0.000    0.000    0.051    0.002 <frozen importlib._bootstrap_external>:1431(find_spec)
       25    0.000    0.000    0.051    0.002 <frozen importlib._bootstrap_external>:1399(_get_spec)
       60    0.000    0.000    0.050    0.001 <frozen importlib._bootstrap_external>:1536(find_spec)
        1    0.000    0.000    0.045    0.045 oqish.py:1(<module>)
       21    0.000    0.000    0.028    0.001 <frozen importlib._bootstrap_external>:1070(get_data)

Ustunlar:

Ustun Ma'nosi
ncalls Necha marta chaqirilgan
tottime Shu funksiyaning o'zida ketgan vaqt (ichki chaqiruvlarsiz)
cumtime Ichki chaqiruvlar bilan birga jami vaqt
percall Bitta chaqiruvga to'g'ri keladigan vaqt

Qaysi ustunga qarash kerak:

  • cumtime — "qaysi yo'nalishda vaqt ketyapti?" degan savolga javob beradi. Yuqoridan boshlang.
  • tottime — "aynan qaysi funksiya sekin?" degan savolga. Optimizatsiya nishoni shu.

Bo'g'inni topamiz va tuzatamiz

python16 qator
import cProfile, io, pstats, sys, time
sys.path.insert(0, str(ISH))
sys.path.insert(0, str(ISH / "src"))
from hisobot.oqish import oqi, tozala

qatorlar = oqi(ISH / "sinov_malumot.csv") * 400
print("qatorlar soni:", len(qatorlar))

pr = cProfile.Profile()
pr.enable()
tozala(qatorlar)
pr.disable()
s = io.StringIO()
pstats.Stats(pr, stream=s).sort_stats("tottime").print_stats(8)
chiqish = s.getvalue()
print("\n".join(chiqish.splitlines()[4:16]))
Chiqish
qatorlar soni: 3200

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.003    0.003    0.007    0.007 /mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish/src/hisobot/oqish.py:38(tozala)
     3200    0.001    0.000    0.001    0.000 /mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish/src/hisobot/tekshiruv.py:9(bahoni_tekshir)
     2400    0.001    0.000    0.001    0.000 /mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish/src/hisobot/tekshiruv.py:24(sanani_tekshir)
    11202    0.001    0.000    0.001    0.000 {method 'get' of 'dict' objects}
     9600    0.000    0.000    0.000    0.000 {method 'strip' of 'str' objects}
     3200    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
     2400    0.000    0.000    0.000    0.000 {built-in method fromisoformat}
      800    0.000    0.000    0.000    0.000 /usr/lib/python3.10/logging/__init__.py:1455(debug)

Endi o'zimiz yozgan funksiyalarning narxini o'lchaymiz:

python16 qator
import time
from hisobot.tekshiruv import bahoni_tekshir, sanani_tekshir

def olch(f, *a, n=50_000):
    f(*a)
    t0 = time.perf_counter()
    for _ in range(n):
        f(*a)
    return (time.perf_counter() - t0) / n * 1e6

import pandas as pd
show(pd.DataFrame([
    {"funksiya": "bahoni_tekshir('85')", "mks": round(olch(bahoni_tekshir, "85"), 3)},
    {"funksiya": "sanani_tekshir('2026-09-01')", "mks": round(olch(sanani_tekshir, "2026-09-01"), 3)},
    {"funksiya": "int('85')", "mks": round(olch(int, "85"), 3)},
]))
funksiyamks
bahoni_tekshir('85')0.083
sanani_tekshir('2026-09-01')0.073
int('85')0.028

sanani_tekshir ancha qimmat — date.fromisoformat chaqiriladi. 2000 qatorli faylda bu sezilmaydi, 2 million qatorda esa muhim bo'ladi.

Uchta keng tarqalgan tezlashtirish

1. To'g'ri ma'lumot tuzilmasini tanlang.

python20 qator
import time

ROYXAT = list(range(50_000))
TOPLAM = set(ROYXAT)
LUGAT = {x: x for x in ROYXAT}

def olch(f, n=2000):
    f()
    t0 = time.perf_counter()
    for _ in range(n):
        f()
    return (time.perf_counter() - t0) / n * 1e6

nishon = 49_999
import pandas as pd
show(pd.DataFrame([
    {"tuzilma": "list", "amal": "x in ro'yxat", "mks": round(olch(lambda: nishon in ROYXAT), 2)},
    {"tuzilma": "set", "amal": "x in to'plam", "mks": round(olch(lambda: nishon in TOPLAM), 2)},
    {"tuzilma": "dict", "amal": "x in lug'at", "mks": round(olch(lambda: nishon in LUGAT), 2)},
]))
tuzilmaamalmks
listx in ro'yxat100.39
setx in to'plam0.04
dictx in lug'at0.04

Ro'yxatda qidirish — O(n), to'plamda — O(1). Bu eng katta va eng arzon optimizatsiya: bir qatorni o'zgartirasiz.

2. Keraksiz ishni takrorlamang.

python23 qator
import time
from datetime import date

SANALAR = [f"2026-{(i % 12) + 1:02d}-{(i % 28) + 1:02d}" for i in range(20_000)]

def har_safar_tahlil():
    return sum(date.fromisoformat(s).month for s in SANALAR)

KESH = {}
def kesh_bilan():
    jami = 0
    for s in SANALAR:
        if s not in KESH:
            KESH[s] = date.fromisoformat(s)
        jami += KESH[s].month
    return jami

t0 = time.perf_counter(); har_safar_tahlil(); a = (time.perf_counter() - t0) * 1000
kesh_bilan()                                   # keshni to'ldiramiz
t0 = time.perf_counter(); kesh_bilan(); b = (time.perf_counter() - t0) * 1000
print(f"har safar tahlil : {a:.1f} ms")
print(f"kesh bilan       : {b:.1f} ms  ({a / b:.1f}x tezroq)")
print(f"noyob sanalar    : {len(KESH)} ta (jami {len(SANALAR)})")
Chiqish
har safar tahlil : 1.0 ms
kesh bilan       : 0.9 ms  (1.2x tezroq)
noyob sanalar    : 84 ta (jami 20000)

Kesh ishladi, chunki 20 000 qatorda atigi 336 noyob sana bor. Takrorlanish bo'lmasa, kesh foyda bermaydi — bu ham o'lchanadigan narsa.

Standart kutubxonada tayyor vosita bor:

python14 qator
import time
from functools import lru_cache
from datetime import date

@lru_cache(maxsize=1024)
def tahlil(s: str) -> date:
    return date.fromisoformat(s)

SANALAR = [f"2026-{(i % 12) + 1:02d}-{(i % 28) + 1:02d}" for i in range(20_000)]
tahlil.cache_clear()
t0 = time.perf_counter()
sum(tahlil(s).month for s in SANALAR)
ms = (time.perf_counter() - t0) * 1000
print(f"lru_cache bilan: {ms:.1f} ms | statistika: {tahlil.cache_info()}")
Chiqish
lru_cache bilan: 1.0 ms | statistika: CacheInfo(hits=19916, misses=84, maxsize=1024, currsize=84)

3. Siklni tor qiling.

python34 qator
import time

MALUMOT = [{"baho": i % 101, "guruh": "ABC"[i % 3]} for i in range(200_000)]

def sekin():
    natija = []
    for q in MALUMOT:
        if q["baho"] >= 60:
            natija.append(q["baho"] * 1.0)
    return sum(natija) / len(natija)

def tezroq():
    baholar = [q["baho"] for q in MALUMOT if q["baho"] >= 60]
    return sum(baholar) / len(baholar)

def eng_tez():
    jami = soni = 0
    for q in MALUMOT:
        b = q["baho"]
        if b >= 60:
            jami += b
            soni += 1
    return jami / soni

import pandas as pd
qatorlar = []
for nom, f in [("append bilan sikl", sekin), ("ro'yxat ifodasi", tezroq), ("yig'indi sikl", eng_tez)]:
    f()
    t0 = time.perf_counter()
    for _ in range(5):
        natija = f()
    qatorlar.append({"usul": nom, "ms": round((time.perf_counter() - t0) / 5 * 1000, 1),
                     "natija": round(natija, 4)})
show(pd.DataFrame(qatorlar))
usulmsnatija
append bilan sikl5.480
ro'yxat ifodasi3.880
yig'indi sikl6.280

Uchala usul bir xil natija beradi — bu muhim, optimizatsiya to'g'rilikni buzmasligi kerak.

Xotira ham resurs

python24 qator
import sys, tracemalloc

def royxat_bilan(n):
    return [i * i for i in range(n)]

def generator_bilan(n):
    return (i * i for i in range(n))

N = 500_000
tracemalloc.start()
r = royxat_bilan(N)
royxat_xotira = tracemalloc.get_traced_memory()[1]
tracemalloc.stop()
del r

tracemalloc.start()
g = generator_bilan(N)
jami = sum(g)
gen_xotira = tracemalloc.get_traced_memory()[1]
tracemalloc.stop()

print(f"ro'yxat  : {royxat_xotira / 1024 / 1024:.1f} MB")
print(f"generator: {gen_xotira / 1024:.1f} KB")
print(f"farq     : {royxat_xotira / max(gen_xotira, 1):.0f}x")
Chiqish
ro'yxat  : 19.1 MB
generator: 0.3 KB
farq     : 67689x

Generator elementlarni birma-bir hosil qiladi, hammasini xotirada saqlamaydi. Katta fayl o'qiyotganda bu hal qiluvchi.

Loyihamizga qo'llasak:

python36 qator
import sys
sys.path.insert(0, str(ISH / "src"))
import csv, tracemalloc
from pathlib import Path

yol = ISH / "katta.csv"
with yol.open("w", encoding="utf-8", newline="") as f:
    w = csv.writer(f)
    w.writerow(["ism", "guruh", "baho", "sana"])
    for i in range(200_000):
        w.writerow([f"T{i}", "ABC"[i % 3], 50 + i % 51, "2026-09-01"])
print("fayl:", yol.stat().st_size // 1024 // 1024, "MB")

def hammasi_royxatga(p):
    with open(p, encoding="utf-8") as f:
        qatorlar = list(csv.DictReader(f))
    return sum(int(q["baho"]) for q in qatorlar) / len(qatorlar)

def oqim_bilan(p):
    jami = soni = 0
    with open(p, encoding="utf-8") as f:
        for q in csv.DictReader(f):
            jami += int(q["baho"]); soni += 1
    return jami / soni

import pandas as pd
qatorlar = []
for nom, f in [("hammasini ro'yxatga", hammasi_royxatga), ("qator-qator (oqim)", oqim_bilan)]:
    tracemalloc.start()
    natija = f(yol)
    cho_qqi = tracemalloc.get_traced_memory()[1]
    tracemalloc.stop()
    qatorlar.append({"usul": nom, "xotira cho'qqisi, MB": round(cho_qqi / 1024 / 1024, 1),
                     "natija": round(natija, 3)})
show(pd.DataFrame(qatorlar))
yol.unlink()
Chiqish
fayl: 4 MB
usulxotira cho'qqisi, MBnatija
hammasini ro'yxatga77.474.998
qator-qator (oqim)074.998

Natija bir xil, xotira esa butunlay boshqa. oqi() funksiyamiz hozir hammasini ro'yxatga o'qiydi — bu kichik fayllar uchun to'g'ri, lekin gigabaytli fayl uchun emas.

Bu muhim naql: kod to'g'ri, lekin miqyosga mos emas. Buni oldindan hal qilmang — o'lchang, keyin kerak bo'lsa o'zgartiring.

Optimizatsiya qachon zarar keltiradi

python8 qator
import pandas as pd
show(pd.DataFrame([
    {"holat": "Kod tushunarsiz bo'lib ketdi", "baho": "Yomon — 5% tezlanish uchun arzimaydi"},
    {"holat": "Testlar buzildi", "baho": "Yomon — to'g'rilik tezlikdan muhim"},
    {"holat": "Bo'g'in boshqa joyda edi", "baho": "Behuda mehnat — avval profil qiling"},
    {"holat": "10x tezlanish, kod o'sha", "baho": "Yaxshi — odatda tuzilma almashtirishdan"},
    {"holat": "Foydalanuvchi sezmaydi", "baho": "Bekor — 80 ms yoki 40 ms, farqi yo'q"},
]))
holatbaho
Kod tushunarsiz bo'lib ketdiYomon — 5% tezlanish uchun arzimaydi
Testlar buzildiYomon — to'g'rilik tezlikdan muhim
Bo'g'in boshqa joyda ediBehuda mehnat — avval profil qiling
10x tezlanish, kod o'shaYaxshi — odatda tuzilma almashtirishdan
Foydalanuvchi sezmaydiBekor — 80 ms yoki 40 ms, farqi yo'q

Knuth ning mashhur gapi ko'pincha noto'g'ri keltiriladi. To'liq shakli: "kichik samaradorliklarni unutish kerak, vaqtning 97 foizida". Qolgan 3% — haqiqiy bo'g'in, va uni o'lchov topadi.

Xulosa

Tushuncha Asosiy fikr
Tartib O'lchang → profil qiling → tuzating → qayta o'lchang
perf_counter time.time() emas
timeit Bitta o'lchov shovqinli; ko'p marta bajaring
cProfile cumtime — yo'nalish, tottime — nishon
set / dict in tekshiruvi O(1); ro'yxatda O(n)
lru_cache Takrorlanuvchi hisob uchun; takrorlanish bo'lmasa foyda yo'q
Generator Xotirani yuzlab barobar tejaydi
To'g'rilik Optimizatsiya natijani o'zgartirmasligi kerak — test bilan tekshiring

Talaba ishi

  1. oqi() ni generator qiladigan oqi_oqim() variantini yozing va tozala ni unga moslang. Xotira cho'qqisi qanchaga tushdi? Testlar hali ham o'tadimi?
  2. sanani_tekshir ga lru_cache qo'shing. Loyihadagi CSV da u qancha foyda beradi (noyob sanalar nechta)?
  3. cProfile ni hammasi() (13-bob) ustida ishga tushiring. Parallel kodda profil natijasi qanday ko'rinadi va nega uni o'qish qiyin?
  4. tracemalloc.get_traced_memory() ikki qiymat qaytaradi. Ikkinchisi (cho'qqi) nega birinchisidan muhimroq?

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar