جا دادن اپلیکیشنهای پیچیده در دستگاههای با محدودیت حافظه (Fitting Complex Applications in Storage-Constrained Devices)
نکات کلیدی
- سیستمهای نهفتهٔ مدرن باید میان افزایش مداوم پیچیدگی نرمافزار و محدودیتهای تقریباً ثابت حافظه توازن برقرار کنند. این وضعیت توسعهدهندگان را ناچار میسازد که در کنار استفاده از زبانهایی مانند سیپلاسپلاس، بهدلیل محدودیتهای سختافزاری، بهطور جدی روی بهینهسازی اندازهٔ باینری تمرکز کنند.
- سیپلاسپلاس «انتزاعهای بدون سربار» ارائه میدهد که امکان برنامهنویسی سطح بالا را بدون تحمیل جریمهٔ کارایی در زمان اجرا فراهم میکند. با این حال، توسعهدهندگان باید آگاه باشند که قابلیتهایی مانند قالبها، اشارهگرهای هوشمند و استفاده از کتابخانهٔ قالب استاندارد چگونه میتوانند بر اندازهٔ باینری اثر بگذارند.
- ابزارهایی مانند «بلوتی» و «پانکاور» برای درک و مدیریت تورم باینری ضروری هستند. این ابزارها بینشی فراهم میکنند دربارهٔ اینکه کدام مؤلفهها و الگوهای طراحی بیشترین سهم را در اندازهٔ فریمور دارند.
- موازنه میان کارایی در زمان اجرا و اندازهٔ باینری باید بر تصمیمهای معماری اثر بگذارد. برای مثال، ترجیح استفاده از مفهومها بهجای چندریختی، یا استفاده از جایگزینهای سادهتر کتابخانهٔ استاندارد مانند ورودیوخروجی سیاستاندارد بهجای جریانهای ورودیوخروجی پیچیده.
- بهینهسازی اندازهٔ باینری یک دغدغهٔ مقطعی نیست، بلکه موضوعی است که کل چرخهٔ عمر توسعه را در بر میگیرد. بهترین رویکرد این است که رهگیری اندازهٔ باینری در فرایندهای خودکار ساخت ادغام شود و تصمیمهای آگاهانهای دربارهٔ قابلیتهای زبان، پرچمهای ابزار ساخت و مقیاسپذیری طراحی اتخاذ گردد.
وقتی به نوع محصولاتی که توسعهدهندگان نرمافزار روی آنها کار میکنند فکر میکنیم، معمولاً سرویسهای وب، اپلیکیشنهای دسکتاپ یا سامانههای محاسباتی با کارایی بالا مانند آموزش یک مدل هوش مصنوعی روی یک خوشهٔ سرور به ذهن میآید.
اما وقتی من به توسعهٔ نرمافزار فکر میکنم، اغلب به بردهای مدار، حسگرها و چراغهای الایدی که روی میز کارم قرار دارند نگاه میکنم. این ابزارهای «ریز» معمولاً دستگاههای نهفته نامیده میشوند. هرچند رایانههای تکبردی مانند رزبریپای نیز در دستهٔ سامانههای لینوکس نهفته قرار میگیرند، اما تمرکز این مقاله بر میکروکنترلرها است.
این مقاله به بررسی محدودیتهایی میپردازد که هنگام نوشتن نرمافزار برای میکروکنترلرها با آنها مواجه میشویم، وضعیت فعلی استفاده از سیپلاسپلاس در این حوزه، و اینکه چگونه میتوان با یکی از بزرگترین چالشها در مسیر افزایش مقیاس و پیچیدگی، یعنی اندازهٔ باینری، کنار آمد.
میکروکنترلرها چه هستند؟
این دسته از تراشهها قادر به اجرای سیستمعاملهای کامل مانند ویندوز یا لینوکس نیستند. معمولاً آنها سیستمعاملهای بلادرنگ سبکتری را اجرا میکنند. در برخی موارد، الزامات آنقدر سختگیرانه است یا توان پردازشی آنقدر محدود است که اپلیکیشنها بهصورت مستقیم و بدون سیستمعامل نوشته میشوند، با دسترسی مستقیم به وقفهها و ثباتهای پردازنده.
در بسیاری از بخشهای محصول، میکروکنترلرها بهعنوان واحد پردازشی پایه در سیستم استفاده میشوند؛ جایی که پردازش کارآمد و کممصرف ضروری است. نمونههایی از این کاربردها شامل پایش محیطی، حسگرهای صنعتی و سامانههای خانهٔ هوشمند هستند. اصطلاح «اینترنت اشیا» معمولاً برای توصیف این موارد به کار میرود، جایی که یک شبکه از حسگرهای کوچک و گرههای پردازش لبهای مورد استفاده قرار میگیرد.
در مقایسه با پردازندههای متعارف، میکروکنترلرها تنوع بسیار بیشتری از نظر معماری سختافزاری دارند.
این پراکندگی در سمت نرمافزار نیز دیده میشود؛ جایی که وابستگی بیشتری به ابزارهای اختصاصی و بستههای توسعهٔ نرمافزاری وجود دارد تا امکان دسترسی سطح پایین به سختافزار هر تراشه و معماری آن فراهم شود. بهدلیل همین ارتباط نزدیک با سختافزار، بستههای توسعهٔ میکروکنترلر تقریباً همیشه با زبانهای سطح پایین، بهویژه زبان سی، نوشته میشوند.
چارچوبها و زبانها
با استفاده از بستههای توسعهٔ سختافزاری، هماکنون نیز میتوان اپلیکیشنهای کامل ایجاد کرد و حتی مستقیماً به قابلیتهای داخلی سختافزار مانند وقفهها و ثباتها دسترسی داشت. با این حال، این نزدیکی شدید به سختافزار باعث میشود تلاش لازم برای توسعهٔ اپلیکیشنهای پیچیده و قابل استفادهٔ مجدد روی چندین پلتفرم بهطور قابلتوجهی افزایش یابد.
بهدلیل این پیچیدگی اضافی، معمولاً اپلیکیشنهای میکروکنترلری روی یک سیستمعامل بلادرنگ ساخته میشوند. این چارچوبها یک لایهٔ انتزاع فراهم میکنند که امکان مدیریت چندریسمانی، تنظیم اولویتها و زمانبندی مناسب اجرای وظایف را فراهم میسازد. نمونههای شناختهشده در این حوزه شامل فریآرتیاواس و زفیر هستند.
برای همگام شدن با تحول نرمافزار و نیازهای محصول، توسعهٔ سامانههای نهفته باید از قدرت بیان و سادگی استفادهٔ زبانهای سطح بالاتر بهره بگیرد. زبانهایی مانند سیپلاسپلاس و راست اکنون در فضای سامانههای نهفته نیز مورد استفاده قرار میگیرند، اما این موضوع مجموعهٔ جدیدی از چالشها را به همراه دارد؛ زیرا این زبانها در ابتدا بدون در نظر گرفتن چنین محدودیتهای سختگیرانهای در زمینهٔ حافظه و توان پردازشی طراحی شده بودند.
++C در میکروکنترلرها
سیپلاسپلاس یک زبان برنامهنویسی همهمنظوره است که طی حدود چهل سال توسعه یافته است. بهروزرسانیهای زبان توسط کمیتهٔ استانداردسازی و در قالب نسخههای مختلف معرفی میشوند. در سالهای اخیر، این کمیته یک چرخهٔ سهساله را دنبال کرده است و جدیدترین بازبینی زبان نسخهٔ ۲۰۲۳ است.
قابلیتهای سیپلاسپلاس عموماً در دو دستهٔ اصلی قرار میگیرند: قابلیتهای زبانی و قابلیتهای کتابخانهای. قابلیتهای زبانی رفتار هستهای و نحو کدی را که نوشته میشود تعریف میکنند؛ مانند معنی کلیدواژهها، عملگرهای ریاضی و نحوهٔ اجرای آنها.
قابلیتهای کتابخانهای ابزارهای اضافی معرفی میکنند، معمولاً بهصورت شیء یا تابع، که با استفاده از هستهٔ زبان پیادهسازی شدهاند. کتابخانههای رایج شامل رشتهها، ساختارهای داده و الگوریتمها هستند. در حالی که قابلیتهای زبانی بهطور ذاتی توسط کامپایلر پیادهسازی میشوند، قابلیتهای کتابخانهای بهصورت یک کتابخانهٔ قابل پیوند در دسترس هستند که به آن کتابخانهٔ استاندارد گفته میشود.
توسعهدهندگان پروژههای مبتنی بر میکروکنترلر معمولاً از استفاده از کتابخانهٔ استاندارد سیپلاسپلاس اجتناب میکنند. دلیل اصلی این موضوع آن است که بسیاری از قابلیتهای کتابخانهٔ استاندارد، مانند ساختارهای داده، از تخصیص پویای حافظه استفاده میکنند. همانطور که پیشتر اشاره شد، تراشههای میکروکنترلر معمولاً حافظهٔ فرّار بسیار محدودی دارند و اغلب اپلیکیشنها بهصورت مستقیم روی سختافزار یا روی یک سیستمعامل بلادرنگ ساده اجرا میشوند، بدون وجود واحد مدیریت حافظه.
در چنین شرایطی، تخصیص و آزادسازی مداوم حافظه در طول عمر اپلیکیشن میتواند منجر به تکهتکه شدن حافظه شود. تکهتکه شدن حافظه فرایندی است که طی آن، حافظهٔ در دسترس یک برنامه بر اثر مجموعهای از تخصیصها و آزادسازیهای کوچک به بخشهای غیرپیوسته تقسیم میشود. پس از مدتی، برنامه دیگر قادر به تخصیص بلوکهای پیوستهٔ جدید نخواهد بود، حتی اگر مجموع حافظهٔ آزاد عدد قابل توجهی را نشان دهد.

در صورت شکست عملیات تخصیص حافظه، برنامه یا یک استثنای کمبود حافظه پرتاب میکند یا بهطور ناگهانی خاتمه مییابد. در زمینهٔ فریمور یک میکروکنترلر که معمولاً از مکانیزم استثنا پشتیبانی نمیکند، این وضعیت به این معناست که اجرای فریمور بهتدریج تمام حافظهٔ قابل استفاده را مصرف میکند تا در نهایت دچار ازکارافتادگی شود و سیستم مجبور به راهاندازی مجدد گردد.
برای اجتناب از تخصیص پویای حافظه، یکی از جایگزینهای رایج استفاده از «کتابخانهٔ قالب نهفته» است؛ کتابخانهای که در آن همهٔ اشیا بهصورت ایستا تخصیص داده میشوند. با این رویکرد، توسعهدهنده دیدی کاملاً قطعی و قابل پیشبینی نسبت به میزان حافظهٔ مصرفی در زمان اجرا دارد.
با این حال، استفاده از کتابخانهٔ قالب استاندارد نیز مزایای قابل توجهی دارد. این کتابخانه دسترسی به جدیدترین قابلیتهای کتابخانهای توسعهیافته برای سیپلاسپلاس و تعامل آنها با ویژگیهای جدید زبان را فراهم میکند. همچنین، مانع ورود برای توسعهدهندگان پایینتر است، زیرا منابع آموزشی و تجربیات بیشتری برای یادگیری و استفاده از آن وجود دارد.
هرچه بتوان اشیا و زمینههای بیشتری را با استفاده از قابلیتهای ارزیابی در زمان کامپایل محاسبه کرد، میتوان منطق بیشتری را پیش از اجرای برنامه حلوفصل کرد و در نتیجه نیاز به تخصیص حافظه در زمان اجرا را کاهش داد.
مهمتر از همه، عیبی که پیشتر در مورد تخصیص پویای حافظه مطرح شد، بسته به حوزهٔ کاری اپلیکیشن و نحوهٔ طراحی آن، ممکن است اصلاً مسئلهٔ مهمی نباشد. فرض کنید اپلیکیشنی داریم که میتوان آن را بهطور کامل در زمان راهاندازی سیستم مشخص کرد؛ برای مثال، یک دستگاه تککاره مانند یک وسیلهٔ اختصاصی. نمونهٔ دیگر سیستمی است که حالتهای جداگانهای برای اجرا و پیکربندی دارد و برای جابهجایی بین این دو حالت نیاز به راهاندازی مجدد کامل اپلیکیشن است.
در چنین سناریوهایی، میتوان تمام اشیا را در مرحلهٔ مقداردهی اولیه تعریف و تخصیص داد و سپس در طول اجرای برنامه، ردپای حافظهای ثابتی داشت. حتی میتوان این محدودیت را کمی انعطافپذیرتر در نظر گرفت، به شرط آنکه چرخههای تخصیص و آزادسازی حافظه پایدار باشند یا وظایف مختلف بهگونهای زمانبندی شوند که چرخههای تخصیص نامنظم ایجاد نکنند و منجر به تکهتکه شدن حافظه نشوند.
بنابراین، کنار گذاشتن کامل کتابخانهٔ استاندارد از همان ابتدا تصمیم درستی نیست. لازم است حوزهٔ کاری اپلیکیشن و چرخهٔ عمر اجرای آن بهدقت تحلیل شود و سپس ارزیابی گردد که آیا استفاده از این کتابخانه از نظر ردپای حافظه مناسب است یا خیر.
نکتهٔ قابل توجه دیگر این است که کتابخانهٔ استاندارد سیپلاسپلاس بهتدریج در حال اضافه کردن قابلیتهایی است که برای سامانههای نهفته مناسبتر هستند. برای نمونه، در نسخههای آیندهٔ استاندارد، ساختارهای دادهای با ظرفیت ثابت معرفی میشوند که امکان افزودن عنصر جدید را تنها در صورت وجود فضای کافی فراهم میکنند. این روند نشان میدهد که زبان بهتدریج به نیازهای محیطهای محدود نیز پاسخ میدهد.
تکامل ++C
در ۲۰ سال گذشته، زبان سیپلاسپلاس شاهد اضافه شدن حجم قابلتوجهی از قابلیتهای زبانی و کتابخانهای به استاندارد خود بوده است. جنبههای متعددی وجود دارد که میتوان دربارهٔ اینکه این تغییرات چگونه بر معماری نرمافزار و فرایند توسعه، بهویژه در حوزهٔ میکروکنترلرها، تأثیر گذاشتهاند صحبت کرد.
دو اثر اصلی که میخواهم روی آنها تأکید کنم این است که اکنون میتوانیم کد سیپلاسپلاس را با سطح انتزاع بالاتری نسبت به گذشته بنویسیم؛ زیرا ابزارهایی مانند اشارهگرهای هوشمند و استنتاج خودکار نوع در اختیار داریم. همچنین کتابخانههای قدرتمندتری مانند قالببندی و چاپ در دسترس هستند که بخش زیادی از تلاش توسعه را در زمینهٔ مدیریت حافظه، سامانهٔ نوع و موارد مشابه کاهش میدهند. افزون بر این، خود زبان و کتابخانههای آن نحوهای قدرتمندتری برای انجام عملیات پیچیده معرفی کردهاند؛ از جمله قالبهای چندمتغیره، عبارتهای جمعشونده و بازهها.
بیایید برخی از این جنبهها را در قالب یک مثال کوچک با یکدیگر ترکیب کنیم و آنها را در عمل ببینیم. ما سامانهای داریم که نقاط دادهای از انواع مختلف را جمعآوری میکند. میتوانیم پیادهسازی نقطهٔ داده را در قالب یک ساختار مبتنی بر قالب تعمیم دهیم، بهگونهای که امکان ذخیرهٔ فرادادهٔ بیشتر نیز وجود داشته باشد:

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

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

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

اکنون توجه خود را به پردازش مقادیری معطوف میکنیم که قصد ارسال آنها را داریم. برای این کار از قابلیتی استفاده میکنیم که در نسخهٔ ۲۰۲۰ سیپلاسپلاس معرفی شده و «مفهومها» نام دارد. مفهومها نحو سادهتری برای تعریف الزامات نوعها در برنامهنویسی فرابرنامهای مبتنی بر قالب فراهم میکنند.
در این مثال، تابع process_and_send_dp را با چند پیادهسازی مختلف بازتعریف میکنیم، بهطوری که هر پیادهسازی به یک مفهوم متفاوت متصل است و نوع دادهٔ ورودی را محدود میکند. در هر پیادهسازی، نقطهٔ داده ابتدا پسپردازش میشود، سپس سختافزار جانبی متناظر بهدست میآید و در نهایت داده بر اساس یک الگوی قالببندی مشخص ارسال میگردد.

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

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

با مشاهدهٔ مثال بالا، میتوان دید که رابطی در اختیار داریم که بسیار انعطافپذیر و قدرتمند است. برای مثال، نیازی به پارامترهای پیکربندی اضافی یا نامهای متفاوت تابع برای هر نوع ورودی نداریم. بهصورت شهودی، ایجاد چنین انعطافی معمولاً این تصور را بهوجود میآورد که سربار کارایی نیز به همراه خواهد داشت؛ زیرا پیادهسازی زیرین باید حالتهای مختلف استفاده را در پشت صحنه تشخیص داده و حلوفصل کند. این سربار در برنامهنویسی شیگرا با سیپلاسپلاس معمولاً بهشکل جدولهای توابع مجازی و لایههای واسط آنها ظاهر میشود که باید مشخص کنند کدام کلاس مشتق قرار است مورد استفاده قرار گیرد. با این حال، این موضوع الزاماً همیشه صادق نیست و سیپلاسپلاس تکنیکهای متعددی برای کاهش یا حذف این سربار در اختیار قرار میدهد.
زمینههای قابل ارزیابی در زمان کامپایل و زمان ارزیابی قطعی، بههمراه برنامهنویسی فرابرنامهای مبتنی بر قالب، میتوانند بخش بزرگی از منطق و تشخیص نوع را به زمان کامپایل منتقل کنند؛ بهویژه زمانی که با معناشناسی جابهجایی برای اجتناب از کپیهای غیرضروری، درونخطیسازی کد برای افزایش سرعت اجرا، و حتی استفاده از مقادیر از پیش محاسبهشده در قالب جدولها ترکیب شوند.
با انتخاب صحیح تابعی که باید در زمان کامپایل فراخوانی شود، نوشتن کد به این شیوه هیچ جریمهای از نظر کارایی به همراه نخواهد داشت. اصطلاح رایجی که برای این دسته از تکنیکها بهکار میرود، «انتزاع بدون سربار» است؛ جایی که میتوان قابلیتهای پیچیدهتری را با حداقل یا حتی بدون هیچ سربار کارایی پیادهسازی کرد. در نتیجه، میتوان کد سمت کاربر را به شکلی کمتخصصیتر نوشت، بدون آنکه پیامد منفیای به همراه داشته باشد.
با وجود آنکه این وضعیت شبیه به یک دنیای ایدهآل به نظر میرسد، ممکن است موازنههایی در بخشهایی غیر از پیچیدگی کد و زمان اجرا وجود داشته باشد که در حال حاضر آنها را در نظر نگرفتهایم. در توسعهٔ سیپلاسپلاس، برخی قابلیتها و الگوهای طراحی میتوانند تأثیر قابلتوجهی بر اندازهٔ باینری داشته باشند. اندازهٔ باینری، بهویژه برای اپلیکیشنهای نهفته، بهدلیل محدودیتهای سختافزاری و هزینههای تولید، میتواند یک معیار بسیار مهم باشد. پیش از آنکه بررسی این قابلیتها را ادامه دهیم، بهتر است ابتدا درک کنیم چرا اندازهٔ باینری در برخی انواع سختافزار تا این حد یک محدودیت حیاتی به شمار میآید.
دیدگاه سختافزاری
پس از آنکه فایل باینری نهایی فریمور شما کامپایل شد، میتوانیم تحلیل را آغاز کنیم تا مشخص شود هر مؤلفه از کد منبع، هر تابع و سایر اجزا چه تعداد بایت در اندازهٔ کلی باینری سهم دارند. برای انجام این نوع تحلیل، ابزارهای متنباز و رایگانی در دسترس هستند که از جملهٔ آنها میتوان به Bloaty و Puncover اشاره کرد.
ابزار Bloaty یک ابزار خط فرمان است که اندازهٔ اجزای مختلف باینری را در سطوح گوناگون، مانند بخشها، قطعهها و واحدهای کامپایل، پروفایلگیری و مرتبسازی میکند. نتایج این تحلیل بهصورت فهرستهایی در ترمینال نمایش داده میشوند:

در مقابل، Puncover یک وبسرور محلی راهاندازی میکند تا نتایج تحلیل بهصورت گرافیکی نمایش داده شوند. این ابزار یک برنامه شبیه به «مرورگر فایل» ایجاد میکند که در آن سهم هر فایل منبع از اندازهٔ باینری نشان داده میشود. هر فایل منبع صفحهٔ اختصاصی خود را دارد که در آن کاربر میتواند کد اسمبلی تولیدشده و فهرستی از نمادها را مشاهده کند؛ فهرستی که بر اساس اندازهٔ پشته، اندازهٔ کد یا اندازهٔ ایستا مرتب شده است.

بهنوعی، این ابزارها مکمل یکدیگر هستند. Bloaty یک نمای کلی و سطح بالا از کل باینری ارائه میدهد و این کار باعث میشود شناسایی بخشهایی از باینری که بیشترین سهم را در افزایش اندازه دارند سریعتر انجام شود. سپس میتوان از Puncover برای بررسی عمیقتر مؤلفههای شناساییشده استفاده کرد و با مقایسهٔ فهرست نمادها یا تفاوتهای مستقیم در کد اسمبلی، تغییرات را بهتر درک کرد.
من یک مخزن عمومی شامل مطالعات موردی مختلف دربارهٔ تأثیر اندازهٔ باینری در توسعهٔ سیپلاسپلاس ایجاد کردهام که با نام cpp_binary_size در دسترس است. ابزارهای یادشده میتوانند برای مقایسهٔ باینریهای حاصل در هر مطالعهٔ موردی و شناسایی دلایل تفاوت در اندازهٔ باینری مورد استفاده قرار گیرند.
با مرور این مثالها، میتوان تأثیر اندازهٔ باینری را در جنبههای مختلف برنامهنویسی سیپلاسپلاس مشاهده کرد. این موضوع همچنین نشان میدهد که رویکردهای متفاوتی برای بهینهسازی اندازهٔ باینری در طول فرایند توسعه وجود دارد. در ادامه، چند نکتهٔ مهم که باید به آنها توجه شود آورده شده است:
اثر تصمیمها را در سطح رابطها بررسی کنید؛ مانند سازندهها و فراخوانی توابع. بهدنبال جاهایی باشید که کپیکردن یا تبدیل نوع غیرضروری انجام میشود. علاوه بر این، تحلیل کنید که این اثر با افزایش مقیاس چگونه تغییر میکند؛ برای مثال زمانی که تعداد اشیا و یا محلهای فراخوانی توابع افزایش مییابد. بهعنوان نمونه، ارسال یک char* به تابعی که امضای آن std::string یا حتی std::string& است، باعث تخصیص یک رشتهٔ موقت میشود. در نمونهای دیگر، استفاده از push_back یا emplace_back برای افزودن یک عنصر به یک بردار میتواند منجر به کپی یا جابهجایی آن عنصر شود. معمولاً کپیها کد بیشتری تولید میکنند و در نتیجه اندازهٔ باینری بزرگتری نسبت به جابهجاییها بهوجود میآورند.
استفاده از کتابخانهها را با پرچمهای کامپایل تنظیم کنید. هنگام اضافهکردن هر کتابخانهٔ شخص ثالث، لازم است با سیستم ساخت و سربرگهای پیکربندی آن آشنا شوید و بررسی کنید که گزینههای موجود چگونه ممکن است بر اندازهٔ باینری اثر بگذارند. در بسیاری از موارد، غیرفعالکردن قابلیتهای استفادهنشده میتواند صرفهجویی قابلتوجهی در اندازهٔ باینری ایجاد کند. برای مثال، بهجای استفاده از <format> در کتابخانهٔ استاندارد، کتابخانهٔ fmtlib که <format> بر اساس آن ساخته شده است، پرچمهای متعددی دارد که میتوانند اندازهٔ باینری را کاهش دهند.
این پرچمها نهتنها برخی قابلیتها را غیرفعال میکنند، بلکه از الگوریتمهای سادهتر استفاده میکنند یا میان اندازهٔ کوچکتر باینری و کارایی پایینتر نوعی موازنه برقرار میسازند. بهویژه در کتابخانهٔ استاندارد، آزمایش اشیا و کتابخانههای مختلفی که کارکرد مشابهی ارائه میدهند میتواند تأثیر زیادی داشته باشد. در آزمایشهای من مشاهده شد که استفاده از توابع موجود در <cstdio> بهجای <iostream> برای چاپ روی خروجی استاندارد، بیش از ۱۰۰ کیلوبایت از اندازهٔ باینری میکاهد؛ زیرا iostream تعداد زیادی رشتهٔ ایستا و همچنین کتابخانهٔ محلیسازی را با خود به همراه میآورد. اثر مشابهی در صرفهجویی حافظه هنگام استفاده از کتابخانهٔ جدید <print> در نسخهٔ ۲۰۲۳ سیپلاسپلاس نیز مشاهده میشود.
بهینهسازی برای اندازهٔ باینری نباید بهعنوان کاری در انتهای مسیر در نظر گرفته شود، بلکه باید بر طراحی کلی نیز اثر بگذارد. هنگام طراحی معماری اپلیکیشن، تصمیمهای طراحیای وجود دارند که میتوانند تأثیر بسیار زیادی بر اندازهٔ باینری داشته باشند، اما باید نسبت به پیامدهای احتمالی آنها در سایر جنبهها نیز هوشیار بود. برای مثال، استفاده از مفهومها که یکی از قابلیتهای نسخهٔ ۲۰۲۰ سیپلاسپلاس است بهجای چندریختی، میتواند با حذف توابع مجازی، لایههای واسط و مخربهای بزرگتر، مقدار قابلتوجهی از اندازهٔ باینری را کاهش دهد. با این حال، این رویکرد کدبیس را ناچار میکند که عمدتاً در فایلهای سرآیند متمرکز شود که نتیجهٔ آن افزایش زمان کامپایل و همچنین بازکامپایل شدن تمام واحدهای کامپایلی است که تحت تأثیر قرار میگیرند.
حتی در چارچوب یک طراحی مشخص نیز مهم است که نسخههای مختلف پیادهسازی آزمایش شوند. برای نمونه، حذف نوع یک الگوی رایج در برنامهنویسی نرمافزار است که امکان استفاده از نوعهای مشخص را از طریق یک رابط عمومی در زمان اجرا فراهم میکند و در نتیجه میتواند تورم باینری را کاهش دهد. این الگو میتواند از طریق وراثت، توابع ایستا یا یک جدول توابع پیادهسازی شود. میتوان آزمایشی طراحی کرد که در آن اثر اندازهٔ باینری با افزایش تعداد اشیای پایه و سپس افزایش تعداد نمونههای هر نوع شیء اندازهگیری شود و در نهایت بررسی شود که کدام پیادهسازی برای اندازهٔ اپلیکیشن مورد نظر مناسبتر است.
فارغ از تمام تحلیلهایی که روی کد منبع انجام میدهید، معماری هدفی که قصد استقرار اپلیکیشن روی آن را دارید نیز میتواند تأثیر قابلتوجهی بر اندازهٔ باینری داشته باشد. برای مثال، گاهی تغییرات طراحی مختلف در معماریهای ۶۴ بیتی به اندازهٔ باینری یکسانی منجر میشوند، زیرا دستورالعملهای این معماری قادر به پشتیبانی از آدرسهای بزرگتر هستند و در نتیجه نسبت به تغییرات در امضای توابع، مانند تعداد متغیرهای ورودی، حساسیت کمتری دارند. در مقابل، در معماری آرم، دستورالعملهای تامب میتوانند استاندارد یا گسترده باشند که دستورالعملهای گسترده فضای بیشتری در باینری اشغال میکنند. بنابراین، کامپایلر ممکن است حتی برای امضاهای کوچکتر تابع نیز ناچار شود ترکیب متفاوتی از دستورالعملهای استاندارد و گسترده را به کار گیرد و در نتیجه، ردپاهای متفاوتی از نظر اندازهٔ باینری برای هر تغییر طراحی ایجاد شود.
بهینهسازی اندازهٔ باینری
در نهایت، سیپلاسپلاس قادر است کدی بسیار کارآمد برای دستگاههای با محدودیت شدید حافظه تولید کند. با این حال، ابزارها و زمینهٔ اپلیکیشن نقش تعیینکنندهای دارند و نکات ظریف و تصمیمهای طراحی مهمی وجود دارند که باید به آنها توجه شود.
نخست، باید تنظیمات بهینه برای محیط ساخت پیدا شود. پرچمهای کامپایلر و پیونددهنده تأثیر بسیار زیادی بر اندازهٔ کل اپلیکیشن دارند. در برخی موارد، حتی بازسازی زنجیرهٔ ابزار یا کتابخانهٔ استاندارد از کد منبع با تنظیمات اختصاصی میتواند صرفهجویی بزرگی در اندازهٔ باینری ایجاد کند.
برای نمونه، میتوان کتابخانهٔ استاندارد را با پنهانسازی پیشفرض نمادها ساخت تا پیونددهنده بتواند کدی را که در فریمور نهایی استفاده نمیشود حذف کند.
دوم، باید به هزینهٔ اندازهٔ باینری قابلیتهای زبانی و کتابخانهای توجه شود. با استفاده از ابزارهای تحلیل، میتوان نمادها یا فایلهایی را که باعث تورم باینری شدهاند شناسایی کرد و سپس بهدنبال جایگزینها یا بهینهسازیها گشت.
همانطور که پیشتر اشاره شد، استفاده از ورودیوخروجی ساده بهجای جریانهای پیچیده میتواند اثر بسیار بزرگی داشته باشد. این مسئله تنها به کتابخانهٔ استاندارد محدود نمیشود، زیرا برای بسیاری از قابلیتها، روشهای پیادهسازی متفاوتی وجود دارد که هر یک ردپای باینری متفاوتی دارند.
انتخاب الگوریتمها و ساختارهای داده
انتخاب الگوریتمها و ساختارهای داده نیز نقش مهمی در اندازهٔ باینری ایفا میکند. الگوریتمهای سادهتر و ساختارهای دادهٔ سبکتر معمولاً کد کمتری تولید میکنند.
برای مثال، استفاده از یک حلقهٔ ساده برای جستوجو در یک مجموعه، در بسیاری از موارد باینری کوچکتری نسبت به استفاده از الگوریتمهای عمومی کتابخانهای تولید میکند. همچنین، استفاده از ساختارهای دادهٔ سادهتر میتواند به کاهش اندازهٔ کد منجر شود، حتی اگر در برخی موارد کارایی کمتری داشته باشند.
برنامهنویسی قالبی پیشرفته میتواند بخش بزرگی از بررسی نوعها را به زمان کامپایل منتقل کند و در نتیجه، کد زمان اجرا سادهتر و کوچکتر شود. زمینههای قابل ارزیابی در زمان کامپایل نیز میتوانند بسیاری از محاسبات را پیش از اجرا انجام دهند، هرچند ممکن است دادههای از پیش محاسبهشدهای را به باینری اضافه کنند.
ادغام اندازهٔ باینری در فرایند توسعه
در نهایت، اندازهٔ فریمور باید بهعنوان یک معیار خودکار در فرایند تحلیل کد و خط لولهٔ یکپارچهسازی مداوم ادغام شود. گزارش تغییرات اندازهٔ باینری به تیم توسعه اجازه میدهد تورم تدریجی اندازه را همزمان با افزودن قابلیتهای جدید رصد کند و در زمان مناسب واکنش نشان دهد.
جمعبندی
نرمافزار مدرن روی میکروکنترلرها تنها از نظر توسعهٔ قابلیتها چالشبرانگیز نیست، بلکه باید نسبت به اثر آن بر اندازهٔ باینری و ردپای حافظه نیز حساس بود. برخلاف سامانههای دسکتاپ و سرور، پلتفرمهای نهفته در مقایسه با رشد توان محاسباتی، افزایش بسیار محدودی در حافظهٔ در دسترس داشتهاند.
به همین دلیل، بهینهسازی اندازهٔ باینری یک معیار حیاتی در کل چرخهٔ توسعه است؛ از معماری و انتخاب کتابخانهها گرفته تا پیادهسازی نهایی. استفاده از ابزارهای تحلیل فریمور و درک اثر قابلیتهای سیپلاسپلاس بر اندازهٔ باینری به توسعهدهندگان کمک میکند رفتار اپلیکیشن خود را بهتر بشناسند.
در نهایت، کوچکتر کردن اپلیکیشن این امکان را فراهم میکند که قابلیتها و موارد استفادهٔ قدرتمندتری در همان محدودیتهای فریمور میزبانی شوند.
