Tim Android Runtime (ART) telah mengurangi waktu kompilasi sebesar 18% tanpa mengorbankan kode yang dikompilasi atau regresi memori puncak. Peningkatan ini adalah bagian dari inisiatif kami pada tahun 2025 untuk meningkatkan waktu kompilasi tanpa mengorbankan penggunaan memori atau kualitas kode yang dikompilasi.
Mengoptimalkan kecepatan waktu kompilasi sangat penting untuk ART. Misalnya, saat kompilasi just-in-time (JIT) secara langsung memengaruhi efisiensi aplikasi dan performa perangkat secara keseluruhan. Kompilasi yang lebih cepat mengurangi waktu sebelum pengoptimalan diterapkan, sehingga menghasilkan pengalaman pengguna yang lebih lancar dan responsif. Selain itu, untuk JIT dan ahead-of-time (AOT), peningkatan kecepatan waktu kompilasi akan mengurangi konsumsi resource selama proses kompilasi, sehingga menguntungkan masa pakai baterai dan suhu perangkat, terutama pada perangkat kelas bawah.
Beberapa peningkatan kecepatan waktu kompilasi ini diluncurkan dalam rilis Android Juni 2025, dan sisanya akan tersedia dalam rilis Android akhir tahun. Selain itu, semua pengguna Android versi 12 dan yang lebih baru memenuhi syarat untuk menerima peningkatan ini melalui update mainline.
Mengoptimalkan compiler pengoptimalan
Mengoptimalkan compiler selalu merupakan permainan kompromi. Anda tidak bisa mendapatkan kecepatan secara gratis; Anda harus mengorbankan sesuatu. Kami menetapkan sasaran yang sangat jelas dan menantang bagi diri kami sendiri: membuat compiler lebih cepat, tetapi melakukannya tanpa menimbulkan regresi memori dan, yang terpenting, tanpa menurunkan kualitas kode yang dihasilkannya. Jika compiler lebih cepat, tetapi aplikasi berjalan lebih lambat, berarti kita gagal.
Satu-satunya resource yang bersedia kami gunakan adalah waktu pengembangan kami sendiri untuk menggali lebih dalam, menyelidiki, dan menemukan solusi cerdas yang memenuhi kriteria ketat ini. Mari kita lihat lebih dekat cara kami berupaya menemukan area yang perlu ditingkatkan, serta menemukan solusi yang tepat untuk berbagai masalah.
Menemukan kemungkinan pengoptimalan yang bermanfaat
Sebelum dapat mulai mengoptimalkan metrik, Anda harus dapat mengukurnya. Jika tidak, Anda tidak akan pernah bisa memastikan apakah Anda telah meningkatkannya atau tidak. Untungnya, kecepatan waktu kompilasi cukup konsisten selama Anda melakukan beberapa tindakan pencegahan seperti menggunakan perangkat yang sama untuk mengukur sebelum dan setelah perubahan, serta memastikan Anda tidak melakukan throttling termal pada perangkat. Selain itu, kami juga memiliki pengukuran deterministik seperti statistik compiler yang membantu kami memahami apa yang terjadi di balik layar.
Karena resource yang kami korbankan untuk peningkatan ini adalah waktu pengembangan, kami ingin dapat melakukan iterasi secepat mungkin. Artinya, kami mengambil beberapa aplikasi representatif (gabungan aplikasi pihak pertama, aplikasi pihak ketiga, dan sistem operasi Android itu sendiri) untuk membuat prototipe solusi. Kemudian, kami memverifikasi bahwa implementasi akhir tersebut sepadan dengan pengujian manual dan otomatis secara luas.
Dengan kumpulan APK yang dipilih secara manual tersebut, kami akan memicu kompilasi manual secara lokal, mendapatkan profil kompilasi, dan menggunakan pprof untuk memvisualisasikan tempat kami menghabiskan waktu.
Contoh grafik flame profil di pprof
Alat pprof sangat canggih dan memungkinkan kita mengiris, memfilter, dan mengurutkan data untuk melihat, misalnya, fase atau metode compiler mana yang paling banyak menghabiskan waktu. Kami tidak akan membahas pprof itu sendiri secara mendetail; ketahuilah bahwa jika panelnya lebih besar, berarti kompilasi memerlukan lebih banyak waktu.
Salah satu tampilan ini adalah tampilan “bottom up” yang menunjukkan metode mana yang paling banyak menghabiskan waktu. Pada gambar di bawah, kita dapat melihat metode yang disebut Kill, yang menyumbang lebih dari 1% waktu kompilasi. Beberapa metode teratas lainnya juga akan dibahas nanti dalam postingan blog ini.
Tampilan bottom-up profil
Dalam compiler pengoptimalan kami, ada fase yang disebut Global Value Numbering (GVN). Anda tidak perlu khawatir tentang fungsinya secara keseluruhan, tetapi bagian yang relevan adalah mengetahui bahwa class ini memiliki metode bernama `Kill` yang akan menghapus beberapa node sesuai dengan filter. Hal ini memakan waktu karena harus melakukan iterasi melalui semua node dan memeriksa satu per satu. Kami melihat bahwa ada beberapa kasus saat kami tahu sebelumnya bahwa pemeriksaan akan salah, apa pun node yang aktif pada saat itu. Dalam kasus ini, kita dapat melewati iterasi sepenuhnya, sehingga menurunkannya dari 1,023% menjadi ~0,3% dan meningkatkan runtime GVN sekitar 15%.
Menerapkan pengoptimalan yang bermanfaat
Kami telah membahas cara mengukur dan cara mendeteksi tempat waktu dihabiskan, tetapi ini baru permulaan. Langkah berikutnya adalah cara mengoptimalkan waktu yang dihabiskan untuk mengompilasi.
Biasanya, dalam kasus seperti `Kill` di atas, kita akan melihat cara melakukan iterasi melalui node dan melakukannya lebih cepat, misalnya, dengan melakukan berbagai hal secara paralel atau meningkatkan algoritma itu sendiri. Bahkan, itulah yang kami coba pada awalnya dan hanya ketika kami tidak dapat menemukan apa pun untuk dilakukan, kami memiliki momen “Tunggu sebentar…” dan melihat bahwa solusinya adalah (dalam beberapa kasus) tidak melakukan iterasi sama sekali. Saat melakukan pengoptimalan semacam ini, Anda mungkin akan melewatkan gambaran besarnya karena terlalu fokus pada detail kecil.
Dalam kasus lain, kami menggunakan beberapa teknik berbeda, termasuk:
- menggunakan heuristik untuk memutuskan apakah pengoptimalan akan gagal memberikan hasil yang bermanfaat sehingga dapat dilewati
- menggunakan struktur data tambahan untuk meng-cache data yang dihitung
- mengubah struktur data saat ini untuk meningkatkan kecepatan
- menghitung hasil secara lambat untuk menghindari siklus dalam beberapa kasus
- gunakan abstraksi yang tepat - fitur yang tidak diperlukan dapat memperlambat kode
- menghindari mengejar pointer yang sering digunakan melalui banyak pemuatan
Bagaimana kita tahu apakah pengoptimalan tersebut layak dilakukan?
Itulah bagian yang menarik, Anda tidak perlu melakukannya. Setelah mendeteksi bahwa suatu area menghabiskan banyak waktu kompilasi dan setelah mencurahkan waktu pengembangan untuk mencoba meningkatkannya, terkadang Anda tidak dapat menemukan solusi. Mungkin tidak ada yang perlu dilakukan, akan memerlukan waktu terlalu lama untuk diterapkan, akan menurunkan metrik lain secara signifikan, meningkatkan kompleksitas code base, dll. Untuk setiap pengoptimalan yang berhasil yang dapat Anda lihat di postingan blog ini, ketahuilah bahwa ada banyak pengoptimalan lain yang tidak membuahkan hasil.
Jika Anda berada dalam situasi yang sama, cobalah memperkirakan seberapa besar Anda akan meningkatkan metrik dengan melakukan pekerjaan sesedikit mungkin. Artinya, secara berurutan:
- Memperkirakan dengan metrik yang telah Anda kumpulkan, atau hanya berdasarkan intuisi
- Memperkirakan dengan prototipe cepat dan sederhana
- Menerapkan solusi.
Jangan lupa untuk mempertimbangkan perkiraan kekurangan solusi Anda. Misalnya, jika Anda akan mengandalkan struktur data tambahan, berapa banyak memori yang bersedia Anda gunakan?
Memeriksa lebih dalam
Tanpa berlama-lama lagi, mari kita lihat beberapa perubahan yang kami terapkan.
Kami menerapkan perubahan untuk mengoptimalkan metode yang disebut FindReferenceInfoOf. Metode ini melakukan penelusuran linear vektor untuk menemukan entri. Kami memperbarui struktur data tersebut agar diindeks oleh ID petunjuk sehingga FindReferenceInfoOf akan menjadi O(1), bukan O(n). Selain itu, kami telah mengalokasikan vektor terlebih dahulu untuk menghindari pengubahan ukuran. Kami sedikit meningkatkan memori karena harus menambahkan kolom tambahan yang menghitung jumlah entri yang kami masukkan ke dalam vektor, tetapi ini adalah pengorbanan kecil karena memori puncak tidak meningkat. Hal ini mempercepat fase LoadStoreAnalysis kami sebesar 34-66% yang pada gilirannya memberikan peningkatan waktu kompilasi sekitar 0,5-1,8%.
Kami memiliki implementasi kustom HashSet yang kami gunakan di beberapa tempat. Pembuatan struktur data ini memerlukan waktu yang cukup lama dan kami mengetahui alasannya. Bertahun-tahun yang lalu, struktur data ini hanya digunakan di beberapa tempat yang menggunakan HashSet yang sangat besar dan diubah agar dioptimalkan untuk hal tersebut. Namun, saat ini tabel tersebut digunakan dengan arah yang berlawanan dengan hanya beberapa entri dan dengan masa aktif yang singkat. Artinya, kita membuang-buang siklus dengan membuat HashSet yang sangat besar ini, tetapi kita hanya menggunakannya untuk beberapa entri sebelum menghapusnya. Dengan perubahan ini, kami meningkatkan waktu kompilasi sekitar 1,3-2%. Sebagai bonus tambahan, penggunaan memori menurun sekitar 0,5-1% karena kami tidak menggunakan struktur data sebesar sebelumnya.
Kami meningkatkan waktu kompilasi sekitar 0,5-1% dengan meneruskan struktur data berdasarkan referensi ke lambda untuk menghindari penyalinan. Hal ini terlewatkan dalam peninjauan awal dan tetap ada di codebase kami selama bertahun-tahun. Berkat melihat profil di pprof, kami menyadari bahwa metode ini membuat dan menghancurkan banyak struktur data, yang mengarahkan kami untuk menyelidiki dan mengoptimalkannya.
Kami mempercepat fase yang menulis output yang dikompilasi dengan meng-cache nilai yang dihitung, yang menghasilkan peningkatan total waktu kompilasi sekitar 1,3-2,8%. Sayangnya, pembukuan tambahan tersebut terlalu banyak dan pengujian otomatis kami memberi tahu kami tentang regresi memori. Kemudian, kami melihat kembali kode yang sama dan menerapkan versi baru yang tidak hanya menangani regresi memori, tetapi juga meningkatkan waktu kompilasi lebih lanjut sebesar ~0,5-1,8%. Dalam perubahan kedua ini, kami harus melakukan refaktorisasi dan membayangkan ulang cara kerja fase ini, untuk menghilangkan salah satu dari dua struktur data.
Kami memiliki fase dalam compiler pengoptimalan yang menyisipkan panggilan fungsi untuk mendapatkan performa yang lebih baik. Untuk memilih metode yang akan di-inline, kami menggunakan kedua heuristik sebelum melakukan komputasi, dan pemeriksaan akhir setelah melakukan pekerjaan, tetapi tepat sebelum menyelesaikan inlining. Jika salah satu dari keduanya mendeteksi bahwa inlining tidak sepadan (misalnya, terlalu banyak petunjuk baru yang akan ditambahkan), kami tidak akan melakukan inlining pada panggilan metode.
Kami memindahkan dua pemeriksaan dari kategori “pemeriksaan akhir” ke kategori “heuristik” untuk memperkirakan apakah inlining akan berhasil atau tidak sebelum kami melakukan komputasi yang memakan waktu. Karena ini adalah perkiraan, hasilnya tidak sempurna, tetapi kami memverifikasi bahwa heuristik baru kami mencakup 99,9% dari apa yang di-inline sebelumnya tanpa memengaruhi performa. Salah satu heuristik baru ini adalah tentang register DEX yang diperlukan (peningkatan ~0,2-1,3%), dan yang lainnya tentang jumlah petunjuk (peningkatan ~2%).
Kami memiliki implementasi kustom BitVector yang kami gunakan di beberapa tempat. Kami mengganti class BitVector yang dapat diubah ukurannya dengan BitVectorView yang lebih sederhana untuk vektor bit berukuran tetap tertentu. Hal ini menghilangkan beberapa pengalihan dan pemeriksaan rentang run-time serta mempercepat pembuatan objek vektor bit.
Selain itu, class BitVectorView dibuat template pada jenis penyimpanan yang mendasarinya (bukan selalu menggunakan uint32_t sebagai BitVector lama). Hal ini memungkinkan beberapa operasi, misalnya Union(), memproses bit dua kali lebih banyak secara bersamaan di platform 64-bit. Sampel fungsi yang terpengaruh berkurang lebih dari 1% secara total saat mengompilasi Android OS. Hal ini dilakukan melalui beberapa perubahan [1, 2, 3, 4, 5, 6]
Jika kita membahas semua pengoptimalan secara mendetail, kita akan menghabiskan waktu seharian di sini. Jika Anda tertarik dengan pengoptimalan lainnya, lihat beberapa perubahan lain yang kami terapkan:
- Menambahkan pembukuan untuk meningkatkan waktu kompilasi sekitar 0,6-1,6%.
- Komputasi data secara lambat untuk menghindari siklus, jika memungkinkan.
- Merefaktorisasi kode kita untuk melewati pekerjaan pra-komputasi saat tidak akan digunakan.
- Hindari beberapa rantai pemuatan dependen jika pengalokasi dapat diperoleh dengan mudah dari tempat lain.
- Kasus lain penambahan pemeriksaan untuk menghindari pekerjaan yang tidak perlu.
- Hindari percabangan yang sering pada jenis register (core/FP) di pengalokasi register.
- Pastikan beberapa array diinisialisasi pada waktu kompilasi. Jangan mengandalkan clang untuk melakukannya.
- Bersihkan beberapa loop. Gunakan loop rentang yang dapat dioptimalkan clang dengan lebih baik karena tidak perlu memuat ulang pointer internal container karena efek samping loop. Hindari memanggil fungsi virtual `HInstruction::GetInputRecords()` dalam loop melalui `InputAt(.)` inline untuk setiap input.
- Hindari fungsi Accept() untuk pola pengunjung dengan memanfaatkan pengoptimalan compiler.
Kesimpulan
Dedikasi kami untuk meningkatkan kecepatan waktu kompilasi ART telah menghasilkan peningkatan yang signifikan, sehingga membuat Android lebih lancar dan efisien sekaligus berkontribusi pada masa pakai baterai dan suhu perangkat yang lebih baik. Dengan mengidentifikasi dan menerapkan pengoptimalan secara cermat, kami telah menunjukkan bahwa peningkatan waktu kompilasi yang signifikan dapat dicapai tanpa mengorbankan penggunaan memori atau kualitas kode.
Perjalanan kami melibatkan pembuatan profil dengan alat seperti pprof, kesediaan untuk melakukan iterasi, dan terkadang bahkan meninggalkan cara yang kurang membuahkan hasil. Upaya kolektif tim ART tidak hanya mengurangi waktu kompilasi dengan persentase yang signifikan, tetapi juga meletakkan dasar untuk kemajuan di masa mendatang.
Semua peningkatan ini tersedia dalam update Android akhir tahun 2025, dan untuk Android 12 dan yang lebih baru melalui update mainline. Semoga pembahasan mendalam tentang proses pengoptimalan kami ini memberikan insight berharga tentang kompleksitas dan manfaat rekayasa compiler.
-
Berita ProdukSeiring dengan percepatan pengembangan berbasis agen, rekayasa Android mencapai batas baru yang menarik: beralih dari penyelesaian kode yang bersifat membantu ke agen yang mengeksekusi secara menyeluruh di bawah arahan Anda. Anda memiliki pilihan dan fleksibilitas total dalam model dan agen yang Anda gunakan.
Jason Buchel • Waktu baca: 3 menit -
Berita ProdukHari ini, kami menyampaikan cara kami membawa visi “lebih dari sekadar toko” ke tahap berikutnya. Mulai dari tab Anda, yang kini menjadi markas pribadi bagi lebih dari 250 juta pengguna aktif bulanan, hingga platform game terpadu kami yang menghubungkan lebih dari 700 juta profil gamer, Google Play memenuhi kebutuhan pengguna di mana pun mereka berada.
Paul Feng • Waktu baca: 3 menit -
Berita ProdukDengan Googlebook, kami senang dapat membantu pengguna mengekspresikan diri dan mendapatkan akses ke alat ini.
Cyrus Stoller • Waktu baca: 2 menit
Dapatkan insight pengembangan Android terbaru yang dikirim ke kotak masuk Anda setiap minggu.