وقتی پردازش یک جریان دادهٔ مهندسی با کمبود حافظه روبه‌رو می‌شود، نخست باید بدانیم چه چیزی را اندازه می‌گیریم. حجم محموله، حافظهٔ اشیای زنده و کل حافظهٔ فرایند یک عدد نیستند. در این درس، دو روش مصرف یک مجموعهٔ ساختگی از بلوک‌های داده را مقایسه می‌کنیم: نگهداری همهٔ بلوک‌ها در فهرست و پردازش مرحله‌ای آن‌ها. پیش از تفسیر حافظه، یکسان بودن ورودی و حاصل محاسبه بررسی می‌شود. هیچ حسگر واقعی، شبکه یا آزمون سرعتی در مثال حضور ندارد.

۱. محمولهٔ ثابت و معنای حافظهٔ ردگیری‌شده

بلوک‌های هم‌اندازه و تعریف دقیق محمولهٔ داده
تصویر مفهومی: بلوک‌های هم‌اندازه و تعریف دقیق محمولهٔ داده؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

دادهٔ تمرین ۱۲۸ بلوک دارد و هر بلوک دقیقاً ۸۱۹۲ بایت است. بلوک شمارهٔ صفر از بایت صفر و بلوک شمارهٔ یک از بایت یک ساخته می‌شود؛ این قاعده تا شمارهٔ ۱۲۷ ادامه دارد. این بایت‌ها محمولهٔ خامِ ساختگی‌اند و قالب اندازه‌گیری ولتاژ یا صدای واقعی را ادعا نمی‌کنند. حجم کل محموله برابر ۱۰۴۸۵۷۶ بایت است، اما اندازهٔ ساختار فهرست و سربار اشیای پایتون به آن محدود نمی‌شود.

مرجع tracemalloc موضوع ابزار را بلوک‌های تخصیص حافظهٔ ردگیری‌شدهٔ پایتون تعریف می‌کند. در این آزمایش، ردگیری را پس از بارگذاری ماژول‌ها آغاز می‌کنیم؛ بنابراین هزینهٔ قبلی آن مرحله در عددهای ما نیست. این اعداد را RSS، حافظهٔ GPU یا کل حافظهٔ فرایند نمی‌نامیم. حافظه‌ای که در محدودهٔ ردگیری قرار نگرفته نیز با این گزارش پوشش داده نمی‌شود.

برای ثبت منصفانهٔ قرارداد، شمار بلوک، اندازهٔ بلوک و قاعدهٔ تولید را کنار نتیجه نگه می‌داریم. جمع مقدار تمام بایت‌ها باید ۶۶۵۸۴۵۷۶ باشد؛ این مقدار با فرمول جمع شماره‌های صفر تا ۱۲۷، مستقل از حلقهٔ مصرف حساب می‌شود. شمار و جمع به تنهایی تضمین عمومی یکتایی محتوا نیستند؛ کد، هش محمولهٔ مرتب را نیز میان دو حالت مقایسه می‌کند و منبع تولید برای بازبینی در دسترس است.

۲. دو اجرای تازه با مرز اندازه‌گیری یکسان

دو اجرای مستقل برای سنجش تخصیص حافظهٔ پایتون
تصویر مفهومی: دو اجرای مستقل برای سنجش تخصیص حافظهٔ پایتون؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

هر حالت در یک فرایند فرزندِ تازه از همان مفسر اجرا می‌شود. رابط subprocess.run برای این اجرا و دریافت خروجی استفاده شده است؛ پوستهٔ فرمان و برنامهٔ خارجی دیگری وارد مسیر نمی‌شوند. هدف، جدا کردن تخصیص‌های دو حالت است، نه ادعای اینکه فرایند فرزند رایگان یا سریع‌تر است. راه‌اندازی فرایند پیش از بازهٔ سنجش قرار دارد.

در هر فرزند، start با عمق یک، ردگیری را آغاز می‌کند. سپس reset_peak مرز قلهٔ گزارش‌شده را تازه می‌کند؛ این تابع داده‌ها را آزاد نمی‌کند و جای پاک‌سازی اشیا نیست. get_traced_memory مقدار جاری و بیشترین مقدارِ ثبت‌شده در همان بازه را می‌دهد. در پایان، stop در finally اجرا می‌شود. برای بازبینی روش ثبت شرایط، مقالهٔ آزمایش تکرارپذیر سیگنال با پایتون زمینهٔ مناسبی فراهم می‌کند.

حالت نگهدارنده ابتدا فهرست تمام بلوک‌ها را می‌سازد؛ حالت جریانی همان مولد را مستقیم به تابع مصرف می‌دهد. آموزش رسمی مولدها رفتار توقف و ادامه با yield را شرح می‌دهد. مولد ما، همهٔ محموله را از پیش آماده نکرده است. با این حال، هنگام ساخت و جایگزینی بلوک بعدی ممکن است ارجاع قبلی هنوز زنده باشد؛ ادعای قلهٔ دقیقاً یک بلوک مطرح نمی‌کنیم.

۳. کد کامل و کنترل برابری کار انجام‌شده

بررسی برابری داده‌ها پیش از مقایسهٔ حافظه
تصویر مفهومی: بررسی برابری داده‌ها پیش از مقایسهٔ حافظه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

کد را با نام tracemalloc_sensors.py ذخیره و دستور python tracemalloc_sensors.py را اجرا کنید. برنامه خودش دو فرزند لازم را اجرا می‌کند و خروجی آن‌ها را می‌سنجد. اجرای ثبت‌شده با پایتون ۳٫۱۴٫۵ و کتابخانهٔ استاندارد انجام شده است. محدودیت سی ثانیه برای جلوگیری از معطل ماندن اجرای فرزند است؛ رسیدن به یک زمان خاص، معیار موفقیت یا مقایسهٔ سرعت این درس نیست.

"""Compare traced Python allocations for a fixed synthetic byte-block fixture, not RSS."""
import hashlib
import json
from pathlib import Path
import platform
import subprocess
import sys
import tracemalloc

BLOCK_COUNT = 128
BLOCK_BYTES = 8192


def sensor_blocks():
    for index in range(BLOCK_COUNT):
        yield bytes([index]) * BLOCK_BYTES


def summarize(blocks):
    count = total_bytes = checksum = 0
    digest = hashlib.sha256()
    for block in blocks:
        count += 1
        total_bytes += len(block)
        checksum += sum(block)
        digest.update(block)
    del block  # The fixed fixture is nonempty; avoid retaining the last loop item.
    return count, total_bytes, checksum, digest.hexdigest()


def measure(mode):
    if mode not in ('retained', 'streaming'):
        raise ValueError('retained or streaming mode required')
    if tracemalloc.is_tracing():
        tracemalloc.stop()
    tracemalloc.start(1)
    try:
        baseline_current, _ = tracemalloc.get_traced_memory()
        tracemalloc.reset_peak()
        held = list(sensor_blocks()) if mode == 'retained' else None
        result = summarize(held if held is not None else sensor_blocks())
        active_current, active_peak = tracemalloc.get_traced_memory()
        tracer_overhead = tracemalloc.get_tracemalloc_memory()
        if held is not None:
            held.clear()
        after_release_current, _ = tracemalloc.get_traced_memory()
    finally:
        tracemalloc.stop()
    return {'mode': mode, 'python': platform.python_version(),
            'measurement': 'traced_python_allocations_bytes_not_rss',
            'traceback_frames': 1, 'block_count': result[0],
            'payload_bytes': result[1], 'checksum_sum_of_byte_values': result[2],
            'payload_sha256': result[3], 'baseline_current_bytes': baseline_current,
            'active_current_bytes': active_current, 'active_peak_bytes': active_peak,
            'after_release_current_bytes': after_release_current,
            'tracer_internal_memory_bytes': tracer_overhead,
            'tracing_stopped': not tracemalloc.is_tracing()}


def run_example():
    script = str(Path(__file__).resolve())
    results = []
    for mode in ('retained', 'streaming'):
        child = subprocess.run([sys.executable, '-X', 'utf8', script, '--measure', mode],
                               capture_output=True, text=True, encoding='utf-8',
                               check=True, timeout=30)
        results.append(json.loads(child.stdout))
    retained, streaming = results
    expected_bytes = BLOCK_COUNT * BLOCK_BYTES
    expected_checksum = BLOCK_BYTES * BLOCK_COUNT * (BLOCK_COUNT - 1) // 2
    checks = {
        'two_separate_modes_measured': [r['mode'] for r in results] == ['retained', 'streaming'],
        'child_python_versions_match_parent': all(r['python'] == platform.python_version() for r in results),
        'both_counts_match_finite_fixture': all(r['block_count'] == BLOCK_COUNT for r in results),
        'both_byte_counts_match_independent_formula': all(r['payload_bytes'] == expected_bytes for r in results),
        'both_checksums_match_independent_formula': all(r['checksum_sum_of_byte_values'] == expected_checksum for r in results),
        'payload_hashes_match_across_modes': retained['payload_sha256'] == streaming['payload_sha256'],
        'both_peaks_cover_current_traced_size': all(r['active_peak_bytes'] >= r['active_current_bytes'] for r in results),
        'retained_current_larger_in_observed_run': retained['active_current_bytes'] > streaming['active_current_bytes'],
        'retained_peak_larger_in_observed_run': retained['active_peak_bytes'] > streaming['active_peak_bytes'],
        'retained_release_reduces_traced_current': retained['after_release_current_bytes'] < retained['active_current_bytes'],
        'fresh_sessions_begin_with_zero_traced_current': all(r['baseline_current_bytes'] == 0 for r in results),
        'both_sessions_stopped_tracing': all(r['tracing_stopped'] for r in results),
    }
    assert all(checks.values()), checks
    return {'python': platform.python_version(), 'isolation': 'one_fresh_subprocess_per_mode',
            'fixture': {'blocks': BLOCK_COUNT, 'bytes_per_block': BLOCK_BYTES,
                        'byte_value_rule': 'block index0through127 repeated8192times'},
            'expected_payload_bytes': expected_bytes, 'expected_checksum': expected_checksum,
            'results': results, 'checks': checks,
            'rss_gpu_total_process_memory_measured': False,
            'execution_speed_benchmark': False}


if __name__ == '__main__':
    if len(sys.argv) == 3 and sys.argv[1] == '--measure':
        output = measure(sys.argv[2])
    else:
        output = run_example()
    print(json.dumps(output, indent=2))

هر دو حالت، بلوک‌ها را با تابع مصرف یکسان بررسی می‌کنند. شمار، مجموع اندازه‌ها، جمع مقدار بایت‌ها و هش محموله باید سازگار باشند. اگر روشی بخشی از داده را اصلاً نخوانده باشد، مقایسهٔ حافظهٔ کمتر آن پاسخ مسئلهٔ ما نیست. دوازده بررسیِ نام‌دار شامل این قراردادها، رابطهٔ مقدار جاری و قله، کاهش مقدار جاری پس از رها کردن بلوک‌ها و پایان ردگیری هستند.

در حالت نگهدارنده، فهرست تا ثبت اندازهٔ فعال زنده می‌ماند و سپس clear می‌شود. تابع مصرف خروجی کوچکِ خلاصه را نگه می‌دارد و بلوک آخرِ حلقه را نیز حفظ نمی‌کند. برای جزئیات طراحی مصرف مرحله‌ای، آموزش مولدها و بسته‌بندی دادهٔ حسگر را بخوانید. اینجا تمرکز بر مشاهدهٔ طول عمر ارجاع‌هاست؛ صرف استفاده از نام generator در بخشی از برنامه، کل زنجیره را جریانی نمی‌کند.

۴. تفسیر قله و مقدار جاری در همین اجرا

تفاوت دادهٔ نگهداری‌شده با پردازش و آزاد کردن بلوک‌ها
تصویر مفهومی: تفاوت دادهٔ نگهداری‌شده با پردازش و آزاد کردن بلوک‌ها؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در گزارش ثبت‌شده، قلهٔ حالت نگهدارنده ۱۰۵۴۰۸۸ بایت و قلهٔ حالت جریانی ۱۶۶۹۰ بایت است. این تفاوت در اجرای حاضر واقعاً اندازه‌گیری شده و با نگهداری هم‌زمان همهٔ بلوک‌ها سازگار است. آن را ضریب بهبود جهانی، سقف ظرفیت سخت‌افزار یا نتیجهٔ اندازه‌گیری سرعت معرفی نمی‌کنیم. تغییر مفسر، ساختار داده یا محل ثبت می‌تواند عددها را تغییر دهد.

{
  "python": "3.14.5",
  "isolation": "one_fresh_subprocess_per_mode",
  "fixture": {
    "blocks": 128,
    "bytes_per_block": 8192,
    "byte_value_rule": "block index0through127 repeated8192times"
  },
  "expected_payload_bytes": 1048576,
  "expected_checksum": 66584576,
  "results": [
    {
      "mode": "retained",
      "python": "3.14.5",
      "measurement": "traced_python_allocations_bytes_not_rss",
      "traceback_frames": 1,
      "block_count": 128,
      "payload_bytes": 1048576,
      "checksum_sum_of_byte_values": 66584576,
      "payload_sha256": "523da049e240ecd3fcfd4f176c60a91597b5e38b3ea2932e9da1d3b548553195",
      "baseline_current_bytes": 0,
      "active_current_bytes": 1053985,
      "active_peak_bytes": 1054088,
      "after_release_current_bytes": 161,
      "tracer_internal_memory_bytes": 8976,
      "tracing_stopped": true
    },
    {
      "mode": "streaming",
      "python": "3.14.5",
      "measurement": "traced_python_allocations_bytes_not_rss",
      "traceback_frames": 1,
      "block_count": 128,
      "payload_bytes": 1048576,
      "checksum_sum_of_byte_values": 66584576,
      "payload_sha256": "523da049e240ecd3fcfd4f176c60a91597b5e38b3ea2932e9da1d3b548553195",
      "baseline_current_bytes": 0,
      "active_current_bytes": 105,
      "active_peak_bytes": 16690,
      "after_release_current_bytes": 105,
      "tracer_internal_memory_bytes": 848,
      "tracing_stopped": true
    }
  ],
  "checks": {
    "two_separate_modes_measured": true,
    "child_python_versions_match_parent": true,
    "both_counts_match_finite_fixture": true,
    "both_byte_counts_match_independent_formula": true,
    "both_checksums_match_independent_formula": true,
    "payload_hashes_match_across_modes": true,
    "both_peaks_cover_current_traced_size": true,
    "retained_current_larger_in_observed_run": true,
    "retained_peak_larger_in_observed_run": true,
    "retained_release_reduces_traced_current": true,
    "fresh_sessions_begin_with_zero_traced_current": true,
    "both_sessions_stopped_tracing": true
  },
  "rss_gpu_total_process_memory_measured": false,
  "execution_speed_benchmark": false
}

مقدار جاریِ نگهدارنده، پیش از clear برابر ۱۰۵۳۹۸۵ و پس از آن ۱۶۱ بایت ثبت شده است. پس از رها کردن ارجاع‌ها، هنوز خروجی خلاصه و بعضی اشیای کمکی وجود دارند؛ انتظار صفر شدن همه چیز نداریم. در حالت جریانی مقدار جاریِ ثبت‌شده پس از مصرف ۱۰۵ بایت است، ولی قلهٔ آن بزرگ‌تر است. بنابراین نگاه کردن فقط به انتهای تابع، تخصیص‌های موقتیِ طول اجرای کار را نشان نمی‌دهد.

کد حافظهٔ داخلی ابزار برای نگهداری ردها را نیز جدا ثبت می‌کند. کوچک شدن تخصیص جاریِ ردگیری‌شده، به‌خودی‌خود کاهش RSS یا بازگرداندن همان تعداد بایت به سیستم‌عامل را ثابت نمی‌کند. همچنین این آزمایش برای تشخیص قطعی نشت حافظه طراحی نشده است. برای چنین نتیجه‌ای باید طول عمر مورد انتظار داده و رفتار تکرارشوندهٔ مسیر واقعی بررسی شوند؛ یک قلهٔ بزرگ به تنهایی اثبات نشت نیست.

۵. چهار تمرین پاسخ‌دار و تصمیم بعدی

آزاد شدن ارجاع‌ها با کم شدن کل حافظهٔ فرایند یکسان نیست
تصویر مفهومی: آزاد شدن ارجاع‌ها با کم شدن کل حافظهٔ فرایند یکسان نیست؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تمرین اول: چرا پیش از مقایسهٔ حافظه، شمار و حاصل محاسبه را می‌سنجیم؟ پاسخ: چون کم شدن کار انجام‌شده ممکن است ظاهراً حافظه را کاهش دهد؛ هر دو روش باید همین محموله را پردازش کنند. تمرین دوم: آیا reset_peak بلوک‌های نگهداری‌شده را پاک می‌کند؟ پاسخ: خیر؛ فقط قلهٔ ثبت‌شده را به مقدار جاری تغییر می‌دهد و آزاد شدن داده به طول عمر ارجاع‌ها مربوط است.

تمرین سوم: اگر مقدار جاریِ پایان کار کوچک باشد، آیا قله هم حتماً کوچک بوده است؟ پاسخ: خیر؛ در حالت جریانیِ همین مثال، مقدار جاری و قله متفاوت‌اند. تمرین چهارم: آیا این نتیجه می‌گوید مدل هوش مصنوعی روی GPU به همین نسبت حافظهٔ کمتری می‌خواهد؟ پاسخ: خیر؛ هیچ GPU، مدل یا ابزار سنجش حافظهٔ آن در کد نیست و تعمیم چنین نسبتی پشتیبانی ندارد.

برای ادامه، ابتدا تعیین کنید آیا مصرف‌کنندهٔ واقعی باید بلوک‌ها را نگه دارد یا فقط خلاصه‌ای از آن‌ها را می‌خواهد؛ سپس همان نیاز را با ابزار متناسب اندازه بگیرید. منابع در ۲۴ مهر ۱۴۰۵ خوانده شده‌اند؛ مستندات ۳٫۱۴٫۸ و اجرای واقعی ۳٫۱۴٫۵ جدا ثبت شده‌اند. این مطلب یک آزمایش آموزشی تألیفی با دادهٔ محدود است و خبر پژوهشی تازه یا ارزیابی عملکرد محصول نیست.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.