Python / Гүйцэтгэлийн оновчлол

Гүйцэтгэлийн оновчлол

"Эрт оновчлох нь бүх муугийн үндэс" гэж алдартай хэлдэг — эхлээд зөв ажиллуулж, дараа нь хурдлуул. Гэхдээ программ удааширч эхлэхэд хаана гацаж байгааг олж, зориудаар оновчлох чадвар маш чухал. Python-д үүний тулд timeit, cProfile болон хэд хэдэн ухаалаг техник байдаг.

Хурдыг хэмжих

Таамаглалаар оновчлол хийхгүй — эхлээд хэмж:

python
import timeit

# Жижиг кодын хурд хэмжих
удаан = timeit.timeit(
    "[x**2 for x in range(1000)]",
    number=10_000
)
хурдан = timeit.timeit(
    "list(map(lambda x: x**2, range(1000)))",
    number=10_000
)
print(f"List comprehension: {удаан:.4f}с")
print(f"map + lambda:       {хурдан:.4f}с")

Том функцийн аль хэсэг удаан байгааг олохд cProfile хэрэглэнэ:

bash
python -m cProfile -s cumulative миний_програм.py
python
import cProfile

def удаан_функц() -> int:
    нийт = 0
    for i in range(100_000):
        нийт += i * i
    return нийт

cProfile.run("удаан_функц()")
# ncalls, tottime, percall гэх мэт статистик хэвлэнэ

List comprehension ба generator

for давталтын оронд list comprehension нь CPython-д оновчилсон байдлаар ажилладаг тул ихэвчлэн хурдан:

python
import timeit

# For давталт
def for_арга() -> list[int]:
    үр_дүн = []
    for i in range(10_000):
        if i % 2 == 0:
            үр_дүн.append(i * i)
    return үр_дүн

# List comprehension
def comprehension_арга() -> list[int]:
    return [i * i for i in range(10_000) if i % 2 == 0]

print(timeit.timeit(for_арга,          number=1000))
print(timeit.timeit(comprehension_арга, number=1000))

Generator нь бүх утгыг санах ойд нэгэн зэрэг ачаалахгүй — нэг нэгээр нь үйлдвэрлэдэг. Том өгөгдөлтэй ажиллахад санах ойг хэмнэдэг:

python
# List — бүгдийг санах ойд хадгална
тоонуудын_жагсаалт = [i * i for i in range(1_000_000)]   # ~8MB

# Generator — зөвхөн нэг утга нэг удаа
тоонуудын_generator = (i * i for i in range(1_000_000))   # ~200 байт

# Generator-г for давталтаар ашиглах
нийт = sum(i * i for i in range(1_000_000))
print(нийт)

lru_cache — үр дүнг цээжлэх

Нэг аргументаар дахин дахин дуудагддаг тооцооллын хүнд функцэд memoization хэрэглэнэ — functools.lru_cache декоратор хариуг цээжилж, давтан тооцохгүй:

python
import functools
import timeit

# Цээжлэхгүй хувилбар
def фибоначи(n: int) -> int:
    if n <= 1:
        return n
    return фибоначи(n - 1) + фибоначи(n - 2)

# Цээжлэх хувилбар
@functools.lru_cache(maxsize=None)
def фибоначи_хурдан(n: int) -> int:
    if n <= 1:
        return n
    return фибоначи_хурдан(n - 1) + фибоначи_хурдан(n - 2)

# Харьцуулах (n=35)
удаан  = timeit.timeit(lambda: фибоначи(35),        number=3)
хурдан = timeit.timeit(lambda: фибоначи_хурдан(35), number=3)

print(f"Цээжлэхгүй: {удаан:.2f}с")
print(f"lru_cache:  {хурдан:.6f}с")
# Цээжлэхгүй: ~4.50с  →  lru_cache: ~0.000003с

Өгөгдлийн бүтэц сонгох

Зөв өгөгдлийн бүтэц сонгох нь алгоритмоос ч илүү нөлөөтэй байж болно:

python
import timeit

их_жагсаалт = list(range(100_000))
их_олонлог  = set(range(100_000))

# Жагсаалтаас хайх — O(n)
хугацаа_жагсаалт = timeit.timeit(
    "99_999 in их_жагсаалт",
    globals={"их_жагсаалт": их_жагсаалт},
    number=10_000
)

# Олонлогоос хайх — O(1)
хугацаа_олонлог = timeit.timeit(
    "99_999 in их_олонлог",
    globals={"их_олонлог": их_олонлог},
    number=10_000
)

print(f"list хайлт:  {хугацаа_жагсаалт:.4f}с")
print(f"set хайлт:   {хугацаа_олонлог:.4f}с")
# set нь жагсаалтаас ~100 дахин хурдан байна

Гүйцэтгэлийн оновчлол бол таамаглах биш — хэмжиж, хаана удааширч байгааг олж, тодорхой аргаар шийдэх ажил юм. Энэ хичээлд сурсан аргуудаа бодит кодондоо хэрэглэж туршаарай.

Дараагийн хичээлд:

Machine Learning-ийн ертөнцөд орно — scikit-learn ашиглан анхны загвараа бүтээж, өгөгдлийг ангилж, урьдчилан таамаглаж сурна. Урьдчилсан мэдлэг огт шаардлагагүй.