مقدمه Performance :واقعاً یعنی چه؟

 

بهینه‌سازی عملکرد یکی از چالش‌های اساسی در توسعه نرم‌افزار است که بسیاری از توسعه‌دهندگان آن را با "سرعت بیشتر" خلاصه می‌کنند. اما Performance در واقع مفهومی چندبعدی است که شامل زمان پاسخ‌دهی (Response Time)، توان عملیاتی (Throughput)، مصرف حافظه، بهره‌وری CPU و مقیاس‌پذیری می‌شود.

در Python، به دلیل ماهیت Interpreted بودن و GIL (Global Interpreter Lock)، بهینه‌سازی اهمیت ویژه‌ای دارد. اما قبل از هرگونه بهینه‌سازی، باید قاعده طلایی دونالد نات را به خاطر بسپاریم: "Premature optimization is the root of all evil"  ابتدا کد را صحیح بنویسید، سپس آن را بهینه کنید.

 

پیدا کردن گلوگاه با  Profiling

 

بهینه‌سازی بدون شناخت گلوگاه‌ها مانند تیراندازی در تاریکی است. Profiling فرآیند شناسایی بخش‌هایی از کد است که بیشترین منابع را مصرف می‌کنند.

 

 :cProfileابزار استاندارد Python

import cProfile
import pstats

def slow_function():
    total = 0
    for i in range(1000000):
        total += i ** 2
    return total


# اجرا با profiling
cProfile.run('slow_function()', 'output.prof' )

# تحلیل نتایج
stats = pstats.Stats('output.prof')
stats.sort_stats('cumulative')
stats.print_stats(10)

 

برای تحلیل دقیق‌تر، `line_profiler` زمان اجرای هر خط را نشان می‌دهد:

from line_profiler import LineProfiler

@profile
def process_data(data):
    result = []
    for item in data:
        processed = item ** 2  # این خط چقدر زمان می‌برد؟
        result.append(processed)
    return result

 

 :memory_profiler ردیابی مصرف حافظه

from memory_profiler import profile

@profile
def memory_hungry_function():
    big_list = [i for i in range(10000000)]
    return sum(big_list)

 


 

تفاوت CPU-bound و I/O-bound

 

شناخت نوع مشکل عملکردی، کلید انتخاب استراتژی درست است.

 :CPU-boundبرنامه‌هایی که بیشتر در حال محاسبه هستند (پردازش تصویر، محاسبات علمی، الگوریتم‌های پیچیده). در اینجا CPU گلوگاه است.

 

 :I/O-boundبرنامه‌هایی که بیشتر منتظر عملیات ورودی/خروجی می‌مانند (درخواست‌های شبکه، خواندن/نوشتن فایل، دیتابیس). در اینجا CPU بیکار است و منتظر داده می‌ماند.

import time

# CPU-bound
def cpu_intensive():
    return sum(i**2 for i in range(10000000))

# I/O-bound
def io_intensive():
    time.sleep(5)  # شبیه‌سازی network request
    return "data"

 


 

بهینه‌سازی الگوریتم و پیچیدگی زمانی

 

گاهی بهترین بهینه‌سازی، تغییر الگوریتم است.

کاهش پیچیدگی زمانی از O(n²) به O(n log n) می‌تواند تأثیر چشمگیری داشته باشد.

# روش کند: O(n²)
def find_duplicates_slow(data):
    duplicates = []
    for i in range(len(data)):
        for j in range(i+1, len(data)):
            if data[i] == data[j] and data[i] not in duplicates:
                duplicates.append(data[i])
    return duplicates

# روش بهینه: O(n)
def find_duplicates_fast(data):
    seen = set()
    duplicates = set()
    for item in data:
        if item in seen:
            duplicates.add(item)
        seen.add(item)
    return list(duplicates)

 

استفاده از ساختارهای داده مناسب نیز حیاتی است:

·       برای جستجوی سریع:`set` یا `dict` به جای `list`

·       برای صف `collections.deque` :به جای `list`

·       برای شمارش: `collections.Counter`

 


 

مدیریت حافظه و Memory Profiling

 

Python  مدیریت حافظه را به صورت خودکار انجام می‌دهد، اما می‌توانیم بهینه‌تر عمل کنیم:

# بد: ایجاد لیست کامل در حافظه
def process_large_file_bad(filename):
    with open(filename) as f:
        lines = f.readlines()  # همه فایل در RAM
    return [line.upper() for line in lines]

# خوب: استفاده از Generator
def process_large_file_good(filename):
    with open(filename) as f:
        for line in f:  # یک خط در هر بار
            yield line.upper()

# استفاده از __slots__ برای کاهش حافظه
class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

 


 

Asyncio و برنامه‌نویسی همزمان

 

برای برنامه‌های I/O-bound، `asyncio` می‌تواند معجزه کند:

import asyncio
import aiohttp

# بدون async: Sequential
def fetch_urls_sync(urls):
    results = []
    for url in urls:
        response = requests.get(url)
        results.append(response.text)
    return results

# با async: Concurrent
async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def fetch_urls_async(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        return await asyncio.gather(*tasks)

# اجرا
urls = ['http://example.com'] * 10
asyncio.run(fetch_urls_async(urls))

 

برای 10 درخواست که هرکدام 1 ثانیه طول می‌کشد:

·       روش Sync: ~10 ثانیه

·       روش Async: ~1 ثانیه

 


 

Threading  در برابر Multiprocessing

 

انتخاب بین Threading و Multiprocessing به نوع مشکل بستگی دارد:

import threading
import multiprocessing
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# Threading برای I/O-bound
def download_file(url):
    # عملیات شبکه
    pass

with ThreadPoolExecutor(max_workers=10) as executor:
    urls = ['url1', 'url2', 'url3']
    executor.map(download_file, urls)

# Multiprocessing برای CPU-bound
def cpu_task(n):
    return sum(i**2 for i in range(n))

with ProcessPoolExecutor(max_workers=4) as executor:
    numbers = [1000000, 2000000, 3000000]
    results = executor.map(cpu_task, numbers)

 

نکته مهم: به دلیل GIL، Threading در Python نمی‌تواند محاسبات CPU را واقعاً موازی کند، اما Multiprocessing می‌تواند.

 


 

Caching و  Memoization

 

ذخیره نتایج محاسبات گران‌قیمت می‌تواند عملکرد را چند برابر کند:

from functools import lru_cache
import time

# بدون cache
def fibonacci_slow(n):
    if n < 2:
        return n
    return fibonacci_slow(n-1) + fibonacci_slow(n-2)

# با cache
@lru_cache(maxsize=128)
def fibonacci_fast(n):
    if n < 2:
        return n
    return fibonacci_fast(n-1) + fibonacci_fast(n-2)

# تست
start = time.time()
print(fibonacci_slow(35))  # ~5 ثانیه
print(f"بدون cache: {time.time() - start:.2f}s")

start = time.time()
print(fibonacci_fast(35))  # ~0.0001 ثانیه
print(f"با cache: {time.time() - start:.2f}s")

# Cache سفارشی با TTL
from cachetools import TTLCache
cache = TTLCache(maxsize=100, ttl=300)  # 5 دقیقه

def expensive_api_call(param):
    if param in cache:
        return cache[param]
    result = # ... فراخوانی واقعی
    cache[param] = result
    return result

 


 

بهینه‌سازی Query های دیتابیس

 

دیتابیس اغلب بزرگترین گلوگاه در اپلیکیشن‌های وب است:

# بد: N+1 Query Problem
def get_users_with_posts_bad():
    users = User.query.all()
    for user in users:
        posts = user.posts.all()  # یک query برای هر user!

# خوب: Eager Loading
def get_users_with_posts_good():
    users = User.query.options(
        joinedload(User.posts)
    ).all()  # فقط یک query

# استفاده از Index
# در مدل:
class User(db.Model):
    email = db.Column(db.String, index=True)  # سرعت جستجو

# Bulk Operations
# بد
for item in data:
    db.session.add(Item(name=item))
    db.session.commit()  # هزاران commit!

# خوب
items = [Item(name=item) for item in data]
db.session.bulk_save_objects(items)
db.session.commit()  # یک commit

 


 

یک مثال واقعی: بهینه‌سازی یک برنامه کند

 

فرض کنید برنامه‌ای داریم که فایل CSV بزرگ را پردازش می‌کند:

# نسخه اولیه (کند)
def process_csv_v1(filename):
    data = []
    with open(filename) as f:
        for line in f:
            parts = line.strip().split(',')
            if int(parts[2]) > 1000:
                data.append({
                    'name': parts[0],
                    'value': int(parts[2]) ** 2
                })
    return data

# نسخه بهینه‌شده
import csv
from itertools import islice

def process_csv_v2(filename, chunk_size=10000):
    with open(filename) as f:
        reader = csv.reader(f)
        next(reader)  # skip header
        
        while True:
            chunk = list(islice(reader, chunk_size))
            if not chunk:
                break
            
            # پردازش موازی chunk
            yield from (
                {'name': row[0], 'value': int(row[2]) ** 2}
                for row in chunk
                if int(row[2]) > 1000
            )

# بهینه‌تری با pandas و NumPy
import pandas as pd

def process_csv_v3(filename):
    df = pd.read_csv(filename, usecols=['name', 'value'])
    df = df[df['value'] > 1000]
    df['value'] = df['value'] ** 2
    return df.to_dict('records')

 

نتایج بنچمارک (برای فایل 1 میلیون سطری):

·        45 :v1  ثانیه، 800MB حافظه

·        12 :v2 ثانیه، 50MB حافظه

·        3 :v3  ثانیه، 200MB حافظه

 


 

اشتباهات رایج در Performance Optimization

 

·       بهینه‌سازی زودهنگام: قبل از اندازه‌گیری بهینه‌سازی نکنید

·       نادیده گرفتن Profiling :حدس نزنید، اندازه بگیرید

·       بهینه‌سازی بخش اشتباه: 90% زمان در 10% کد صرف می‌شود

·       فدا کردن Readability :کد باید قابل نگهداری بماند

·       فراموش کردن :Caching ساده‌ترین و مؤثرترین روش

·       استفاده نادرست از :Threadingبرای CPU-bound کار نمی‌کند

·       بی‌توجهی به :Memory Leaksدر برنامه‌های Long-running مهم است

 

# اشتباه رایج: String concatenation در حلقه
result = ""
for item in large_list:
    result += str(item)  # هر بار یک string جدید!

# درست
result = "".join(str(item) for item in large_list)

 

 


 

جمع‌بندی و چک‌لیست نهایی

 

بهینه‌سازی Performance یک فرآیند تکراری و داده‌محور است. چک‌لیست زیر را دنبال کنید:

 

چک‌لیست بهینه‌سازی:

·       اندازه‌گیری کنید: با cProfile و memory_profiler شروع کنید

·       گلوگاه را شناسایی کنیدCPU-bound :یا I/O-bound؟

·       الگوریتم را بررسی کنید: آیا می‌توان پیچیدگی را کاهش داد؟

·       ساختارهای داده مناسب: set, dict, deque

·       از Generator استفاده کنید: برای داده‌های بزرگ

·       Caching اضافه کنید lru_cache :برای توابع خالص

·       Async  را امتحان کنید: برای I/O-bound tasks

·       Multiprocessing: برای  CPU-bound tasks

·       دیتابیس را بهینه کنید:Index, Eager Loading, Bulk Operations

·       دوباره اندازه‌گیری کنید: تأثیر را تأیید کنید

 

کلام آخر:

بهینه‌سازی Performance هنری است که با تجربه بهتر می‌شود. ابزارها را یاد بگیرید، اما همیشه با اندازه‌گیری شروع کنید و نتایج را تأیید کنید.