Performance و Profiling
قبل از بهینهسازی، باید بدانیم کجا کند است. در این فصل با ابزارهای پروفایل (timeit، cProfile، line_profiler، memory_profiler)، تکنیکهای بهینهسازی، Big O، lazy evaluation و راهحلهای نهایی (numpy، Cython) آشنا میشویم.
قبل از بهینهسازی، باید بدانیم کجا کند است. در این فصل با ابزارهای پروفایل (timeit، cProfile، line_profiler، memory_profiler)، تکنیکهای بهینهسازی، Big O، lazy evaluation و راهحلهای نهایی (numpy، Cython) آشنا میشویم.
قانون طلایی بهینهسازی
“Premature optimization is the root of all evil” – Donald Knuth
- اول کد درست بنویسید
- اگر کند بود، اندازهگیری کنید
- گلوگاه واقعی را پیدا و بهینه کنید
- دوباره اندازهگیری کنید
timeit – اندازهگیری ساده
import timeit
# اندازهگیری یک خط
t = timeit.timeit("'-'.join(str(n) for n in range(100))", number=10000)
print(f"{t:.4f}s")
# مقایسه دو روش
t1 = timeit.timeit(
"'-'.join([str(n) for n in range(100)])", # list comp
number=10000
)
t2 = timeit.timeit(
"'-'.join(str(n) for n in range(100))", # generator
number=10000
)
print(f"List: {t1:.4f}s")
print(f"Gen: {t2:.4f}s")
# با setup
t = timeit.timeit(
"f(100)",
setup="from math import factorial as f",
number=10000
)
در خط فرمان
python -m timeit "'-'.join(str(n) for n in range(100))"
# 10000 loops, best of 5: 30.5 usec per loop
python -m timeit -s "data = list(range(1000))" "sum(data)"
در Jupyter
# magic command
%timeit sum(range(1000))
%%timeit
total = 0
for i in range(1000):
total += i
cProfile – پروفایل تابعمحور
import cProfile
def slow_function():
total = 0
for i in range(1_000_000):
total += i ** 2
return total
# پروفایل
cProfile.run("slow_function()")
# ذخیره برای تحلیل
cProfile.run("slow_function()", "profile_output.prof")
# در خط فرمان
python -m cProfile -o output.prof script.py
# با مرتبسازی
python -m cProfile -s cumulative script.py
تحلیل با pstats
import pstats
from pstats import SortKey
p = pstats.Stats("profile_output.prof")
p.strip_dirs() # حذف مسیرهای طولانی
p.sort_stats(SortKey.CUMULATIVE) # مرتب بر cumulative time
p.print_stats(20) # 20 ردیف اول
# یا
p.sort_stats(SortKey.TIME) # زمان درون تابع
p.print_stats(20)
snakeviz – نمایش گرافیکی
pip install snakeviz
snakeviz profile_output.prof
# مرورگر باز میشود با نمودار interactive
line_profiler – پروفایل خط به خط
pip install line_profiler
# اضافه کردن @profile به تابع
@profile
def slow_function():
total = 0
for i in range(1_000_000):
total += i ** 2
return total
slow_function()
kernprof -lv script.py
# Line # Hits Time Per Hit % Time Line
# 2 1 2 2.0 0.0 def slow_function():
# 3 1 1 1.0 0.0 total = 0
# 4 1000001 450000 0.5 45.0 for i in range(...):
# 5 1000000 550000 0.6 55.0 total += i ** 2
memory_profiler
pip install memory_profiler
from memory_profiler import profile
@profile
def memory_heavy():
big_list = [i for i in range(10_000_000)]
big_set = set(big_list)
return len(big_set)
memory_heavy()
python -m memory_profiler script.py
# Line Mem usage Increment Line Contents
# 3 38.4 MiB 0.0 MiB @profile
# 4 38.4 MiB 0.0 MiB def memory_heavy():
# 5 423.5 MiB 385.1 MiB big_list = [...]
# 6 701.2 MiB 277.7 MiB big_set = set(big_list)
تکنیکهای بهینهسازی
۱. انتخاب درست ساختار داده
import time
# جستجو در list - O(n)
data_list = list(range(100_000))
start = time.perf_counter()
for _ in range(1000):
99_999 in data_list
print(f"list: {time.perf_counter() - start:.4f}s")
# جستجو در set - O(1)
data_set = set(data_list)
start = time.perf_counter()
for _ in range(1000):
99_999 in data_set
print(f"set: {time.perf_counter() - start:.4f}s")
# تفاوت 10000 برابری!
| عملیات | list | dict/set | deque |
|---|---|---|---|
| دسترسی به ایندکس | O(1) | – | O(n) |
| جستجو in | O(n) | O(1) | O(n) |
| append | O(1) | – | O(1) |
| insert(0) | O(n) | – | O(1) |
| pop(0) | O(n) | – | O(1) |
۲. List Comprehension بهجای حلقه
# کند
result = []
for i in range(1000):
if i % 2 == 0:
result.append(i ** 2)
# سریعتر (~30%)
result = [i ** 2 for i in range(1000) if i % 2 == 0]
# سریعتر برای aggregation
total = sum(i ** 2 for i in range(1000) if i % 2 == 0)
۳. تابعهای built-in
# کند
total = 0
for x in numbers:
total += x
# سریع - C implementation
total = sum(numbers)
# همچنین: max, min, any, all, sorted
# join بهجای +=
result = "".join(parts) # O(n)
# نه: result += part # O(n^2)
۴. lru_cache برای توابع pure
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# fibonacci(100) را در میلیثانیه محاسبه میکند
۵. متغیرهای محلی بهجای global
import math
# کند - دسترسی به global در حلقه
def slow():
result = []
for i in range(1_000_000):
result.append(math.sqrt(i))
return result
# سریعتر - lookup محلی
def fast():
sqrt = math.sqrt # local reference
result = []
append = result.append # local method reference
for i in range(1_000_000):
append(sqrt(i))
return result
۶. __slots__
class Normal:
def __init__(self, x, y):
self.x = x
self.y = y
class Slotted:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
# برای 1 میلیون شیء:
# Normal: ~64 MB
# Slotted: ~32 MB (نصف)
یادآوری Big O
| پیچیدگی | مثال | n=1M |
|---|---|---|
| O(1) | dict lookup | میلیثانیه |
| O(log n) | binary search | میلیثانیه |
| O(n) | list iteration | ثانیه |
| O(n log n) | sort | ثانیه |
| O(n²) | nested loop | ساعت |
| O(2^n) | recursion بدون کش | غیرممکن |
# O(n²) - بد
duplicates = []
for i, x in enumerate(items):
for j, y in enumerate(items):
if i != j and x == y and x not in duplicates:
duplicates.append(x)
# O(n) - خوب
from collections import Counter
counts = Counter(items)
duplicates = [k for k, v in counts.items() if v > 1]
NumPy – vectorization
import numpy as np
import time
# Python خالص
def slow_dot(a, b):
return sum(x*y for x, y in zip(a, b))
# NumPy - vectorized
def fast_dot(a, b):
return np.dot(a, b)
a = list(range(1_000_000))
b = list(range(1_000_000))
start = time.perf_counter()
slow_dot(a, b)
print(f"Python: {time.perf_counter() - start:.4f}s") # ~0.15s
a_np = np.array(a)
b_np = np.array(b)
start = time.perf_counter()
fast_dot(a_np, b_np)
print(f"NumPy: {time.perf_counter() - start:.4f}s") # ~0.001s
# 100-500 برابر سریعتر!
پسوندهای C
ctypes – استفاده از کتابخانه C
import ctypes
# بارگذاری کتابخانه
libc = ctypes.CDLL("libc.so.6") # Linux
# libc = ctypes.CDLL("msvcrt.dll") # Windows
# فراخوانی printf
libc.printf(b"Hello from C!n")
Cython – کامپایل پایتون به C
pip install cython
# fast.pyx
def compute(int n):
cdef int i, total = 0
for i in range(n):
total += i * i
return total
# setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("fast.pyx"))
# بیلد: python setup.py build_ext --inplace
# استفاده:
import fast
fast.compute(1_000_000) # ~30 برابر سریعتر از پایتون خالص
Async vs Threading vs Multiprocessing
import time
# I/O-bound - 100 درخواست HTTP
# Sync: ~100s
# Threading(20): ~5s
# Asyncio: ~1s
# CPU-bound - محاسبه fibonacci
# Sync: ~10s
# Threading(4): ~10s (GIL!)
# Multiprocessing(4): ~3s
پروفایل کردن Django
pip install django-debug-toolbar django-silk
# settings.py
INSTALLED_APPS = [
...
"debug_toolbar",
"silk",
]
MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware",
"silk.middleware.SilkyMiddleware",
...
]
# ابزار silk صفحهای دارد که queryهای کند را نمایش میدهد
N+1 Query Problem
# بد - N+1 query
posts = Post.objects.all()
for post in posts:
print(post.author.name) # query جدا برای هر post!
# خوب - 1 query
posts = Post.objects.select_related("author").all()
for post in posts:
print(post.author.name) # بدون query اضافه
بهترین شیوهها
- قبل از بهینهسازی، اندازهگیری کنید
- گلوگاه را پیدا کنید (معمولاً 20% کد، 80% زمان)
- بعد از بهینهسازی، دوباره تست کنید
- کاهش زمان: ابتدا الگوریتم، سپس ساختار داده، در آخر زبان
- برای کارهای ریاضی، NumPy
- برای N+1 در ORM، select_related/prefetch_related
- کش (Redis، lru_cache) برای محاسبات تکراری
جمعبندی
- timeit برای benchmark خط واحد
- cProfile برای پروفایل کل برنامه
- line_profiler برای دیدن خط به خط
- memory_profiler برای حافظه
- تکنیکها: built-ins، set/dict، lru_cache، list comp
- NumPy برای محاسبات عددی
- Cython/C extensions برای حداکثر سرعت