اخبار محصول

‫٪۱۸ سریع‌تر همگردانی می‌کند، هیچ‌گونه مصالحه‌ای ندارد

‫۸ دقیقه خواندن

تیم زمان اجرای Android (ART) زمان کامپایل را بدون به‌خطر انداختن کد کامپایل‌شده یا هرگونه پسرفت حافظه اوج، ۱۸٪ کاهش داده است. این بهبود بخشی از طرح ابتکاری ما برای سال ۲۰۲۵ بود که هدف آن بهبود زمان ترجمه بدون فدا کردن استفاده از حافظه یا کیفیت کد ترجمه‌شده بود.

بهینه‌سازی سرعت زمان ترجمه برای ART بسیار مهم است. برای مثال، هنگام ترجمه هم‌زمان (JIT)، مستقیماً بر کارایی برنامه‌ها و عملکرد کلی دستگاه تأثیر می‌گذارد. گردآوری‌های سریع‌تر زمان قبل‌از شروع بهینه‌سازی‌ها را کاهش می‌دهد و درنتیجه تجربه کاربری روان‌تر و واکنش‌گراتری ارائه می‌شود. علاوه‌براین، هم برای JIT و هم برای AOT، بهبود سرعت زمان ترجمه به کاهش مصرف منابع درطول فرایند ترجمه منجر می‌شود که به‌نفع عمر باتری و حرارت دستگاه، به‌ویژه در دستگاه‌های پایین‌رده، است.

برخی‌از این بهبودهای سرعت زمان گردآوری در نسخه Android ژوئن ۲۰۲۵ راه‌اندازی شد و بقیه در نسخه پایان سال Android دردسترس قرار خواهد گرفت. علاوه‌براین، همه کاربران Android در نسخه‌های ۱۲ و بالاتر واجدشرایط دریافت این بهبودها ازطریق به‌روزرسانی‌های اصلی هستند.

درحال بهینه‌سازی کردن مترجم بهینه‌ساز

بهینه‌سازی یک کامپایلر همیشه یک بازی از مبادله است. نمی‌توانید سرعت را رایگان به‌دست آورید؛ باید چیزی را ازدست بدهید. هدف بسیار واضح و چالش‌برانگیزی برای خودمان تعیین کردیم: کامپایلر را سریع‌تر کنیم، اما این کار را بدون ایجاد پسرفت در حافظه و مهم‌تر از همه، بدون کاهش کیفیت کدی که تولید می‌کند انجام دهیم. اگر کامپایلر سریع‌تر باشد اما برنامه‌ها کندتر اجرا شوند، ما شکست خورده‌ایم.

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

درحال پیدا کردن بهینه‌سازی‌های ممکن ارزشمند

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

 

ازآنجایی‌که منبعی که برای این بهبودها قربانی می‌کردیم زمان توسعه ما بود، می‌خواستیم بتوانیم تا جایی که ممکن است سریع تکرار کنیم. این یعنی ما چند برنامه نماینده (ترکیبی از برنامه‌های طرف اول، برنامه‌های طرف سوم، و خود سیستم‌عامل Android) را برای نمونه‌سازی راهکارها انتخاب کردیم. بعداً، با آزمایش‌های دستی و خودکار در مقیاس گسترده، تأیید کردیم که پیاده‌سازی نهایی ارزشش را داشت.

 

با آن مجموعه apk دست‌چین‌شده، به‌صورت محلی کامپایل دستی را راه‌اندازی می‌کردیم، نمایه کامپایل را دریافت می‌کردیم، و از pprof برای دیداری‌سازی کردن اینکه زمانمان را کجا صرف می‌کنیم استفاده می‌کردیم.

image.png

مثالی از گراف شعله نمایه در pprof

ابزار pprof بسیار قدرتمند است و به ما امکان می‌دهد داده‌ها را برش دهیم، فیلتر کنیم، و مرتب کنیم تا ببینیم، برای مثال، کدام مراحل یا روش‌های کامپایلر بیشترین زمان را می‌گیرند. درباره خود pprof جزئیات ارائه نمی‌دهیم؛ فقط بدانید که اگر نوار بزرگ‌تر باشد، یعنی زمان بیشتری از گردآوری را گرفته است.

یکی از این نماها نمای «پایین به بالا» است که در آن می‌توانید ببینید کدام روش‌ها بیشترین زمان را می‌برند. در تصویر زیر، روشی به‌نام «کشتن» را می‌بینیم که بیش‌از ۱٪ از زمان ترجمه را به‌خود اختصاص داده است. برخی‌از روش‌های برتر دیگر نیز در ادامه پست وبلاگ مورد بحث قرار خواهند گرفت.

image.png

نمای پایین به بالای نمایه

در کامپایلر بهینه‌سازی ما، مرحله‌ای به‌نام «شماره‌گذاری مقدار سراسری» (GVN) وجود دارد. لازم نیست نگران عملکرد کلی آن باشید، اما بخش مربوطه این است که بدانید روشی به‌نام `Kill` دارد که براساس فیلتری برخی‌از گره‌ها را حذف می‌کند. این کار زمان‌بر است زیرا باید همه گره‌ها را تکرار کند و یکی‌یکی آن‌ها را بررسی کند. متوجه شدیم که در برخی موارد، ازقبل می‌دانیم که بررسی نادرست خواهد بود، صرف‌نظر از اینکه در آن زمان چه گره‌هایی فعال هستند. در این موارد، می‌توانیم به‌طورکلی از تکرار صرف‌نظر کنیم، آن را از ۱٫۰۲۳٪ به حدود ۰٫۳٪ کاهش دهیم و زمان اجرای GVN را حدود ۱۵٪ بهبود دهیم.

پیاده‌سازی بهینه‌سازی‌های ارزشمند

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

معمولاً در مواردی مانند `Kill` در بالا، به نحوه تکرار در گره‌ها نگاه می‌کنیم و با انجام کارها به‌صورت موازی یا بهبود خود الگوریتم، آن را سریع‌تر می‌کنیم. درواقع، این همان کاری بود که ابتدا انجام دادیم و فقط وقتی نتوانستیم کاری انجام دهیم، لحظه‌ای فکر کردیم و متوجه شدیم که راه‌حل این است که (در برخی موارد) اصلاً تکرار نکنیم! هنگام انجام این نوع بهینه‌سازی‌ها، به‌راحتی می‌توان از هدف اصلی غافل شد.

در موارد دیگر، از چند تکنیک مختلف استفاده کردیم، ازجمله:

  • استفاده از اکتشافی‌ها برای تصمیم‌گیری درباره اینکه آیا بهینه‌سازی نتایج ارزشمندی تولید خواهد کرد یا نه و بنابراین می‌تواند رد شود
  • استفاده از ساختارهای داده اضافی برای ذخیره کردن داده‌های محاسباتی در حافظه نهان
  • تغییر ساختارهای داده فعلی برای افزایش سرعت
  • محاسبه تنبل نتایج برای جلوگیری از دوره‌های تکراری در برخی موارد
  • از انتزاع صحیح استفاده کنید - ویژگی‌های غیرضروری می‌تواند کد را کند کند
  • از تعقیب اشاره‌گر پرکاربرد در میان بارگذاری‌های زیاد خودداری کنید

چگونه متوجه شویم که بهینه‌سازی‌ها ارزش پیگیری دارند؟

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

اگر در شرایط مشابهی هستید، سعی کنید تخمین بزنید که با انجام کمترین کار ممکن چقدر می‌توانید سنجه را بهبود دهید. این یعنی، به ترتیب:

  1. تخمین زدن با سنجه‌هایی که قبلاً جمع‌آوری کرده‌اید، یا فقط براساس احساس درونی
  2. تخمین زدن با نمونه نخستین سریع و ساده
  3. راه‌حلی را پیاده‌سازی کنید.

فراموش نکنید که معایب راه‌حلتان را تخمین بزنید. برای مثال، اگر می‌خواهید به ساختارهای داده اضافی تکیه کنید، چقدر حافظه می‌خواهید استفاده کنید؟

کاوش عمیق‌تر

بدون معطلی، بیایید به برخی‌از تغییراتی که پیاده‌سازی کرده‌ایم نگاهی بیندازیم.

تغییری برای بهینه‌سازی روشی به‌نام FindReferenceInfoOf پیاده‌سازی کردیم. این روش برای یافتن ورودی، جستجوی خطی بردار انجام می‌داد. آن ساختار داده را به‌روز کردیم تا براساس شناسه دستور نمایه شود و «FindReferenceInfoOf» به‌جای O(n) به O(1) تبدیل شود. همچنین، بردار را ازقبل تخصیص دادیم تا از تغییر اندازه جلوگیری کنیم. حافظه را کمی افزایش دادیم زیرا مجبور بودیم فیلد اضافه‌ای اضافه کنیم که تعداد ورودی‌هایی را که در بردار وارد کرده بودیم شمارش کند، اما این فداکاری کوچکی بود زیرا حافظه اوج افزایش نیافت. این کار فاز LoadStoreAnalysis ما را ۳۴ تا ۶۶٪ تسریع کرد که به نوبه خود باعث بهبود زمان ترجمه به میزان ۰٫۵ تا ۱٫۸٪ می‌شود.

ما پیاده‌سازی سفارشی از HashSet داریم که در چند جا از آن استفاده می‌کنیم. ایجاد این ساختار داده زمان قابل‌توجهی می‌برد و دلیل آن را پیدا کردیم. سال‌ها پیش، این ساختار داده فقط در چند مکان که از HashSets بسیار بزرگ استفاده می‌کردند استفاده می‌شد و برای بهینه‌سازی آن تغییراتی ایجاد شد. بااین‌حال، امروزه در جهت مخالف با تنها چند ورودی و با طول عمر کوتاه استفاده می‌شود. این یعنی با ایجاد این HashSet بزرگ، چرخه‌ها را هدر می‌دادیم اما فقط برای چند ورودی از آن استفاده می‌کردیم و سپس آن را دور می‌انداختیم. با این تغییر، زمان ترجمه را حدود ۱٫۳ تا ۲٪ بهبود دادیم. به‌عنوان یک مزیت اضافی، استفاده از حافظه حدود ۰٫۵ تا ۱٪ کاهش یافت زیرا از ساختارهای داده بزرگ مانند قبل استفاده نمی‌کردیم.

با گذراندن ساختارهای داده با مرجع به لامبدا برای جلوگیری از کپی کردن آن‌ها، زمان گردآوری را حدود ۰٫۵ تا ۱٪ بهبود دادیم. این چیزی بود که در مرور اصلی ازدست رفته بود و سال‌ها در پایگاه کد ما باقی ماند. به‌لطف نگاهی به نمایه‌های pprof متوجه شدیم که این روش‌ها ساختارهای داده زیادی ایجاد و ازبین می‌برند، که باعث شد آن‌ها را بررسی و بهینه‌سازی کنیم.

با ذخیره کردن مقادیر محاسبه‌شده، مرحله‌ای را که برونداد کامپایل‌شده را می‌نویسد تسریع کردیم، که به بهبود حدود ۱٫۳ تا ۲٫۸ درصدی در کل زمان کامپایل منجر شد. متأسفانه، دفترداری اضافی بیش‌ازحد بود و آزمایش خودکار ما به ما درباره پسرفت حافظه هشدار داد. بعداً، نگاهی دوباره به همان کد انداختیم و نسخه جدیدی را پیاده‌سازی کردیم که نه تنها از پس‌رفت حافظه جلوگیری می‌کرد، بلکه زمان کامپایل را نیز حدود ۰٫۵ تا ۱٫۸٪ بهبود می‌بخشید! در این تغییر دوم، برای اینکه یکی از دو ساختار داده را حذف کنیم، مجبور شدیم نحوه عملکرد این مرحله را بازسازی و بازطراحی کنیم.

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

دو بررسی را از دسته «بررسی‌های نهایی» به دسته «ابتکاری» منتقل کردیم تا قبل‌از انجام هرگونه محاسبات زمان‌بر، تخمین بزنیم که آیا یک درون‌خطی‌سازی موفق خواهد بود یا نه. ازآنجایی‌که این یک تخمین است، کامل نیست، اما تأیید کردیم که اکتشافات جدید ما ۹۹٫۹٪ از آنچه قبلاً درون‌خطی بود را بدون تأثیر بر عملکرد پوشش می‌دهد. یکی از این اکتشافات جدید درباره ثبت‌کننده‌های DEX موردنیاز بود (بهبود حدود ۰٫۲ تا ۱٫۳٪)، و دیگری درباره تعداد دستورالعمل‌ها (بهبود حدود ۲٪).

ما پیاده‌سازی سفارشی از BitVector داریم که در چندین جا از آن استفاده می‌کنیم. کلاس BitVector قابل‌تغییر اندازه را با BitVectorView ساده‌تری برای برخی‌از بردارهای بیتی با اندازه ثابت جایگزین کردیم. این کار برخی‌از غیرمستقیم‌ها و بررسی‌های محدوده زمان اجرا را حذف می‌کند و ساخت اشیای بردار بیت را سرعت می‌بخشد.

علاوه‌براین، کلاس BitVectorView براساس نوع فضای ذخیره‌سازی زیرین قالب‌بندی شده است (به‌جای اینکه همیشه از uint32_t به‌عنوان BitVector قدیمی استفاده کند). این کار به برخی‌از عملیات‌ها، برای نمونه Union()، اجازه می‌دهد تا دو برابر بیت‌ها را در پلاتفرم‌های ۶۴ بیتی با هم پردازش کنند. نمونه‌های توابع تحت‌تأثیر قرارگرفته هنگام ترجمه سیستم‌عامل Android در مجموع بیش‌از ۱٪ کاهش یافت. این کار در چندین تغییر انجام شد [۱، ۲، ۳، ۴، ۵، ۶]

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

نتیجه‌گیری

تعهد ما به بهبود سرعت زمان ترجمه ART منجر به پیشرفت‌های قابل‌توجهی شده است که باعث می‌شود Android روان‌تر و کارآمدتر شود و درعین‌حال به بهبود عمر باتری و عملکرد حرارتی دستگاه نیز کمک می‌کند. با شناسایی و پیاده‌سازی دقیق بهینه‌سازی‌ها، نشان داده‌ایم که بدون به‌خطر انداختن استفاده از حافظه یا کیفیت کد، می‌توان به بهبودهای قابل‌توجهی در زمان کامپایل دست یافت.

سفر ما شامل نمایه‌سازی با ابزارهایی مانند pprof، تمایل به تکرار، و گاهی حتی کنار گذاشتن مسیرهای کم‌ثمر بود. تلاش‌های جمعی تیم ART نه تنها زمان تدوین را به میزان قابل توجهی کاهش داده است، بلکه زمینه را برای پیشرفت‌های آینده نیز فراهم کرده است.

همه این بهبودها در به‌روزرسانی Android در پایان سال ۲۰۲۵ و برای Android 12 و بالاتر ازطریق به‌روزرسانی‌های اصلی دردسترس است. امیدواریم این بررسی دقیق فرایند بهینه‌سازی ما اطلاعات آماری ارزشمندی درباره پیچیدگی‌ها و مزایای مهندسی کامپایلر ارائه دهد!

ادامه خواندن