اگر از کندی مرورگر در هنگام باز کردن صفحات سنگین رنج میبرید، احتمالاً با اثرات مدیریت حافظه در سطح سیستم مواجه شدهاید. گوگل اکنون با یک تغییر معماری جسورانه، زمان پاکسازی حافظه در رشته اصلی (Main Thread) کروم را بهطور میانگین ۴۲٪ کاهش داده است. این بهبود نتیجه انتقال Oilpan — یک کتابخانه تخصصی برای مدیریت حافظه در زبان C++ — از موتور رندرینگ Blink به موتور V8 است. هدف گوگل این است که مدیریت حافظه در C++ را برای تمام توسعهدهندگانی که از V8 استفاده میکنند و بهطور کلی برای تعداد بیشتری از توسعهدهندگان C++، در دسترس و بهینه کند.
مدیریت حافظه در مرورگر شبیه به یک بازی تعادلی پیچیده و پر هرجومرج است. در حالی که جاوااسکریپت حافظه خودش (Heap) را مدیریت میکند، بخش بزرگی از مرورگر و موتور رندرینگ Blink — جایی که V8 در آن جاسازی شده است — با زبان C++ نوشته شدهاند. جاوااسکریپت برای تعامل با DOM استفاده میشود و سپس این DOM توسط خط لوله رندرینگ پردازش میگردد. از آنجا که گراف اشیاء C++ در اطراف DOM بهشدت با اشیاء جاوااسکریپت گره خوردهاند، برخورد با آنها بهعنوان دو حافظه (Heap) مجزا، هزینهی پردازشی و سربار بسیار بالایی ایجاد میکند.
همانطور که در تحلیلهای پیشین ما دربارهی بهینهسازی موتورهای جاوااسکریپت اشاره کردیم، کاهش تداخل بین زبانهای مختلف کلید افزایش سرعت است. تیم کرومیوم برای حل این مشکل، Oilpan را توسعه داد تا گراف پیچیده و درهمتنیده C++/JavaScript را بهعنوان یک حافظه واحد ببیند. Oilpan در واقع یک جمعآورنده زباله است که به زبان C++ نوشته شده تا حافظه C++ را مدیریت کند و از طریق «ردیابی بینمولفهای» (Cross-component tracing) به V8 متصل میشود. این قابلیت به سیستم اجازه میدهد تا اشارهگرها را در مرزهای بین دو زبان ردیابی کند، بدون اینکه ردی از این که کدام اشیاء هنوز در حال استفاده هستند، گم شود. طبق گزارش v8.dev در ۱۴ سپتامبر ۲۰۲۶، این معماری اکنون برای اکوسیستم گستردهتر توسعهدهندگان عمومی شده است.
سازوکار Oilpan
سیستم Oilpan از یک مدل جمعآوری زباله (Garbage Collector) از نوع Mark-Sweep استفاده میکند. این فرآیند به دو مرحله متمایز تقسیم میشود:
- علامتگذاری (Marking): سیستم حافظه مدیریتشده را برای یافتن اشیاء زنده اسکن میکند. این مرحله بهعنوان پیمایش یک گراف دیده میشود که در آن اشیاء «گره» و اشارهگرهای بین اشیاء «یال» هستند. پیمایش از ریشهها (Roots) شروع میشود که شامل رجیسترها، متغیرهای سراسری (Globals) و پشته اجرای نیتیو (Native execution stack) است.
- پاکسازی (Sweeping): سیستم اشیاء مرده را شناسایی میکند؛ یعنی اشیائی که در طول مرحله علامتگذاری غیرقابل دسترس بودهاند و بیت علامتگذاری ندارند. سپس حافظه آنها بازپس گرفته میشود. این حافظه یا به سیستمعامل بازگردانده میشود و یا برای تخصیصهای بعدی در دسترس قرار میگیرد.
برخلاف جاوااسکریپت، اشیاء C++ دارای نوع دادهای استاتیک (Statically typed) هستند و نمیتوانند نمایش خود را در زمان اجرا تغییر دهند. Oilpan از این ویژگی با استفاده از یک الگوی بازدیدکننده (Visitor pattern) از طریق متد Trace بهره میبرد. برای مثال، یک کلاس LinkedNode که از GarbageCollected<LinkedNode> ارثبری میکند، از متد Trace(Visitor* visitor) استفاده میکند تا به جمعآورنده درباره اشارهگرهای Member<LinkedNode> خود اطلاع دهد. این سازوکار باعث میشود جمعآورنده دقیقاً بداند اشارهگرها در کجای اشیاء مدیریتشده قرار دارند و نیازی به حدس زدن نباشد.
یکی از چالشهای اصلی، مدیریت پشته اجرا (Execution Stack) است. Oilpan هیچ لایه انتزاعی (Abstraction) برای کار با پشته اضافه نمیکند. در عوض، برای یافتن اشارهگرها به حافظه مدیریتشده در هنگام پردازش ریشهها، بر «اسکن محافظهکارانه پشته» (Conservative stack scanning) تکیه میکند. این روش با پیمایش کلمه به کلمه پشته و تفسیر آن کلمات بهعنوان اشارهگر عمل میکند.
این رویکرد با انتقال هزینه پردازشی به مرحله جمعآوری زباله، از ایجاد جریمههای عملکردی در حین اجرای عادی برنامه جلوگیری میکند. برای بهینهسازی بیشتر، Oilpan در حالت ادغامشده با رندر، سعی میکند جمعآوری زباله را تا زمانی به تعویق بیندازد که تضمین شود هیچ پشتهی مهمی وجود ندارد. از آنجا که وب مبتنی بر رویداد است و اجرا توسط پردازش وظایف در حلقههای رویداد (Event loops) هدایت میشود، چنین فرصتهایی بسیار زیاد هستند.
مدیریت پیچیدگیهای C++
به دلیل کار در محیط عظیم کدبیس Blink که شامل حجم زیادی از کدهای قدیمی و بالغ است، Oilpan قابلیتهای پیشرفتهای را پشتیبانی میکند که اکثر جمعآورندههای حافظه استاندارد در مدیریت آنها با مشکل مواجه میشوند:
- وراثت: پشتیبانی از وراثت چندگانه از طریق Mixinها و ارجاعات به این Mixinها (که به عنوان اشارهگرهای داخلی یا Interior pointers شناخته میشوند).
- مدیریت چرخه حیات: توانایی فعال کردن جمعآوری زباله در حین اجرای سازندهها (Constructors).
- مدیریت ریشه: زنده نگه داشتن اشیاء از طریق حافظههای غیرمدیریتشده با استفاده از اشارهگرهای هوشمند Persistent که بهعنوان ریشه در نظر گرفته میشوند.
- مجموعهها: پشتیبانی از کانتینرهای متوالی (مانند vector) و کانتینرهای تداعیکننده (مانند set و map)، از جمله فشردهسازی حافظههای پشتیبان این مجموعهها.
- ارجاعات پیشرفته: پشتیبانی از ارجاعات ضعیف (Weak references)، بازگشتهای ضعیف (Weak callbacks) و Ephemeronها.
- پاکسازی: توابع بازگشتی نهاییکننده (Finalizer callbacks) که پیش از بازپسگیری هر شیء اجرا میشوند.
تکامل فرآیند پاکسازی
آزاد کردن حافظه در C++ به دلیل وجود «تخریبکنندهها» (Destructors) ریسکپذیر است. برای حفظ معنای زبان C++، پاککننده باید پیش از آزاد کردن حافظه هر شیء مرده، تخریبکننده آن را فراخوانی کند. تخریبکنندههای غیرساده به عنوان Finalizerها پیادهسازی میشوند.
با این حال، هیچ ترتیب مشخصی برای اجرای تخریبکنندهها وجود ندارد، زیرا پیمایش پاککننده ترتیب ساخت اشیاء را در نظر نمیگیرد. این موضوع یک محدودیت سختگیرانه ایجاد میکند: نهاییکنندهها اجازه ندارند به سایر اشیاء موجود در Heap دسترسی داشته باشند. این یک چالش رایج در زبانهای مدیریتشده مانند Java است که آنها نیز ترتیب خاصی را در معنای نهاییسازی پشتیبانی نمیکنند.
برای اجرای این قانون، تیم گوگل از یک پلاگین Clang استفاده میکند. این ابزار بهصورت استاتیک تأیید میکند که در حین تخریب یک شیء، به هیچ شیء دیگری در Heap دسترسی پیدا نشود. برای مثال، اگر یک تخریبکننده ~GCed() سعی کند متدی را روی یک فیلد Member<GCed> فراخوانی کند، پلاگین خطا میدهد، زیرا ممکن است آن فیلد قبلاً نهایی شده باشد.
برای موارد پیچیدهای که نیاز به دسترسی به Heap پیش از تخریب دارند، Oilpan «بازگشتهای پیشنهاییسازی» (Pre-finalization callbacks) را ارائه میدهد. این قابلیت در Blink بهندرت استفاده میشود، زیرا نسبت به تخریبکنندههای استاندارد، سربار بیشتری در هر چرخه جمعآوری زباله ایجاد میکند.
استراتژی پاکسازی Oilpan برای مبارزه با تأخیر (Latency) در سه مرحله تکامل یافت:
۱. توقف کامل (Stop-the-world): کل برنامه بهطور کامل متوقف میشد تا پاککننده بهعنوان بخشی از توقف نهایی GC اجرا شود که باعث لگهای بصری و تأخیر بالا میشد.
۲. افزایشی (Incremental): پاکسازی به واحدهای کوچکتر بر اساس «صفحات» (Pages) تقسیم شد. صفحاتی که باید پاک شوند، در وظایف اضافی رشته اصلی، ترجیحاً در زمانهای بیکاری (Idle time)، پردازش میشدند. تخصیص حافظه، با استفاده از لیستهای آزاد در صفحاتی که قبلاً پاک شدهاند، بافرهای تخصیص محلی (LABs) را پر میکرد. اگر حافظهای در دسترس نبود، برنامه ممکن بود الگوریتم پاکسازی را در حین فرآیند تخصیص، پیش از درخواست حافظه جدید از سیستمعامل، اجرا کند.
۳. همزمان (Concurrent): بازپسگیری حافظه به وظایفی در پسزمینه منتقل شد که بهطور موازی با رشته اصلی اجرا میشوند تا تأثیر روی رشته اصلی بیش از پیش کاهش یابد.
دستیابی به توان عملیاتی بالا
پاکسازی همزمان بر دو اصل (Invariant) سختگیرانه استوار است تا از تداخل دادهها (Data Race) بین وظایف پسزمینه و برنامه جلوگیری شود:
- اصل اول: پاککننده فقط حافظههای «مرده» را پردازش میکند که طبق تعریف، توسط برنامه قابل دسترسی نیستند.
- اصل دوم: برنامه فقط در صفحاتی حافظه تخصیص میدهد که قبلاً پاکسازی شدهاند و طبق تعریف، دیگر توسط پاککننده پردازش نمیشوند.
از آنجا که توابع نهاییکننده باید در رشته اصلی اجرا شوند تا به توسعهدهندگان کمک کنند و احتمال تداخل دادهها در کد برنامه را از بین ببرند، Oilpan از یک «صف نهاییسازی» (Finalization queue) استفاده میکند. هرگاه پاککننده همزمان در پسزمینه با شیئی مواجه شود که دارای تخریبکننده است، آن را به این صف میفرستد. این صف در یک مرحله نهاییسازی مجزا که در رشته اصلی اجرا میشود، پردازش میگردد.
اگر یک شیء دارای نهاییکننده باشد، اضافه کردن حافظه آن به لیست آزاد تا پس از اجرای نهاییکننده به تعویق میافتد. اگر نهاییکنندهای لازم نباشد، پاککننده پسزمینه بلافاصله حافظه بازپسگرفته شده را به لیست آزاد اضافه میکند.
این معماری نتایج ملموسی داشته است. پاکسازی پسزمینه از نسخه M78 کروم عرضه شد. بنچمارکهای واقعی نشان میدهند که زمان پاکسازی در رشته اصلی بین ۲۵٪ تا ۵۰٪ کاهش یافته و میانگین این بهبود ۴۲٪ است. تنها هزینه باقیمانده در رشته اصلی، اجرای خودِ نهاییکنندهها است. در حال حاضر تلاشهایی در Blink در جریان است تا تعداد نهاییکنندهها برای انواع اشیائی که بهشدت نمونهسازی میشوند، کاهش یابد.
این تغییر، فرض قدیمی را که مدیریت حافظه در C++ باید یا دستی باشد یا بر اساس شمارش ارجاعات (Reference Counting)، تغییر میدهد. با ارائه این کتابخانه با کارایی بالا، V8 به توسعهدهندگان C++ اجازه میدهد گرافهای پیچیده و درهمتنیده اشیاء را با امنیت یک زبان مدیریتشده اما با سرعت کد نیتیو مدیریت کنند. توسعهدهندگان باید منتظر انتشار رسمی کتابخانه Oilpan بهعنوان یک جزء V8 باشند، زیرا این امر به توسعهدهندگانی که خارج از مرورگر از V8 استفاده میکنند اجازه میدهد مدیریت حافظه با کارایی مشابه را در اپلیکیشنهای C++ خود پیادهسازی کنند.
گام بعدی شما
- توسعهدهندگان C++ که با مدیریت حافظه در پروژههای بزرگ دستوپنجه نرم میکنند، باید منتظر انتشار رسمی کتابخانه Oilpan بهعنوان یک جزء مستقل از V8 باشند.
- بررسی مستندات v8.dev برای درک نحوه پیادهسازی Concurrent GC در محیطهای غیرمرورگر.
- تحلیل اثر کاهش تأخیر در رشته اصلی بر تجربه کاربری (UX) در اپلیکیشنهای مبتنی بر Electron.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو