Tezlik: o'lchash, profiling, optimizatsiya
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:
- O'lchang — qancha vaqt ketyapti?
- Profil qiling — qayerda ketyapti?
- Tuzating — faqat eng katta bo'g'inni.
- Qayta o'lchang — haqiqatan yaxshilandimi?
time.perf_counter va timeit
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")
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
"""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)
.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()) "
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
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]))
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:
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)}, ]))
| funksiya | mks |
|---|---|
| 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.
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)}, ]))
| tuzilma | amal | mks |
|---|---|---|
| list | x in ro'yxat | 100.39 |
| set | x in to'plam | 0.04 |
| dict | x in lug'at | 0.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.
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)})")
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:
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()}")
lru_cache bilan: 1.0 ms | statistika: CacheInfo(hits=19916, misses=84, maxsize=1024, currsize=84)
3. Siklni tor qiling.
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))
| usul | ms | natija |
|---|---|---|
| append bilan sikl | 5.4 | 80 |
| ro'yxat ifodasi | 3.8 | 80 |
| yig'indi sikl | 6.2 | 80 |
Uchala usul bir xil natija beradi — bu muhim, optimizatsiya to'g'rilikni buzmasligi kerak.
Xotira ham resurs
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")
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:
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()
fayl: 4 MB
| usul | xotira cho'qqisi, MB | natija |
|---|---|---|
| hammasini ro'yxatga | 77.4 | 74.998 |
| qator-qator (oqim) | 0 | 74.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
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"}, ]))
| holat | baho |
|---|---|
| Kod tushunarsiz bo'lib ketdi | Yomon — 5% tezlanish uchun arzimaydi |
| Testlar buzildi | Yomon — to'g'rilik tezlikdan muhim |
| Bo'g'in boshqa joyda edi | Behuda mehnat — avval profil qiling |
| 10x tezlanish, kod o'sha | Yaxshi — odatda tuzilma almashtirishdan |
| Foydalanuvchi sezmaydi | Bekor — 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
oqi()ni generator qiladiganoqi_oqim()variantini yozing vatozalani unga moslang. Xotira cho'qqisi qanchaga tushdi? Testlar hali ham o'tadimi?sanani_tekshirgalru_cacheqo'shing. Loyihadagi CSV da u qancha foyda beradi (noyob sanalar nechta)?cProfilenihammasi()(13-bob) ustida ishga tushiring. Parallel kodda profil natijasi qanday ko'rinadi va nega uni o'qish qiyin?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.