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

این مقاله سیر نسخه‌به‌نسخه را دنبال می‌کند، الگوریتم‌های پشت هر جهش بزرگ را کالبدشکافی می‌کند و نشان می‌دهد که معماری مدرن GC گو چه تأثیری بر سرویس‌های حساس به تأخیر دارد.

یک قانون ثابت در طراحی

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

زیرساخت اولیه: استراتژی مارک‌اند‌اسویپ

رن‌تایم گو استراتژی مارک‌اند‌اسویپ (Mark-and-Sweep) را به‌کار می‌گیرد و به‌جای شمارش ارجاعات (Reference Counting) یا کالکترهای کپی‌کننده (Copying Collector) می‌رود. این کالکتر دقیق، همزمان و غیرنسل‌محور طراحی شده و تحت هیچ شرایطی عملیات فشرده‌سازی هیپ (Heap Compaction) را اجرا نمی‌کند.

فرایند مارک‌کردن از ریشه‌ها آغاز می‌شود: متغیرهای گلوبال و استک تمام گوروتین‌ها. کالکتر تمام اشیاء قابل دسترسی را پیمایش کرده و وضعیت حیات (Liveness) آن‌ها را در یک بول‌نمایی (Bitmap) اختصاصی در بخش مدیریت‌شدهٔ هیپ ثبت می‌کند. اشیاء هیپ که پس از پیمایش مارک‌نشده باقی بمانند به‌عنوان زباله شناسایی شده و در فهرست‌های آزاد (Free Lists) متعلق به هر پردازندهٔ منطقی (P) برای استفادهٔ مجدد قرار می‌گیرند.

چالش اصلی در این طراحی، پدیدهٔ جهش (Mutation) بود. اگر کد کاربر همزمان با پیمایش گراف پوینترها اجرا شود، یک عملیات نوشتن می‌تواند یک شیء زنده را از دید مارکر پنهان کند و منجر به حذف اشتباه آن شود. اولین پاسخ تیم گو به این مشکل، راه‌حلی ساده اما مطلق بود: توقف کامل اجرای کد کاربر تا زمان پایان کالکشن.

نسخه‌های ابتدایی: دورهٔ توقف مطلق

پیاده‌سازی اولیهٔ GC در نسخه‌های 1.0 تا 1.4، نسخهٔ کلاسیک مارک‌اند‌اسویپ بود که کل فرایند را در حالت توقف مطلق (Stop-The-World) نگه می‌داشت. هنگامی که کالکشن آغاز می‌شد، تمام گوروتین‌های کاربر متوقف می‌شدند. تنها یک ترد یا تعداد محدودی از تردها وظیفهٔ «مارک‌کردن زنده‌ها و اسویپ‌کردن مرده‌ها» را بر عهده داشتند و تنها پس از اتمام این فرایند، اجرای اپلیکیشن از سر گرفته می‌شد.

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

هزینهٔ این طراحی مستقیماً در زمان‌های توقف نمودار می‌شد: هرچه هیپ بزرگ‌تر بود، اشیاء بیشتری نیاز به اسکن داشتند و توقف مطلق طولانی‌تر می‌شد. زمان‌های اندازه‌گیری‌شده در بازهٔ ده‌ها تا صدها میلی‌ثانیه قرار می‌گرفتند. برای یک وب‌سرویس که در حال پردازش درخواست‌های کاربر است، تأخیر دم (Tail Latency) می‌توانست در هر لحظه با یک چرخهٔ جمع‌آوری زباله به پیک‌های صد میلی‌ثانیه‌ای بکشد. این نقطهٔ درد، دستورکار مهندسان گو برای دههٔ بعد را تعیین کرد. برای درک عمیق‌تر مفاهیم پایه‌ای مدیریت حافظه در رن‌تایم‌ها، مطالعهٔ منابع عمومی‌تر مانند تاریخچه و مفاهیم جمع‌آوری زباله می‌تواند راهگشا باشد.

نقطهٔ عطف: گو 1.5 و مارکینگ همزمان

انتشار گو 1.5 در سال 2015، نقطهٔ عطف تاریخچهٔ GC این زبان بود. بازنویسی بنیادین توسط ریچارد هادسون و تیمش، فاز مارک‌کردن را کاملاً همزمان با اجرای کد کاربر امکان‌پذیر کرد. در این معماری جدید، کالکتر حین پیمایش هیپ فعالیت می‌کند، در حالی که گوروتین‌ها همچنان در حال اجرا، اختصاص حافظه و بازنویسی پوینترها هستند.

پیشران این تغییر، الگوریتم مارکینگ سه‌رنگی (Tri-Color Marking) بود که به طور مفهومی هر شیء در هیپ را به یکی از سه وضعیت طبقه‌بندی می‌کند:

  • سفید (White): دست‌نرسیده و کاندیدای زباله
  • خاکستری (Grey): دست‌رس‌یافته اما ارجاعات فرزندانش هنوز پیمایش نشده‌اند
  • سیاه (Black): کاملاً دست‌رس‌یافته و اسکن‌شده

کالکتر گره‌های خاکستری را پردازش می‌کند، اشیاء هدف آن‌ها را خاکستری کرده و پس از تکمیل، وضعیت گرهٔ فعلی را به سیاه تغییر می‌دهد. برای حفظ سازگاری (Invariants) در حین اجرای همزمان کالکتر و مواتورها (Mutators)، گو 1.5 دیوارهٔ نوشتن سبک استایل دیجکسترا (Dijkstra-style Write Barrier) را معرفی کرد. این مکانیسم به‌روزرسانی‌های پوینتر را رهگیری می‌کند و تضمین می‌کند هر ارجاعِ جدید پیش از ذخیرهٔ نهایی، به‌درستی خاکستری شود:

writePointer(slot, ptr):
    shade(ptr)       # شیء جدید را خاکستری کن
    *slot = ptr      # ذخیرهٔ نهایی پوینتر

اشیاء جدیدی که در حین مارکینگ اختصاص می‌یابند به‌صورت پیش‌فرض سیاه در نظر گرفته می‌شوند و از قلم‌انداز شدن (Missing) جلوگیری می‌کنند. هدف اعلام‌شدهٔ این بازنویسی، کاهش زمان توقف مطلق به زیر 10 میلی‌ثانیه بود. اما شاید مهم‌تر از عدد، تغییر فلسفهٔ طراحی پروژه باشد. اسناد فنی و سخنرانی‌های رسمی تیم گو بارها بر این نکته تأکید کرده‌اند که اولویت اصلی، تأخیرِ پیش‌بینی‌پذیر و پایین است. این یعنی تیم گو حاضر است اندکی از بهره‌وری نهایی (Throughput) و حداکثر مصرف حافظه (Peak Memory) بکاهد تا سرویس‌ها در محیط‌های حساس به زمان، پایدار پیش بروند.

چرخهٔ مدرن GC و تأثیر آن بر سرویس‌های حساس

با ورود مارکینگ همزمان، چرخهٔ استاندارد GC گو به الگویی پایدار تبدیل شد که هنوز هم ادامه دارد: کمک‌مارک (Mark Assist)، مارکینگ همزمان، توقف نهایی برای تکمیل فرایند (Final Mark STW) و اسویپ همزمان. تنها پنجره‌های توقف مطلق به حداقل ممکن رسانده شدند و معماریٔ رن‌تایم گو به سمت سرویس‌های توزیع‌شده با تأخیر پایین حرکت کرد.

امروزه این تکامل مهندسی اجازه می‌دهد تا پلتفرم‌های مبتنی بر گو حتی در حجم‌های ترافیک بسیار بالا، با ثباتِ زمانیِ پاسخ‌دهی (Consistency in Latency) عمل کنند و از پدیده‌های لرزشی در شبکه‌های میکروسرویس جلوگیری شود. با رشد معماری‌های ابری و نیاز به پاسخ‌دهی بلادرنگ، این الگوریتم‌های همزمان نه تنها یک بهبود فنی، بلکه زیربنای قابل‌اتکای سرویس‌های مقیاس‌پذیر جهانی خواهند ماند.