Faragiri

همهٔ زبان‌ها

آموزش حافظه در برنامهکد چطور اجرا می‌شود

انبار یک فروشگاه، یک دیکشنری با یک لیست داخلش. کپی سطحی لیست داخلی را کپی نمی‌کند، و همین است که هر دو نسخه با هم عوض می‌شوند.

۴بخش
۳دقیقه خواندن
۴عمق

یک انتساب

چیزی که زیر یک نام نشسته و در ویرایشگر دیده نمی‌شود

آنچه نوشته‌اید یک نام است؛ آنچه حافظه می‌گیرد سه چیز جداست که هیچ‌کدام در آن سطر پیدا نیستند.

  • anbarیک نام، که فقط یک آدرس را نگه می‌دارد
چیزی که در کد نوشته‌اید
  • دیکشنری بیرونیتنها چیزی که کپی سطحی از نو می‌سازد
  • لیست داخلیمشترک می‌ماند، و همین است که هر دو نسخه را با هم عوض می‌کند
  • خود عددهاهرکدام یک شی جدا با بیست‌وهشت بایت، بیرون از لیست

این نقشه یک دیکشنری با یک کلید است. با ده کلید، فقط لایه بالا ده برابر می‌شود و لایه پایین به تعداد کل عددها بزرگ می‌شود.

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

  • anbar = {"shirazi": [12000, 8000, 15000]}
  • poshtiban = dict(anbar)
  • poshtiban["shirazi"].append(9500)

حالا anbar برابر {'shirazi': [12000, 8000, 15000, 9500]} است، و poshtiban["shirazi"] is anbar["shirazi"] جواب می‌دهد True. dict() یک دیکشنری تازه ساخت، اما مقدارهای داخلش را کپی نکرد: کپی سطحی فقط لایه بیرونی را می‌سازد و ارجاع‌های داخل را همان‌طور برمی‌دارد. تنها چیزی که تازه است، خود دیکشنری بیرونی است.

copy.deepcopy(anbar) همان کار را تا ته پایین می‌رود و لیست داخلی را هم از نو می‌سازد؛ بعد از آن، افزودن به نسخه پشتیبان چیزی از انبار اصلی را عوض نمی‌کند. هزینه‌اش هم واقعی است، چون هر شی داخلی یک بار دیگر ساخته می‌شود.

اندازه‌ای که گزارش می‌شود، اندازه‌ای که مصرف می‌شود نیست

پایتون هر عدد را یک شی جدا نگه می‌دارد و لیست فقط ارجاع‌ها را در خود دارد. برای همین sys.getsizeof روی یک لیست، عددی می‌دهد که تمام ماجرا نیست: خود عددها جای دیگری نشسته‌اند و هرکدام بیست‌وهشت بایت‌اند. اگر می‌خواهید بدانید یک قطعه کد واقعاً چقدر گرفته، ابزارش tracemalloc است که بیشینه مصرف را در همان بازه اندازه می‌گیرد.

روی پایتون 3.14 و در تاریخ ۲۰۲۶-۰۹-۰۳، ساختن یک لیست از یک میلیون عدد بیشینه‌ای حدود ۳۸ مگابایت گرفت. همان جمع، وقتی به‌جای لیست با یک عبارت مولد نوشته شود، بیشینه‌ای برابر ۳۷۶ بایت دارد و جواب یکسانی می‌دهد. تفاوت در الگوریتم نیست؛ در این است که یکی همه سطرها را هم‌زمان زنده نگه می‌دارد و دیگری هر بار یکی را.

آزاد شدن با شمارش ارجاع اتفاق می‌افتد، نه با del

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

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

جایی که این درس گران تمام می‌شود، سرور یک گیگابایتی است

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

ابزار، و سالی که این حرف درست است

در سال ۲۰۲۶، tracemalloc در کتابخانه استاندارد است و جای حدس زدن از روی getsizeof را گرفته، چون تخصیص‌ها را در یک بازه مشخص می‌شمارد به‌جای اینکه اندازه یک شی را جدا بگوید. رایگان است و به شبکه هم نیازی ندارد، که برای کسی که نمی‌تواند هر بسته‌ای را نصب کند خودش یک ویژگی است. برای دیدن اینکه چه چیزی هنوز زنده مانده، gc.get_objects() در همان کتابخانه هست و در پایتون 3.14 گزارشش دقیق‌تر از نسخه‌های پیش از 3.12 است.

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

کنار این

بالای این