Hacker News

چرا اولین تخصیص C++ (m) همیشه 72 کیلوبایت است؟

نظرات

1 min read Via joelsiks.com

Mewayz Team

Editorial Team

Hacker News

راز پشت اولین تخصیص C++ شما

شما یک برنامه C++ ساده می نویسید. یک int جدید. چهار بایت شما strace یا نمایه‌گر حافظه مورد علاقه خود را فعال می‌کنید، و همین‌جاست - فرآیند شما تقریباً 72 کیلوبایت از سیستم عامل درخواست کرده است. نه 4 بایت. نه 64 بایت. 72 کیلوبایت کامل اگر تا به حال به آن عدد خیره شده اید و فکر کرده اید که آیا ابزار شما به شما دروغ می گوید، شما تنها نیستید. این رفتار به ظاهر عجیب یکی از سوالات متداول در میان توسعه دهندگان ++C است که برای اولین بار به بررسی داخلی حافظه می پردازند، و پاسخ ما را به سفری شگفت انگیز در لایه هایی می برد که بین کد شما و سخت افزار واقعی قرار دارند.

وقتی با جدید

تماس می‌گیرید چه اتفاقی می‌افتد

برای درک رقم 72 کیلوبایت، باید زنجیره تخصیص کامل را ردیابی کنید. وقتی کد ++C شما int new را اجرا می‌کند، کامپایلر آن را به یک فراخوانی با operator new ترجمه می‌کند که در اکثر سیستم‌های لینوکس از glibc به malloc واگذار می‌شود. اما malloc مستقیماً 4 بایت حافظه را از هسته درخواست نمی کند. هسته در صفحات کار می کند - معمولاً 4 کیلوبایت در x86_64 - و هزینه یک تماس سیستمی نسبت به دسترسی ساده به حافظه بسیار زیاد است. فراخوانی brk() یا mmap() برای هر تخصیص منفرد، هر برنامه غیر پیش پا افتاده را متوقف می کند.

در عوض، تخصیص‌دهنده حافظه glibc - پیاده‌سازی به نام ptmalloc2 که خود از dlmalloc کلاسیک داگ لی نشات گرفته است، به‌عنوان یک واسطه عمل می‌کند. بلوک‌های بزرگی از حافظه را از هسته درخواست می‌کند، سپس آن‌ها را بر اساس نیاز برنامه‌تان به قطعات کوچک‌تر حک می‌کند. این دلیل اساسی است که اولین تخصیص 4 بایتی شما درخواست بسیار بزرگتری را برای سیستم عامل ایجاد می کند. تخصیص دهنده اسراف نمی کند. استراتژیک است.

تشریح 72 کیلوبایت: جایی که بایت ها می روند

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

ابتدا، malloc glibc، عرصه اصلی را راه‌اندازی می‌کند - ساختار دفترداری اولیه که همه تخصیص‌ها را در رشته اصلی دنبال می‌کند. این عرصه شامل ابرداده برای پشته، نشانگرهای لیست آزاد و ساختارهای bin برای اندازه های مختلف تخصیص است. تخصیص دهنده شکست برنامه را از طریق sbrk() گسترش می دهد و پسوند اولیه توسط یک پارامتر داخلی به نام M_TOP_PAD کنترل می شود که به طور پیش فرض 128 کیلوبایت padding است. با این حال، درخواست اولیه واقعی برای تراز صفحه و موقعیت شکست موجود تنظیم می‌شود، که اغلب منجر به اولین درخواست کوچک‌تر می‌شود - معمولاً در یک فرآیند تازه شروع شده نزدیک به آن رقم 72 کیلوبایتی فرود می‌آید.

دوم، از glibc 2.26، تخصیص دهنده یک کش نخ محلی (tcache) را در اولین استفاده مقداردهی اولیه می کند. tcache شامل 64 bin (یکی برای هر کلاس اندازه تخصیص کوچک) است که هر کدام می توانند تا 7 تکه کش را در خود نگه دارند. خود tcache_perthread_struct حدود 1 کیلوبایت مصرف می‌کند، اما عمل اولیه‌سازی آن، راه‌اندازی عرصه گسترده‌تری را آغاز می‌کند. سوم، زمان اجرا C++ قبلاً تخصیص‌ها را قبل از اجرای main() شما انجام داده است - سازنده‌های استاتیک، مقداردهی اولیه بافر iostream برای std::cout و دوستان، و تنظیم محلی، همگی به این ردپای اولیه پشته کمک می‌کنند.

سیستم Arena و چرا پیش تخصیص هوشمند است

تصمیم به تخصیص بخشی قابل توجهی از حافظه به جای درخواست تکه تکه، تصادفی در اجرا نیست. این یک معاوضه مهندسی عمدی است که ریشه در چندین دهه تجربه برنامه نویسی سیستم دارد. هر فراخوانی به brk() یا mmap() شامل یک سوئیچ زمینه از فضای کاربر به فضای هسته، اصلاح نگاشت‌های حافظه مجازی فرآیند و به‌روزرسانی‌های بالقوه جدول صفحه است. در سخت افزار مدرن، یک تماس سیستمی تقریباً 100 تا 200 نانوثانیه هزینه دارد - در انزوا بی اهمیت، در مقیاس فاجعه بار.

برنامه ای را در نظر بگیرید که 10000 تخصیص کوچک را در طول مقداردهی اولیه انجام می دهد. بدون پیش تخصیص، این به معنای 10000 تماس سیستمی است که تقریباً 1-2 میلی ثانیه سربار خالص هزینه دارد. با یک تخصیص دهنده مبتنی بر عرصه، اولین تخصیص یک فراخوانی سیستمی را راه‌اندازی می‌کند و 9999 تخصیص بعدی به طور کامل در فضای کاربر از طریق عملیات حسابی اشاره‌گر و لیست پیوندی سرویس می‌شوند که هر کدام تقریباً 10 تا 50 نانوثانیه طول می‌کشد. ریاضی بدون ابهام است: پیش تخصیص بر اساس مرتبه های بزرگی برنده می شود.

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

حافظه مجازی در مقابل حافظه فیزیکی: چرا مهم نیست

یک نگرانی رایج در میان توسعه دهندگانی که برای اولین بار با این رفتار مواجه می شوند، اتلاف منابع است. اگر من فقط به 4 بایت نیاز دارم، چرا برنامه من 72 کیلوبایت مصرف می کند؟ بینش مهم این است که حافظه مجازی حافظه فیزیکی نیست. وقتی glibc شکست برنامه را 72 کیلوبایت افزایش می‌دهد، هسته نگاشت‌های حافظه مجازی فرآیند را به‌روزرسانی می‌کند، اما بلافاصله از آن صفحات با RAM فیزیکی پشتیبانی نمی‌کند. صفحات فیزیکی واقعی در صورت تقاضا از طریق عیوب صفحه تخصیص می‌یابند — تنها زمانی که برنامه شما در یک آدرس خاص می‌نویسد، هسته یک صفحه واقعی از حافظه را به آن اختصاص می‌دهد.

💡 DID YOU KNOW?

Mewayz replaces 8+ business tools in one platform

CRM · Invoicing · HR · Projects · Booking · eCommerce · POS · Analytics. Free forever plan available.

Start Free →

این بدان معناست که حتی اگر اندازه مجازی فرآیند شما 72 کیلوبایت افزایش می‌یابد، اندازه مجموعه مقیم آن (RSS) - میزان رم فیزیکی مصرف‌شده - فقط با صفحاتی که واقعاً لمس می‌کنید افزایش می‌یابد. برای یک int جدید، که معمولاً یک صفحه 4 کیلوبایتی است، به‌علاوه هر صفحه‌ای که ابرداده عرصه اشغال می‌کند. فضای مجازی باقیمانده در آنجا قرار دارد و آماده استفاده است و هزینه ای جز فضای آدرس ندارد - که از این مقدار 128 ترابایت در یک سیستم لینوکس 64 بیتی دارید.

این تمایز هنگام نمایه سازی و نظارت بر برنامه های تولید بسیار مهم است. اگر در حال ساختن نرم‌افزاری هستید که نیاز به ردیابی مصرف واقعی منابع دارد - خواه یک پشتیبان SaaS، یک میکروسرویس یا یک خط لوله تجزیه و تحلیل مانند مواردی که روی پلتفرم‌هایی مانند Mewayz برای عملیات تجاری اجرا می‌شوند - همیشه باید RSS را به جای اندازه مجازی کنترل کنید. ابزارهایی مانند /proc/[pid]/smaps، valgrind --tool=massif، و pmap می‌توانند به جای گمراه کردن ارقام حافظه مجازی، ردپای حافظه فیزیکی دقیقی را به شما ارائه دهند.

تخصیص دهنده های مختلف چگونه اولین تخصیص را مدیریت می کنند

شکل 72 کیلوبایتی مختص ptmalloc2 glibc است. تخصیص دهندگان دیگر معاوضه های متفاوتی انجام می دهند و سربار تخصیص اولیه بر این اساس متفاوت است. درک این تفاوت ها هنگام انتخاب یک تخصیص دهنده برای برنامه های کاربردی حساس به عملکرد ارزشمند است.

  • jemalloc (استفاده شده توسط فیس بوک، FreeBSD) - از ساختار میدانی ریزتر با کش های محلی رشته ای استفاده می کند. سربار اولیه بیشتر است (اغلب 200+ KB) اما عملکرد چند رشته ای بهتری را به دلیل کاهش اختلاف قفل ارائه می دهد.
  • tcmalloc (Google's Thread-Caching Malloc) - به طور پیش‌فرض یک حافظه پنهان در هر رشته تقریباً 2 مگابایت را با پیش‌تخصیص تهاجمی اختصاص می‌دهد. سربار اولیه بالاتر است، اما تخصیص های کوچک بعدی بسیار سریع هستند.
  • musl libc's malloc — از طراحی بسیار ساده تری بر اساس mmap برای همه تخصیص ها استفاده می کند. سربار اولیه حداقل است (اغلب فقط 4 کیلوبایت در هر تخصیص)، اما هزینه هر تخصیص به دلیل تماس های سیستمی بیشتر است.
  • mimalloc (Microsoft) - از تخصیص مبتنی بر بخش با بخش‌های 64 مگابایتی استفاده می‌کند. اولین تخصیص یک رزرو مجازی 64 مگابایتی (با حداقل تعهد فیزیکی)، فضای آدرس تجاری برای موقعیت و توان عملیاتی استثنایی ایجاد می کند.

انتخاب بین این تخصیص‌دهنده‌ها کاملاً به حجم کاری شما بستگی دارد. برای برنامه های سرور طولانی مدت با تخصیص چند رشته ای سنگین، jemalloc یا tcmalloc معمولاً از پیش فرض glibc بهتر عمل می کند. برای سیستم‌های تعبیه‌شده با محدودیت حافظه، رویکرد ساده‌تر musl ممکن است علی‌رغم توان عملیاتی کمتر ترجیح داده شود. برای اکثر برنامه های کاربردی دسکتاپ و سرور همه منظوره، سربار اولیه 72 کیلوبایتی ptmalloc2 یک پیش فرض معقول را نشان می دهد که بدون تنظیم به خوبی کار می کند.

تنظیم رفتار تخصیص اولیه

اگر سربار اولیه 72 کیلوبایتی پیش‌فرض واقعاً برای مورد استفاده شما مشکل‌ساز است - شاید شما هزاران فرآیند کوتاه‌مدت را ایجاد می‌کنید که هر کدام تنها تعداد انگشت شماری تخصیص می‌دهند - glibc چندین قابلیت تنظیم را از طریق mallopt() و خانواده MALLOC_ از متغیرهای محیطی ارائه می‌دهد.

پارامتر M_TOP_PAD کنترل می‌کند که تخصیص‌دهنده چه مقدار حافظه اضافی را فراتر از آنچه فوراً مورد نیاز است درخواست می‌کند. تنظیم آن بر روی 0 با mallopt(M_TOP_PAD, 0) به تخصیص‌دهنده می‌گوید که فقط آنچه را که لازم است درخواست کند و سربار اولیه را به میزان قابل توجهی کاهش دهد. پارامتر M_MMAP_THRESHOLD اندازه‌ای را کنترل می‌کند که تخصیص‌ها در بالای آن از mmap به‌جای عرصه استفاده می‌کنند. M_TRIM_THRESHOLD زمانی را کنترل می کند که حافظه آزاد شده به سیستم عامل بازگردانده شود. و از glibc 2.26، تنظیم‌های glibc.malloc.tcache_count و glibc.malloc.tcache_max به شما امکان می‌دهند رفتار حافظه پنهان رشته را کنترل کنید.

با این حال، یک کلمه احتیاط: تنظیم این پارامترها بدون محک زدن دقیق تقریباً همیشه اوضاع را بدتر می کند. پیش‌فرض‌ها بر اساس نمایه‌سازی گسترده در دنیای واقعی انتخاب شده‌اند و نقطه‌ای شیرین برای اکثر حجم کاری‌ها هستند. مگر اینکه شواهد محکمی از پروفایل تولید داشته باشید که سربار malloc یک گلوگاه است - و تأثیر تغییرات خود را اندازه گیری کرده باشید - پیش فرض ها را رها کنید. بهینه‌سازی زودهنگام تخصیص‌دهنده یک شکل مخصوصاً موذیانه از تراشیدن گیلاس است که ساعت‌های مهندسی بی‌شماری را برای سود ناچیز صرف کرده است.

آنچه در مورد برنامه نویسی سیستم به ما می آموزد

معمای 72 کیلوبایتی تخصیص اول، در هسته خود، درسی درباره لایه های انتزاعی است. C++ به شما این توهم را می دهد که new int 4 بایت را اختصاص می دهد. استاندارد زبان این را می گوید. مدل ذهنی شما این را می گوید. اما بین کد شما و سخت افزار مجموعه ای از سیستم های پیچیده قرار دارد - زمان اجرا ++C، تخصیص دهنده کتابخانه C، زیرسیستم حافظه مجازی هسته، و MMU و TLB سخت افزار - هر کدام رفتارها، بهینه سازی ها و سربار خود را اضافه می کنند.

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

توسعه دهندگانی که قابل اعتمادترین و با کارایی ترین سیستم ها را می سازند کسانی هستند که این لایه ها را درک می کنند - نه به این دلیل که باید دائماً به آنها فکر کنند، بلکه به این دلیل که وقتی اتفاق غیرمنتظره ای رخ می دهد (مثل یک تخصیص مرموز 72 کیلوبایت)، آنها مدل ذهنی برای درک دلیل آن را دارند. چه در حال ساختن یک سیستم معاملاتی بلادرنگ، یک موتور بازی، یا یک پلتفرم تجاری باشید که به هزاران کاربر خدمت می‌کند، توانایی استدلال در مورد آنچه که کد شما واقعاً در سطح سیستم انجام می‌دهد چیزی است که توسعه‌دهندگان شایسته را از توسعه‌دهندگان استثنایی جدا می‌کند. 72 کیلوبایت یک باگ نیست. این تخصیص دهنده شماست که کار خود را به خوبی انجام می دهد.

امروز سیستم عامل کسب و کار خود را بسازید

از فریلنسرها گرفته تا آژانس‌ها، Mewayz بیش از 138000 کسب‌وکار را با 207 ماژول یکپارچه قدرت می‌دهد. رایگان شروع کنید، وقتی رشد کردید ارتقا دهید.

رایگان ایجاد کنید