اخبار محصول

۱۸٪ کامپایل سریع‌تر، ۰٪ افت سرعت

۸ دقیقه مطالعه
نمایه Santiago Aboy Solanes را مشاهده کنید مشاهده پروفایل چارلز مانگر
Santiago Aboy Solanes و Vladimír Marko

تیم Android Runtime (ART) زمان کامپایل را بدون به خطر انداختن کد کامپایل شده یا هرگونه پسرفت در حافظه اوج، 18٪ کاهش داده است. این بهبود بخشی از ابتکار عمل 2025 ما برای بهبود زمان کامپایل بدون قربانی کردن استفاده از حافظه یا کیفیت کد کامپایل شده بود.

Optimizing compile-time speed is crucial for ART. For example, when just-in-time (JIT) compiling it directly impacts the efficiency of applications and overall device performance. Faster compilations reduce the time before the optimizations kick in, leading to a smoother and more responsive user experience. Furthermore, for both JIT and ahead-of-time (AOT), improvements in compile-time speed translate to reduced resource consumption during the compilation process, benefiting battery life and device thermals, especially on lower-end devices.

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

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

Optimizing a compiler is always a game of trade-offs. You can't just get speed for free; you have to give something up. We set a very clear and challenging goal for ourselves: make the compiler faster, but do it without introducing memory regressions and, crucially, without degrading the quality of the code it produces. If the compiler is faster but the apps run slower, we've failed.

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

یافتن بهینه‌سازی‌های ارزشمند و ممکن

Before you can begin to optimize a metric, you have to be able to measure it. Otherwise, you can't ever be sure if you improved it or not. Luckily for us, compile time speed is fairly consistent as long as you take some precautions like using the same device you use for measuring before and after a change, and making sure you don't thermal throttle your device. On top of that, we also have deterministic measurements like compiler statistics that help us understand what's going on under the hood.

Since the resource we were sacrificing for these improvements was our development time, we wanted to be able to iterate as fast as we could. This meant that we grabbed a handful of representative apps (a mix of first-party apps, third-party apps, and the Android operating system itself) to prototype solutions. Later, we verified that the final implementation was worth it with both manual and automated testing in a widespread manner.

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

تصویر.png

نمونه‌ای از نمودار شعله‌ای یک پروفایل در pprof

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

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

تصویر.png

نمای پایین به بالای یک پروفایل

In our optimizing compiler, there's a phase called Global Value Numbering (GVN). You don't have to worry about what it does as a whole, but the relevant part is to know that it has a method called `Kill` that it will delete some nodes according to a filter. This is time consuming as it has to iterate through all the nodes and check one by one. We noticed that there are some cases in which we know in advance that the check will be false, no matter the nodes we have alive at that point. In these cases, we can skip iterating altogether , bringing it from 1.023% down to ~0.3% and improving GVN's runtime by ~15%.

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

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

Usually, in a case like the `Kill` one above we would take a look at how we iterate through the nodes and do it faster by, for example, doing things in parallel or improving the algorithm itself. In fact, that's what we tried at first and only when we couldn't find anything to do we had a “Wait a minute…” moment and saw that the solution was to (in some cases) not iterate at all! When doing these kinds of optimizations, it is easy to miss the forest for the trees.

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

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

از کجا بفهمیم که آیا بهینه‌سازی‌ها ارزش دنبال کردن را دارند یا خیر؟

That's the neat part, you don't. After detecting that an area is consuming a lot of compile time and after devoting development time to try to improve it, sometimes you can't just find a solution. Maybe there's nothing to do, it will take too long to implement, it will regress another metric significantly, increase code base complexity, etc. For every successful optimization that you can see in this blog post, know that there are countless others that just didn't come to fruition.

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

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

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

غواصی عمیق‌تر

بدون مقدمه چینی بیشتر، بیایید نگاهی به برخی از تغییراتی که اعمال کرده‌ایم بیندازیم.

We implemented a change to optimize a method called FindReferenceInfoOf. This method was doing a linear search of a vector to find an entry. We updated that data structure to be indexed by the instruction's id so that FindReferenceInfoOf would be O(1) instead of O(n). Also, we pre-allocated the vector to avoid resizing. We slightly increased memory as we had to add an extra field which counted how many entries we inserted in the vector, but it was a small sacrifice to make as the peak memory didn't increase. This sped up our LoadStoreAnalysis phase by 34-66% which in turns gives ~0.5-1.8% compile time improvement.

We have a custom implementation of HashSet that we use in several places. Creating this data structure was taking a considerable amount of time and we found out why. Many years ago, this data structure was used in only a few places that were using very big HashSets and it was tweaked to be optimized for that. However, nowadays it was used in the opposite direction with only a few entries and with a short lifespan. This meant that we were wasting cycles by creating this huge HashSet but we only used it for a few entries before discarding it. With this change , we improved ~1.3-2% of compile time. As an added bonus, memory usage decreased by ~0.5-1% since we weren't using as big data structures as before.

We improved ~0.5-1% of compile time by passing data structures by reference to the lambda to avoid copying them around. This was something that was missed in the original review and sat in our codebase for years. It was thanks to taking a look at the profiles in pprof that we noticed that these methods were creating and destroying a lot of data structures, which led us to investigate and optimize them.

We sped up the phase that writes the compiled output by caching computed values , which translated to ~1.3-2.8% of total compile time improvement. Sadly, the extra bookkeeping was too much and our automated testing alerted us of the memory regression. Later, we took a second look at the same code and implemented a new version which not only took care of the memory regression but also improved the compile time a further ~0.5-1.8%! In this second change we had to refactor and reimagine how this phase should work, in order to get rid of one of the two data structures.

We have a phase in our optimizing compiler which inlines function calls in order to get better performance. To choose which methods to inline we use both heuristics before we do any computation, and final checks after doing work but right before we finalize the inlining. If any of those detect that the inlining is not worth it (for example, too many new instructions would be added), then we don't inline the method call.

We moved two checks from the “final checks” category to the “heuristic” category to estimate whether an inlining will succeed or not before we do any time-expensive computation. Since this is an estimate it is not perfect, but we verified that our new heuristics cover 99.9% of what was inlined before without affecting performance. One of these new heuristics was about the needed DEX registers (~0.2-1.3% improvement), and the other one about number of instructions (~2% improvement).

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

Furthermore, the BitVectorView class was templatized on the underlying storage type (instead of always using uint32_t as the old BitVector). This allows some operations, for example Union(), to process twice as many bits together on 64-bit platforms. The samples of the affected functions were reduced by more than 1% in total when compiling the Android OS. This was done across several changes [ 1 , 2 , 3 , 4 , 5 , 6 ]

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

نتیجه‌گیری

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

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

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

    نوشته شده توسط:
    ادامه مطلب