شناسایی هزینهها (Identifying Costs)
پیش از تلاش برای بهینهسازی نحوه تعامل برنامه زبان Go خود با GC (مخفف Garbage Collector)، مهم است که ابتدا اطمینان حاصل کنید آیا اصلاً GC یک هزینه عمده در برنامه محسوب میشود یا خیر.
۱. اکوسیستم زبان گو ابزارهای متعددی را برای شناسایی هزینهها و بهینهسازی برنامههای Go فراهم میکند. برای مرور خلاصه این ابزارها، راهنمای مربوط به عیبیابی (Diagnostics) را مطالعه کنید. در اینجا، ما روی زیرمجموعهای از این ابزارها و یک ترتیب منطقی برای استفاده از آنها تمرکز میکنیم تا تأثیر و رفتار GC را بهتر درک کنیم.
پروفایلهای CPU (CPU Profiles)
۲. یک نقطه شروع عالی، استفاده از پروفایلگیری CPU است. پروفایلهای CPU نمای کلی از محل مصرف زمان پردازنده (CPU) را ارائه میدهند؛ هرچند برای چشمهای ناآشنا ممکن است تشخیص میزان اهمیت نقش GC در یک برنامه خاص دشوار باشد. خوشبختانه، درک نحوه عملکرد GC عمدتاً به شناخت معنای توابع مختلف در پکیج runtime خلاصه میشود. در ادامه، زیرمجموعه مفیدی از این توابع برای تفسیر پروفایلهای CPU آورده شده است:
۳. توجه داشته باشید که توابع فهرستشده در زیر از نوع توابع برگشتی نهایی (Leaf Functions) نیستند، بنابراین ممکن است در خروجی پیشفرض ابزار pprof با دستور top ظاهر نشوند. به جای آن، از دستور top -cum یا دستور list به طور مستقیم روی این توابع استفاده کنید و روی ستون درصد تجمعی (Cumulative Percent) تمرکز نمایید.
-
runtime.gcBgMarkWorker: نقطه ورود به گوروتورینهای کارگر علامتگذاری در پسزمینه (Background Mark Worker Goroutines). زمان صرف شده در اینجا با فرکانس GC و همچنین پیچیدگی و اندازه گراف اشیاء مقیاس میشود. این تابع نشاندهنده یک مبنا (Baseline) برای میزان زمانی است که برنامه صرف علامتگذاری و اسکن میکند.-
توجه داشته باشید که در درون این گوروتورینها، فراخوانیهایی به
runtime.gcDrainMarkWorkerDedicated،runtime.gcDrainMarkWorkerFractionalوruntime.gcDrainMarkWorkerIdleپیدا خواهید کرد که نوع کارگر را نشان میدهند. در یک برنامه Go که تا حد زیادی در حالت بیکار (Idle) است، GC زبان گو از منابع CPU اضافی (بیکار) برای انجام سریعتر کار خود استفاده میکند که با نمادruntime.gcDrainMarkWorkerIdleمشخص میشود. در نتیجه، زمان صرف شده در اینجا ممکن است بخش بزرگی از نمونههای CPU را تشکیل دهد که GC گو آنها را آزاد میداند. اگر برنامه فعالتر شود، زمان CPU در کارگرهای بیکار کاهش مییابد. یک دلیل رایج برای رخداد این حالت این است که برنامه کاملاً در یک گوروتورین اجرا میشود اما مقدارGOMAXPROCSبزرگتر از ۱ است.
-
-
runtime.mallocgc: نقطه ورود به تخصیصدهنده حافظه (Memory Allocator) برای حافظه Heap. مقدار زیادی از زمان تجمعی صرف شده در اینجا (بیش از ۱۵٪) معمولاً نشاندهنده تخصیص حجم زیادی از حافظه است. -
runtime.gcAssistAlloc: تابعی است که گوروتورینها برای واگذاری بخشی از زمان خود به منظور کمک به GC در اسکن و علامتگذاری وارد آن میشوند. مقدار زیادی از زمان تجمعی صرف شده در اینجا (بیش از ۵٪) نشان میدهد که برنامه احتمالاً از نظر سرعت تخصیص حافظه، از GC پیشی گرفته است (Out-pacing). این امر نشاندهنده تأثیر بسیار بالا از طرف GC است و همچنین زمان صرف شده توسط برنامه برای علامتگذاری و اسکن را نشان میدهد. توجه داشته باشید که این مورد در درخت فراخوانیruntime.mallocgcگنجانده شده است، بنابراین آن را نیز افزایش میدهد.
تریسهای اجرای برنامه (Execution Traces)
۴. در حالی که پروفایلهای CPU برای شناسایی محل کلی صرف زمان عالی هستند، اما برای نشان دادن هزینههای عملکردی که ظریفتر، نادرتر یا بهطور خاص مربوط به تاخیر (Latency) هستند، کارایی کمتری دارند. در مقابل، تریسهای اجرا نمای غنی و عمیقی از یک پنجره کوتاه از اجرای برنامه Go ارائه میدهند. آنها حاوی انواع رویدادهای مرتبط با GC گو هستند و مسیرهای اجرایی خاص را میتوان به همراه نحوه تعامل برنامه با GC به طور مستقیم مشاهده کرد. تمام رویدادهای ردیابیشدهی مربوط به GC به راحتی در نمایشگر تریس (Trace Viewer) برچسبگذاری شدهاند.
۵. برای آشنایی با نحوه شروع کار با تریسهای اجرا، مستندات پکیج runtime/trace را مطالعه کنید.
تریسهای GC (GC Traces)
۶. هنگامی که هیچکدام از روشهایپرطرفدار دیگر پاسخگو نبودند، GC گو تریسهای خاص مختلفی را ارائه میدهد که بینش بسیار عمیقتری نسبت به رفتار GC فراهم میکنند. این تریسها همیشه مستقیماً به STDERR چاپ میشوند (یک خط به ازای هر چرخه GC) و از طریق متغیر محیطی GODEBUG که تمام برنامههای Go آن را میشناسند، پیکربندی میشوند. این تریسها عمدتاً برای دیباگ کردن خود GC گو مفید هستند زیرا نیازمندیهایی به آشنایی با جزئیات پیادهسازی GC دارند، اما با این وجود گاهی اوقات برای به دست آوردن درک بهتری از رفتار GC مفید واقع میشوند.
۷. تریس اصلی GC با تنظیم متغیر GODEBUG=gctrace=1 فعال میشود. خروجی تولید شده توسط این تریس در بخش متغیرهای محیطی در مستندات پکیج runtime مستند شده است.
۸. یک تریس مکمل برای GC به نام «تریس پیسر» (Pacer Trace) بینشهای حتی عمیقتری را فراهم میکند و با تنظیم GODEBUG=gcpacertrace=1 فعال میشود. تفسیر این خروجی مستلزم درک «پیسر» (Pacer) GC است (به منابع تکمیلی مراجعه کنید) که خارج از محدوده این راهنما است.
حذف تخصیصهای Heap (Eliminating Heap Allocations)
یک راه برای کاهش هزینههای ناشی از GC این است که از همان ابتدا کاری کنید تا GC مقادیر کمتری را مدیریت کند. تکنیکهای شرح داده شده در زیر میتوانند برخی از بزرگترین بهبودها را در عملکرد ایجاد کنند، زیرا همانطور که بخش GOGC نشان داد، نرخ تخصیص (Allocation Rate) یک برنامه Go فاکتور عمدهای در فرکانس GC است که به عنوان متریک کلیدی هزینه در این راهنما استفاده میشود.
پروفایلگیری Heap (Heap Profiling)
پس از شناسایی اینکه GC منبع هزینههای قابلتوجهی است، گام بعدی در حذف تخصیصهای Heap این است که بفهمیم بیشتر آنها از کجا سرچشمه میگیرند. برای این منظور، پروفایلهای حافظه (در واقع، پروفایلهای حافظه Heap) بسیار مفید هستند. برای نحوه شروع کار با آنها، مستندات مربوطه را بررسی کنید.
پروفایلهای حافظه توصیف میکنند که تخصیصهای Heap در کجا از برنامه سرچشمه میگیرند و آنها را با استفاده از استک ترِیس (Stack Trace) در نقطه تخصیص شناسایی میکنند. هر پروفایل حافظه میتواند حافظه را به چهار روش تفکیک کند:
-
inuse_objects— تعداد اشیایی که در حال حاضر زنده هستند را تفکیک میکند. -
inuse_space— اشیاء زنده را بر اساس میزان حافظهای که به بایت مصرف میکنند، تفکیک میکند. -
alloc_objects— تعداد کل اشیایی را که از زمان شروع اجرای برنامه Go تخصیص یافتهاند، تفکیک میکند. -
alloc_space— کل میزان حافظه تخصیصیافته از زمان شروع اجرای برنامه Go را تفکیک میکند.
جابجایی بین این نماهای مختلف از حافظه Heap را میتوان یا با پرچم -sample_index در ابزار pprof و یا از طریق گزینهی sample_index هنگام استفاده تعاملی از ابزار انجام داد.
💡 نکته: پروفایلهای حافظه به طور پیشفرض تنها زیرمجموعهای از اشیاء Heap را نمونهبرداری (Sample) میکنند، بنابراین آنها حاوی اطلاعات مربوط به تکتک تخصیصهای Heap نخواهند بود. با این حال، این میزان برای یافتن نقاط داغ (Hot-spots) کافی است. برای تغییر نرخ نمونهبرداری، به
runtime.MemProfileRateمراجعه کنید.
برای اهداف کاهش هزینههای GC، معمولاً alloc_space مفیدترین نما است زیرا مستقیماً با نرخ تخصیص مطابقت دارد. این نما نقاط داغ تخصیص را مشخص میکند که بیشترین بهرهوری را به همراه خواهند داشت.
تحلیل فرار (Escape Analysis)
هنگامی که محلهای نامزد تخصیص Heap با کمک پروفایلهای حافظه شناسایی شدند، چگونه میتوان آنها را حذف کرد؟ کلید کار، استفاده از تحلیل فرار کامپایلر Go (Escape Analysis) است تا کامپایلر گو بتواند حافظه جایگزین و کارآمدتری را برای این حافظه پیدا کند؛ به عنوان مثال، استفاده از پشته گوروتورین (Goroutine Stack). خوشبختانه، کامپایلر Go قابلیت این را دارد که توضیح دهد چرا تصمیم میگیرد یک مقدار را به Heap بفرستد (Escape دهد). با داشتن این دانش، مسئله به سازماندهی مجدد کد منبع برای تغییر نتیجه تحلیل تبدیل میشود (که اغلب سختترین بخش است، اما خارج از محدوده این راهنما قرار دارد).
در خصوص نحوه دسترسی به اطلاعات تحلیل فرار کامپایلر Go، سادهترین راه استفاده از یک پرچم دیباگ پشتیبانیشده توسط کامپایلر است که تمام بهینهسازیهای اعمالشده یا نشده روی یک پکیج را در قالب متنی توصیف میکند. این موارد شامل این موضوع میشود که آیا مقادیر فرار میکنند یا خیر. دستور زیر را امتحان کنید، که در آن [package] مسیر یک پکیج Go است:
$ go build -gcflags=-m=3 [package]
این اطلاعات همچنین میتوانند به عنوان یک روکش (Overlay) در یک ویرایشگر پشتیبانیکننده از LSP به صورت بصری نمایش داده شوند؛ این قابلیت به عنوان یک اکشن کد (Code Action) ارائه شده است. برای مثال، در VS Code، دستور "Source Action... > Show compiler optimization details" را برای فعالسازی عیبیابی پکیج فعلی اجرا کنید (همچنین میتوانید دستور "Go: Toggle compiler optimization details" را اجرا کنید). از تنظیمات پیکربندی زیر برای کنترل اینکه کدام حاشیهنویسیها نمایش داده شوند استفاده کنید:
-
روکش مربوط به تحلیل فرار را با تنظیم
ui.diagnostic.annotationsبه طوری که شاملescapeباشد، فعال کنید.
در نهایت، کامپایلر Go این اطلاعات را در یک قالب قابل خواندن توسط ماشین (JSON) فراهم میکند که ممکن است برای ساخت ابزارهای سفارشی اضافی استفاده شود. برای اطلاعات بیشتر در این زمینه، مستندات موجود در کد منبع Go را مطالعه کنید.
بهینهسازیهای خاص پیادهسازی (Implementation-Specific Optimizations)
GC گو نسبت به جمعیتشناسی حافظه زنده (Demographics of Live Memory) حساس است، زیرا گراف پیچیدهای از اشیاء و پ اشارهگرها هم موازیسازی را محدود میکند و هم کار بیشتری برای GC تولید میکند. در نتیجه، GC شامل چند بهینهسازی برای ساختارهای رایج خاص است. مفیدترین موارد برای بهینهسازی عملکرد به طور مستقیم در زیر فهرست شدهاند:
⚠️ هشدار: اعمال بهینهسازیهای زیر ممکن است با پنهان کردن هدف اصلی، خوانایی کد شما را کاهش دهد و ممکن است در نسخههای مختلف Go پایدار نماند. ترجیح دهید این بهینهسازیها را تنها در جاهایی اعمال کنید که بیشترین اهمیت را دارند. این مکانها را میتوان با استفاده از ابزارهای فهرستشده در بخش «شناسایی هزینهها» شناسایی کرد.
-
مقادیر بدون اشارهگر (Pointer-free values) از سایر مقادیر جدا میشوند.
-
در نتیجه، ممکن است مزیتبخش باشد که اشارهگرها را از ساختارهای دادهای که به شدت به آنها نیاز ندارند حذف کنید، زیرا این کار فشار کش (Cache Pressure) را که GC بر روی برنامه اعمال میکند کاهش میدهد. در نتیجه، ساختارهای دادهای که به جای مقادیر اشارهگر بر روی اندیسها تکیه میکنند، اگرچه تایپپذیری کمتری دارند، ممکن است عملکرد بهتری داشته باشند. این کار تنها در صورتی ارزش انجام دادن دارد که مشخص باشد گراف اشیاء پیچیده است و GC زمان زیادی را صرف علامتگذاری و اسکن میکند.
-
-
GC اسکن کردن مقادیر را در آخرین اشارهگر موجود در آن مقدار متوقف میکند.
-
در نتیجه، ممکن است مفید باشد که فیلدهای اشارهگر در مقادیر از نوع struct را در ابتدای مقدار گروهبندی کنید. این کار تنها در صورتی ارزش انجام دارد که مشخص باشد برنامه زمان زیادی از خود را صرف علامتگذاری و اسکن میکند (در تئوری، کامپایلر میتواند این کار را به طور خودکار انجام دهد، اما هنوز پیادهسازی نشده است و فیلدهای ساختار دقیقاً همانطور که در کد منبع نوشته شدهاند چیده میشوند).
-
علاوه بر این، GC باید با تقریباً هر اشارهگری که میبیند تعامل داشته باشد، بنابراین استفاده از اندیسها در یک اسلایس (Slice)، به عنوان مثال، به جای اشارهگرها، میتواند به کاهش هزینههای GC کمک کند.
صفحات بزرگ شفاف لینوکس (Transparent Huge Pages - THP)
هنگامی که یک برنامه به حافظه دسترسی پیدا میکند، پردازنده نیاز دارد آدرسهای حافظه مجازی که استفاده میکند را به آدرسهای حافظه فیزیکی که به دادههای مورد نظر اشاره دارند تبدیل کند. برای انجام این کار، پردازنده به «جدول صفحه» (Page Table) مراجعه میکند؛ ساختار دادهای که نگاشت حافظه مجازی به فیزیکی را نشان میدهد و توسط سیستمعامل مدیریت میشود. هر ورودی در جدول صفحه نمایانگر یک بلوک غیرقابل تقسیم از حافظه فیزیکی به نام یک صفحه (Page) است، به همین دلیل این نام انتخاب شده است.
صفحات بزرگ شفاف (THP) یک ویژگی در لینوکس است که به طور شفاف صفحات حافظه فیزیکی پشتیبان مناطق حافظه مجازی پیوسته را با بلوکهای بزرگتری از حافظه به نام صفحات بزرگ (Huge Pages) جایگزین میکند. با استفاده از بلوکهای بزرگتر، ورودیهای جدول صفحه کمتری برای نمایش همان منطقه حافظه مورد نیاز است و زمانهای جستجوی جدول صفحه بهبود مییابد. با این حال، بلوکهای بزرگتر به معنای هدررفت بیشتر در صورتی است که تنها بخش کوچکی از صفحه بزرگ توسط سیستم استفاده شود.
هنگامی که برنامههای Go را در محیط پروداکشن (Production) اجرا میکنید، فعال کردن صفحات بزرگ شفاف در لینوکس میتواند توان عملیاتی (Throughput) و تاخیر را به قیمت مصرف حافظه اضافی بهبود بخشد. برنامههای دارای Heap کوچک معمولاً از THP بهرهمند نمیشوند و ممکن است در نهایت مقدار قابلتوجهی حافظه اضافی (تا ۵۰٪) مصرف کنند. با این حال، برنامههای دارای Heap بزرگ (۱ گیگابایت یا بیشتر) معمولاً سود زیادی میبرند (تا ۱۰٪ بهبود توان عملیاتی) بدون اینکه سربار حافظه اضافی بسیار زیادی داشته باشند (۱-۲٪ یا کمتر). آگاهی از تنظیمات THP در هر دو حالت میتواند مفید باشد و آزمایش کردن همیشه توصیه میشود.
شما میتوانید صفحات بزرگ شفاف را در محیط لینوکس با ویرایش مسیر /sys/kernel/mm/transparent_hugepage/enabled فعال یا غیرفعال کنید. برای جزئیات بیشتر، راهنمای رسمی مدیریت لینوکس را مشاهده کنید. اگر انتخاب میکنید که محیط پروداکشن لینوکس شما صفحات بزرگ شفاف را فعال کند، تنظیمات اضافی زیر را برای برنامههای Go توصیه میکنیم:
-
تنظیم
/sys/kernel/mm/transparent_hugepage/defragرویdeferیاdefer+madvise.-
این تنظیم کنترل میکند که هسته لینوکس با چه شدتی صفحات معمولی را به صفحات بزرگ ادغام (Coalesce) کند. گزینه
deferبه هسته میگوید که صفحات بزرگ را به صورت تنبل (Lazy) و در پسزمینه ادغام کند. یک تنظیم تهاجمیتر میتواند باعث ایجاد توقف (Stall) در سیستمهای محدود از نظر حافظه شود و اغلب به تاخیرهای برنامه لطمه بزند. گزینهdefer+madviseمشابهdeferاست، اما با سایر برنامههای روی سیستم که صراحتاً درخواست صفحات بزرگ میکنند و برای عملکرد به آنها نیاز دارند، سازگارتر است.
-
-
تنظیم
/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_noneروی0.-
این تنظیم کنترل میکند که دیمن هسته لینوکس هنگام تلاش برای تخصیص یک صفحه بزرگ، چه تعداد صفحه اضافی را میتواند تخصیص دهد. تنظیم پیشفرض به طور حداکثری تهاجمی است و اغلب میتواند کارهایی را که زمان اجرای Go برای بازگرداندن حافظه به سیستمعامل انجام میدهد، خنثی کند. قبل از نسخه Go 1.21، رانتایم گو تلاش میکرد اثرات منفی تنظیم پیشفرض را کاهش دهد، اما این کار با هزینه CPU همراه بود. با Go 1.21+ و Linux 6.2+، رانتایم گو دیگر وضعیت صفحه بزرگ را تغییر نمیدهد.
-
اگر هنگام ارتقا به Go 1.21.1 یا بالاتر با افزایش مصرف حافظه مواجه شدید، سعی کنید این تنظیم را اعمال کنید؛ احتمالاً مشکل شما را حل خواهد کرد. به عنوان یک راهحل جایگزین، میتوانید تابع
Prctlرا با پارامترPR_SET_THP_DISABLEبرای غیرفعال کردن صفحات بزرگ در سطح فرایند فراخوانی کنید، یا میتوانید متغیرGODEBUG=disablethp=1را تنظیم کنید تا صفحات بزرگ برای حافظه Heap غیرفعال شوند. توجه داشته باشید که ممکن است تنظیمGODEBUGدر نسخه آینده حذف شود.
-
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات