تیم زمان اجرای Android (ART) زمان ترجمه را بدون بهخطر انداختن کد ترجمهشده یا هرگونه پسرفت حافظه اوج ۱۸٪ کاهش داده است. این بهبود بخشی از طرح ابتکاری ما در سال ۲۰۲۵ برای بهبود زمان ترجمه بدون فدا کردن استفاده از حافظه یا کیفیت کد ترجمهشده بود.
بهینهسازی سرعت زمان ترجمه برای ART بسیار مهم است. برای مثال، هنگام ترجمه درجا (JIT)، این امر مستقیماً بر کارایی برنامهها و عملکرد کلی دستگاه تأثیر میگذارد. کامپایلهای سریعتر زمان قبلاز شروع بهینهسازیها را کاهش میدهد و منجر به تجربه کاربری روانتر و واکنشگراتر میشود. علاوهبراین، هم برای JIT و هم برای «پیشاز موعد» (AOT)، بهبود سرعت زمان ترجمه به کاهش مصرف منابع درطول فرایند ترجمه منجر میشود که برای عمر باتری و دمای دستگاه، بهویژه در دستگاههای ردهپایین، مفید است.
برخیاز این بهبودهای سرعت زمان ترجمه در نسخه ژوئن ۲۰۲۵ Android عرضه شده است و بقیه در نسخه پایان سال Android دردسترس قرار خواهد گرفت. علاوهبراین، همه کاربران Android در نسخههای ۱۲ و بالاتر واجدشرایط دریافت این بهبودها ازطریق بهروزرسانیهای اصلی هستند.
درحال بهینهسازی کردن مترجم بهینهساز
بهینهسازی کامپایلر همیشه بازی بدهبستان است. نمیتوانید سرعت را رایگان بهدست آورید؛ باید چیزی را فدا کنید. هدف بسیار واضح و چالشبرانگیزی برای خودمان تعیین کردیم: کامپایلر را سریعتر کنیم، اما این کار را بدون ایجاد پسرفت حافظه و، مهمتر از همه، بدون کاهش کیفیت کدی که تولید میکند انجام دهیم. اگر کامپایلر سریعتر باشد اما برنامهها کندتر اجرا شوند، ما شکست خوردهایم.
تنها منبعی که مایل بودیم صرف کنیم زمان توسعه خودمان بود تا عمیقاً کاوش کنیم، بررسی کنیم، و راهحلهای هوشمندانهای پیدا کنیم که این معیارهای سختگیرانه را برآورده کند. بیایید نگاهی دقیقتر به نحوه کارمان برای یافتن زمینههای بهبود و همچنین یافتن راهحلهای مناسب برای مشکلات مختلف بیندازیم.
یافتن بهینهسازیهای احتمالی ارزشمند
قبلاز اینکه بتوانید بهینهسازی یک سنجه را شروع کنید، باید بتوانید آن را اندازهگیری کنید. درغیراینصورت، هرگز نمیتوانید مطمئن شوید که آن را بهبود دادهاید یا نه. خوشبختانه، سرعت زمان ترجمه نسبتاً ثابت است، بهشرطی که برخی اقدامات احتیاطی را رعایت کنید، مثلاً از همان دستگاهی که برای اندازهگیری قبل و بعداز تغییر استفاده میکنید استفاده کنید و مطمئن شوید که دستگاهتان را بهدلیل افزایش دما محدود نمیکنید. علاوهبراین، ما اندازهگیریهای قطعی مانند آمار کامپایلر را نیز داریم که به ما کمک میکند تا بفهمیم در پشت صحنه چه اتفاقی میافتد.
ازآنجاییکه منبعی که برای این بهبودها فدا میکردیم زمان توسعه ما بود، میخواستیم بتوانیم تا جایی که میتوانیم سریع تکرار کنیم. این یعنی ما چند برنامه نمونه (ترکیبی از برنامههای طرف اول، برنامههای طرف سوم، و خود سیستمعامل Android) را برای نمونهسازی راهحلها انتخاب کردیم. بعداً با آزمایش دستی و خودکار بهطور گسترده تأیید کردیم که پیادهسازی نهایی ارزشش را داشت.
با آن مجموعه از فایلهای APK دستچینشده، یک کامپایل دستی را بهصورت محلی راهاندازی میکنیم، نمایهای از کامپایل دریافت میکنیم، و از pprof برای تجسم جایی که وقت خود را صرف میکنیم استفاده میکنیم.
نمونهای از نمودار شعلهای نمایه در pprof
ابزار pprof بسیار قدرتمند است و به ما امکان میدهد دادهها را برش دهیم، فیلتر کنیم، و مرتب کنیم تا ببینیم، برای مثال، کدام فازها یا روشهای کامپایلر بیشترین زمان را میبرند. وارد جزئیات pprof نمیشویم؛ فقط بدانید که اگر نوار بزرگتر باشد، یعنی زمان بیشتری برای کامپایل صرف شده است.
یکی از این نماها، نمای «پایین به بالا» است که در آن میتوانید ببینید کدام روشها بیشترین زمان را میگیرند. در تصویر زیر، میتوانیم روشی بهنام Kill را ببینیم که بیشاز ۱٪ زمان ترجمه را به خود اختصاص داده است. برخیاز روشهای برتر دیگر نیز بعداً در پست وبلاگ مورد بحث قرار خواهند گرفت.
نمای پایین به بالای نمایه
در کامپایلر بهینهسازی ما، مرحلهای بهنام «شمارهگذاری مقدار سراسری» (GVN) وجود دارد. لازم نیست نگران عملکرد کلی آن باشید، اما بخش مرتبط این است که بدانید روشی به نام `Kill` دارد که برخیاز گرهها را براساس فیلتر حذف میکند. این کار زمانبر است زیرا باید همه گرهها را تکرار کند و یکییکی بررسی کند. متوجه شدیم که در برخی موارد ازقبل میدانیم که بررسی نادرست خواهد بود، صرفنظر از اینکه در آن نقطه چه گرههایی فعال باشند. در این موارد، میتوانیم از تکرار بهطور کامل صرفنظر کنیم، آن را از ۱٫۰۲۳٪ به حدود ۰٫۳٪ کاهش دهیم و زمان اجرای GVN را حدود ۱۵٪ بهبود بخشیم.
اجرای بهینهسازیهای ارزشمند
ما نحوه اندازهگیری و نحوه تشخیص اینکه زمان کجا صرف میشود را پوشش دادیم، اما این تازه شروع کار است. مرحله بعدی این است که چگونه زمان صرفشده برای کامپایل کردن را بهینه کنید.
معمولاً در موردی مانند `Kill` در بالا، نگاهی به نحوه تکرار گرهها میاندازیم و آن را سریعتر انجام میدهیم، مثلاً با انجام کارها بهصورت موازی یا بهبود خود الگوریتم. درواقع، این همان چیزی است که در ابتدا امتحان کردیم و فقط زمانی که نتوانستیم کاری برای انجام دادن پیدا کنیم، لحظهای «صبر کنید…» داشتیم و دیدیم که راهحل این است که (در برخی موارد) اصلاً تکرار نکنیم! هنگام انجام این نوع بهینهسازیها، بهراحتی ممکن است بهدلیل توجه به جزئیات، از تصویر کلی غافل شوید.
در موارد دیگر، از چند تکنیک مختلف استفاده کردیم، ازجمله:
- استفاده از روشهای اکتشافی برای تصمیمگیری درباره اینکه آیا بهینهسازی نتایج ارزشمندی تولید نخواهد کرد و بنابراین میتوان از آن صرفنظر کرد
- استفاده از ساختارهای داده اضافی برای ذخیره دادههای محاسبهشده در حافظه نهان
- تغییر ساختارهای داده فعلی برای افزایش سرعت
- محاسبه تنبل نتایج برای جلوگیری از چرخهها در برخی موارد
- از انتزاع مناسب استفاده کنید - ویژگیهای غیرضروری میتوانند کد را کند کنند
- از تعقیب اشارهگر پرکاربرد در بارگیریهای متعدد اجتناب کنید
چگونه بفهمیم که بهینهسازیها ارزش دنبال کردن دارند؟
نکته جالب این است که لازم نیست این کار را بکنید. پساز تشخیص اینکه بخشی زمان ترجمه زیادی مصرف میکند و پساز اختصاص دادن زمان توسعه برای تلاش در جهت بهبود آن، گاهی اوقات نمیتوانید راهحلی پیدا کنید. شاید کاری برای انجام دادن وجود نداشته باشد، پیادهسازی آن خیلی طول بکشد، سنجه دیگری را بهطور قابلتوجهی پسرفت دهد، پیچیدگی پایگاه کد را افزایش دهد، و غیره. برای هر بهینهسازی موفقی که در این پست وبلاگ میبینید، بدانید که بیشمار بهینهسازی دیگر وجود دارد که به نتیجه نرسیده است.
اگر در موقعیت مشابهی هستید، سعی کنید با انجام کمترین کار ممکن، تخمین بزنید که چقدر میتوانید سنجه را بهبود ببخشید. این یعنی بهترتیب:
- برآورد کردن با سنجههایی که قبلاً جمعآوری کردهاید، یا فقط براساس حدس و گمان
- برآورد کردن با نمونه اولیه سریع و کثیف
- راهحلی را اجرا کنید.
فراموش نکنید که معایب راهحل خود را نیز برآورد کنید. برای مثال، اگر قرار است به ساختارهای داده اضافی تکیه کنید، چقدر حافظه میخواهید استفاده کنید؟
کاوش عمیقتر
بدون مقدمه، بیایید به برخیاز تغییراتی که اعمال کردهایم نگاهی بیندازیم.
تغییری برای بهینهسازی روشی بهنام FindReferenceInfoOf اعمال کردیم. این روش برای پیدا کردن ورودی، جستجوی خطی بردار انجام میداد. ساختار آن داده را بهروز کردیم تا با شناسه دستورالعمل نمایه شود تا FindReferenceInfoOf بهجای O(n) برابر با O(1) باشد. همچنین، بردار را ازقبل اختصاص دادیم تا از تغییر اندازه جلوگیری شود. حافظه را کمی افزایش دادیم چون مجبور بودیم فیلد اضافهای اضافه کنیم که تعداد ورودیهای درجشده در بردار را میشمرد، اما این فداکاری کوچکی بود چون اوج حافظه افزایش نیافت. این کار سرعت مرحله LoadStoreAnalysis ما را ۳۴ تا ۶۶ درصد افزایش داد که بهنوبه خود باعث بهبود حدود ۰٫۵ تا ۱٫۸ درصدی زمان ترجمه میشود.
ما یک پیادهسازی سفارشی از HashSet داریم که در چندین مکان از آن استفاده میکنیم. ایجاد این ساختار داده زمان قابلتوجهی میبرد و ما دلیل آن را پیدا کردیم. سالها پیش، این ساختار داده فقط در چند مکان که از HashSets بسیار بزرگ استفاده میکردند استفاده میشد و برای بهینهسازی آن تغییراتی در آن ایجاد شد. بااینحال، امروزه در جهت مخالف با تنها چند ورودی و با طول عمر کوتاه استفاده میشود. این یعنی ما با ایجاد این HashSet بزرگ چرخهها را هدر میدادیم اما فقط برای چند ورودی از آن استفاده میکردیم و سپس آن را دور میانداختیم. با این تغییر، زمان ترجمه را حدود ۱٫۳ تا ۲ درصد بهبود بخشیدیم. بهعنوان یک مزیت اضافی، استفاده از حافظه حدود ۰٫۵ تا ۱ درصد کاهش یافت زیرا دیگر از ساختارهای داده بزرگ مانند گذشته استفاده نمیکردیم.
با ارسال ساختارهای داده با ارجاع به لامبدا برای جلوگیری از کپی کردن آنها، زمان ترجمه را حدود ۰٫۵ تا ۱ درصد بهبود دادیم. این چیزی بود که در بازبینی اصلی ازقلم افتاده بود و سالها در پایگاه کد ما باقی مانده بود. بهلطف نگاهی که به نمایهها در pprof انداختیم متوجه شدیم که این روشها ساختارهای داده زیادی را ایجاد و تخریب میکنند، که باعث شد آنها را بررسی و بهینهسازی کنیم.
با ذخیره کردن مقادیر محاسبهشده در حافظه نهان، فازی را که خروجی کامپایلشده را مینویسد تسریع کردیم که منجر به بهبود حدود ۱٫۳ تا ۲٫۸ درصدی کل زمان کامپایل شد. متأسفانه، این ثبت اضافی بیشاز حد بود و آزمایش خودکار ما درباره پسرفت حافظه به ما هشدار داد. بعداً، نگاهی دوباره به همان کد انداختیم و نسخه جدیدی را پیادهسازی کردیم که نهتنها مشکل پسرفت حافظه را برطرف کرد، بلکه زمان ترجمه را نیز حدود ۰٫۵ تا ۱٫۸ درصد بهبود بخشید! در این تغییر دوم، مجبور شدیم نحوه عملکرد این مرحله را بازسازی و بازانگاری کنیم تا از یکی از دو ساختار داده خلاص شویم.
ما در کامپایلر بهینهسازی خود مرحلهای داریم که فراخوانیهای تابع را بهصورت درونخطی انجام میدهد تا عملکرد بهتری داشته باشیم. برای انتخاب روشهای درونخطی، هم از اکتشافات قبلاز انجام هرگونه محاسبه استفاده میکنیم و هم از بررسیهای نهایی پساز انجام کار اما درست قبلاز نهایی کردن درونخطی. اگر هریک از اینها تشخیص دهد که درونخطی کردن ارزشش را ندارد (برای مثال، دستورالعملهای جدید زیادی اضافه خواهد شد)، فراخوانی روش را درونخطی نمیکنیم.
دو بررسی را از دسته «بررسیهای نهایی» به دسته «اکتشافی» منتقل کردیم تا قبلاز انجام هرگونه محاسبات زمانبر، تخمین بزنیم که آیا درونخطیسازی موفقیتآمیز خواهد بود یا نه. ازآنجاییکه این یک تخمین است، کامل نیست، اما ما تأیید کردیم که اکتشافات جدید ما ۹۹٫۹٪ از آنچه را که قبلاً درونخطی شده بود، بدون تأثیر بر عملکرد پوشش میدهد. یکی از این اکتشافیهای جدید درباره ثبتهای DEX موردنیاز (بهبود حدود ۰٫۲ تا ۱٫۳ درصد) و دیگری درباره تعداد دستورالعملها (بهبود حدود ۲ درصد) بود.
ما پیادهسازی سفارشی BitVector داریم که در چندین مکان از آن استفاده میکنیم. کلاس BitVector با قابلیت تغییر اندازه را با BitVectorView سادهتر برای برخیاز بردارهای بیتی با اندازه ثابت جایگزین کردیم. این کار برخیاز ارجاعهای غیرمستقیم و بررسیهای محدوده زمان اجرا را حذف میکند و ساخت اشیای بردار بیت را سرعت میبخشد.
علاوهبراین، کلاس BitVectorView روی نوع ذخیرهسازی زیربنایی قالببندی شد (بهجای اینکه همیشه از uint32_t بهعنوان BitVector قدیمی استفاده شود). این کار به برخی عملیاتها، برای مثال Union()، اجازه میدهد تا دو برابر بیتهای بیشتری را با هم در پلاتفرمهای ۶۴ بیتی پردازش کنند. نمونههای توابع تحتتأثیرقرارگرفته درمجموع بیشاز ۱٪ درطول کامپایل کردن سیستمعامل Android کاهش یافت. این کار در چندین تغییر انجام شد [۱، ۲، ۳، ۴، ۵، ۶]
اگر بخواهیم درباره همه بهینهسازیها با جزئیات صحبت کنیم، تمام روز را باید اینجا باشیم! اگر به بهینهسازیهای بیشتری علاقه دارید، به برخی تغییرات دیگری که اعمال کردهایم نگاهی بیندازید:
- دفترداری را اضافه کنید تا زمانهای ترجمه حدود ۰٫۶ تا ۱٫۶ درصد بهبود یابد.
- درصورت امکان، برای جلوگیری از چرخهها، دادهها را بهصورت تنبل محاسبه کنید.
- کد ما را بازسازی کنید تا کار پیشمحاسبه را وقتی استفاده نمیشود رد کند.
- از برخی زنجیرههای بار وابسته اجتناب کنید وقتی که تخصیصدهنده بهراحتی از جاهای دیگر قابل دریافت است.
- مورد دیگری از افزودن بررسی برای جلوگیری از کار غیرضروری.
- از انشعاب مکرر در نوع ثبات (core/FP) در تخصیصدهنده ثبات اجتناب کنید.
- مطمئن شوید برخیآرایهها در زمان ترجمه مقداردهی اولیه شده باشند. برای انجام این کار به clang تکیه نکنید.
- برخیاز حلقهها را پاکسازی کنید. از حلقههای محدوده استفاده کنید که clang میتواند بهتر بهینهسازی کند زیرا بهدلیل عوارض جانبی حلقه نیازی به بار کردن مجدد اشارهگرهای داخلی محفظه ندارد. از فراخوانی تابع مجازی `HInstruction::GetInputRecords()` در حلقه ازطریق `InputAt(.)` درونخطی برای هر ورودی اجتناب کنید.
- با بهرهبرداری از بهینهسازی کامپایلر، از توابع Accept() برای الگوی بازدیدکننده اجتناب کنید.
نتیجهگیری
تعهد ما به بهبود سرعت زمان ترجمه ART منجر به بهبودهای قابلتوجهی شده است که Android را روانتر و کارآمدتر میکند و درعینحال به بهبود عمر باتری و دمای دستگاه کمک میکند. با شناسایی و اجرای دقیق بهینهسازیها، نشان دادهایم که دستاوردهای قابلتوجهی در زمان ترجمه بدون بهخطر انداختن استفاده از حافظه یا کیفیت کد امکانپذیر است.
سفر ما شامل نمایهسازی با ابزارهایی مانند pprof، تمایل به تکرار، و گاهی اوقات حتی رها کردن مسیرهای کمثمر بود. تلاشهای جمعی تیم ART نه تنها زمان ترجمه را به میزان قابلتوجهی کاهش داده است، بلکه زمینهساز پیشرفتهای آینده نیز شده است.
همه این بهبودها در بهروزرسانی Android پایان سال ۲۰۲۵ و برای Android 12 و بالاتر ازطریق بهروزرسانیهای خط اصلی دردسترس است. امیدواریم این بررسی عمیق فرایند بهینهسازی ما اطلاعات آماری ارزشمندی درباره پیچیدگیها و پاداشهای مهندسی کامپایلر ارائه دهد!
-
اخبار محصولامروز در «نشست تولیدکنندگان نرمافزار Android»، دردسترس بودن گسترده بسیاری از ابزارها و میاناهای برنامهسازی کاربردی را اعلام میکنیم که برای حذف اصطکاک روزمره و ارائه انعطافپذیری بیشتر به شما برای ساختن برنامههای Android عالی طراحی شدهاند.
Matthew McCullough • زمان خواندن: ۴ دقیقه -
اخبار محصولAndroid Studio Rabbit 2 اکنون پایدار است و برای استفاده در تولید آماده است. این نسخه شامل ویژگیهای کلیدی برای بهبود توسعه شما است، ازجمله Bring Your Own Agent، ویژگیهای جدید «شبیهساز واقعیت گسترده»، بهروزرسانی LeakCanary، ابزار جدید مدیریت مهارتهای عامل، بازبینی کد با هوش مصنوعی، و موارد دیگر.
Matthew Warner • ۳ دقیقه خواندن -
اخبار محصولبرنامههایی که نسخه آتی Android را هدف قرار میدهند دیگر نمیتوانند با اجازه WRITE_CONTACTS مستقیماً در مخاطبین بنویسند.
Roxanna Aliabadi Walker • ۶ دقیقه خواندن
جدیدترین اطلاعات آماری توسعه Android را بهصورت هفتگی در صندوق ورودیتان دریافت کنید.