Berita Produk

Kompilasi 18% Lebih Cepat, 0% Kompromi

Waktu baca: 8 menit

Tim Android Runtime (ART) telah mengurangi waktu kompilasi sebesar 18% tanpa mengorbankan kode yang dikompilasi atau regresi memori puncak. Peningkatan ini merupakan 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 tepat waktu (JIT), hal ini secara langsung memengaruhi efisiensi aplikasi dan performa perangkat secara keseluruhan. Kompilasi yang lebih cepat mengurangi waktu sebelum pengoptimalan dimulai, sehingga menghasilkan pengalaman pengguna yang lebih lancar dan responsif. Selain itu, untuk JIT dan mendahului waktu (AOT), peningkatan kecepatan waktu kompilasi akan mengurangi konsumsi resource selama proses kompilasi, sehingga menguntungkan masa pakai baterai dan termal 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 di versi 12 dan yang lebih baru memenuhi syarat untuk menerima peningkatan ini melalui update utama.

Mengoptimalkan compiler pengoptimalan

Mengoptimalkan compiler selalu merupakan game trade-off. Anda tidak bisa mendapatkan kecepatan secara gratis; Anda harus mengorbankan sesuatu. Kami menetapkan tujuan yang sangat jelas dan menantang untuk 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, kami telah 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 bekerja untuk 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 yakin apakah Anda telah meningkatkannya atau tidak. Untungnya bagi kami, kecepatan waktu kompilasi cukup konsisten selama Anda mengambil beberapa tindakan pencegahan seperti menggunakan perangkat yang sama yang Anda gunakan untuk mengukur sebelum dan sesudah 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, kami ingin dapat melakukan iterasi secepat mungkin. Artinya, kami mengambil beberapa aplikasi representatif (campuran aplikasi pihak pertama, aplikasi pihak ketiga, dan sistem operasi Android itu sendiri) untuk membuat prototipe solusi. Kemudian, kami memverifikasi bahwa implementasi akhir sepadan dengan pengujian manual dan otomatis secara luas.

 

Dengan kumpulan apk pilihan tersebut, kami akan memicu kompilasi manual secara lokal, mendapatkan profil kompilasi, dan menggunakan pprof untuk memvisualisasikan tempat kami menghabiskan waktu.

image.png

Contoh grafik nyala profil di pprof

Alat pprof sangat canggih dan memungkinkan kami membagi, memfilter, dan mengurutkan data untuk melihat, misalnya, fase atau metode compiler mana yang menghabiskan sebagian besar waktu. Kami tidak akan membahas pprof secara mendetail; cukup ketahui bahwa jika batang lebih besar, berarti kompilasi membutuhkan lebih banyak waktu.

Salah satu tampilan ini adalah tampilan “bottom up” tempat Anda dapat melihat metode mana yang menghabiskan sebagian besar 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.

image.png

Tampilan bottom up profil

Di compiler pengoptimalan kami, ada fase yang disebut Global Value Numbering (GVN). Anda tidak perlu khawatir tentang apa yang dilakukannya secara keseluruhan, tetapi bagian yang relevan adalah mengetahui bahwa metode ini memiliki metode yang disebut `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 menyadari bahwa ada beberapa kasus yang kami ketahui sebelumnya bahwa pemeriksaan akan salah, apa pun node yang kami miliki saat itu. Dalam kasus ini, kita dapat melewati iterasi sepenuhnya, sehingga mengurangi dari 1,023% menjadi ~0,3% dan meningkatkan runtime GVN sebesar ~15%.

Mengimplementasikan pengoptimalan yang bermanfaat

Kami membahas cara mengukur dan cara mendeteksi tempat waktu dihabiskan, tetapi ini baru permulaan. Langkah berikutnya adalah cara mengoptimalkan waktu yang dihabiskan untuk kompilasi.

Biasanya, dalam kasus seperti `Kill` di atas, kami akan melihat cara melakukan iterasi melalui node dan melakukannya lebih cepat, misalnya, dengan melakukan hal-hal secara paralel atau meningkatkan algoritma itu sendiri. Faktanya, itulah yang kami coba lakukan pada awalnya dan hanya ketika kami tidak dapat menemukan apa pun yang dapat dilakukan, kami memiliki momen “Tunggu sebentar…” dan melihat bahwa solusinya adalah (dalam beberapa kasus) tidak melakukan iterasi sama sekali. Saat melakukan pengoptimalan semacam ini, mudah untuk tidak melihat gambaran besarnya.

Dalam kasus lain, kami menggunakan beberapa teknik yang berbeda, termasuk:

  • menggunakan heuristik untuk memutuskan apakah pengoptimalan akan gagal menghasilkan hasil yang bermanfaat dan oleh karena itu dapat dilewati
  • menggunakan struktur data tambahan untuk menyimpan data yang dihitung dalam cache
  • mengubah struktur data saat ini untuk mendapatkan peningkatan kecepatan
  • menghitung hasil secara lambat untuk menghindari siklus dalam beberapa kasus
  • menggunakan abstraksi yang tepat - fitur yang tidak perlu dapat memperlambat kode
  • menghindari pengejaran pointer yang sering digunakan melalui banyak beban

Bagaimana kita tahu apakah pengoptimalan layak dilakukan?

Itulah bagian yang rapi, Anda tidak. 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 dapat dilakukan, implementasinya akan memakan waktu terlalu lama, akan menurunkan metrik lain secara signifikan, meningkatkan kompleksitas basis kode, dll. Untuk setiap pengoptimalan yang berhasil yang dapat Anda lihat di postingan blog ini, ketahuilah bahwa ada banyak pengoptimalan lain yang tidak berhasil.

Jika Anda berada dalam situasi yang serupa, coba perkirakan seberapa besar Anda akan meningkatkan metrik dengan melakukan pekerjaan sesedikit mungkin. Artinya, dalam urutan:

  1. Memperkirakan dengan metrik yang telah Anda kumpulkan, atau hanya perasaan
  2. Memperkirakan dengan prototipe cepat dan kotor
  3. Mengimplementasikan 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 basa-basi lagi, mari kita lihat beberapa perubahan yang kami implementasikan.

Kami mengimplementasikan perubahan untuk mengoptimalkan metode yang disebut FindReferenceInfoOf. Metode ini melakukan penelusuran linear vektor untuk menemukan entri. Kami memperbarui struktur data tersebut agar diindeks berdasarkan ID instruksi sehingga FindReferenceInfoOf akan menjadi O(1), bukan O(n). Selain itu, kami mengalokasikan vektor terlebih dahulu untuk menghindari perubahan ukuran. Kami sedikit meningkatkan memori karena harus menambahkan kolom tambahan yang menghitung jumlah entri yang kami sisipkan dalam vektor, tetapi itu adalah pengorbanan kecil karena memori puncak tidak meningkat. Hal ini mempercepat fase LoadStoreAnalysis kami sebesar 34-66% yang pada gilirannya memberikan peningkatan waktu kompilasi sebesar ~0,5-1,8%.

Kami memiliki implementasi HashSet kustom yang kami gunakan di beberapa tempat. Membuat struktur data ini membutuhkan waktu yang cukup lama dan kami menemukan 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 digunakan ke arah yang berlawanan dengan hanya beberapa entri dan dengan masa pakai yang singkat. Artinya, kami membuang siklus dengan membuat HashSet yang sangat besar ini, tetapi kami hanya menggunakannya untuk beberapa entri sebelum membuangnya. Dengan perubahan ini, kami meningkatkan ~1,3-2% waktu kompilasi. Sebagai bonus tambahan, penggunaan memori menurun sebesar ~0,5-1% karena kami tidak menggunakan struktur data sebesar sebelumnya.

Kami meningkatkan ~0,5-1% waktu kompilasi dengan meneruskan struktur data berdasarkan referensi ke lambda untuk menghindari penyalinan. Hal ini terlewat dalam peninjauan awal dan berada di basis kode kami selama bertahun-tahun. Berkat melihat profil di pprof, kami menyadari bahwa metode ini membuat dan menghancurkan banyak struktur data, yang membuat kami menyelidiki dan mengoptimalkannya.

Kami mempercepat fase yang menulis output yang dikompilasi dengan menyimpan nilai yang dihitung dalam cache, yang diterjemahkan menjadi ~1,3-2,8% peningkatan waktu kompilasi total. Sayangnya, pembukuan tambahan terlalu banyak dan pengujian otomatis kami memberi tahu kami tentang regresi memori. Kemudian, kami melihat kembali kode yang sama dan mengimplementasikan versi baru yang tidak hanya menangani regresi memori, tetapi juga meningkatkan waktu kompilasi sebesar ~0,5-1,8%! Dalam perubahan kedua ini, kami harus memfaktorkan ulang dan membayangkan kembali cara kerja fase ini, untuk menghapus salah satu dari dua struktur data.

Kami memiliki fase di compiler pengoptimalan yang memanggil fungsi inline untuk mendapatkan performa yang lebih baik. Untuk memilih metode mana yang akan di-inline, kami menggunakan heuristik sebelum melakukan komputasi apa pun, dan pemeriksaan akhir setelah melakukan pekerjaan, tetapi tepat sebelum kami menyelesaikan inlining. Jika salah satu dari metode tersebut mendeteksi bahwa inlining tidak sepadan (misalnya, terlalu banyak instruksi baru yang akan ditambahkan), kami tidak akan meng-inline 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 mahal. Karena ini adalah perkiraan, perkiraan ini 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 instruksi (peningkatan ~2%).

Kami memiliki implementasi BitVector kustom yang kami gunakan di beberapa tempat. Kami mengganti class BitVector yang dapat diubah ukurannya dengan BitVectorView yang lebih sederhana untuk vektor bit ukuran tetap tertentu. Hal ini menghilangkan beberapa pengalihan dan pemeriksaan rentang runtime serta mempercepat pembuatan objek vektor bit.

Selain itu, class BitVectorView di-template pada jenis penyimpanan yang mendasarinya (bukan selalu menggunakan uint32_t sebagai BitVector lama). Hal ini memungkinkan beberapa operasi, misalnya Union(), untuk memproses dua kali lebih banyak bit secara bersamaan di platform 64-bit. Sampel fungsi yang terpengaruh berkurang lebih dari 1% secara total saat mengompilasi Android OS. Hal ini dilakukan di beberapa perubahan [123456]

Jika kita membahas semua pengoptimalan secara mendetail, kita akan berada di sini sepanjang hari. Jika Anda tertarik dengan beberapa pengoptimalan lainnya, lihat beberapa perubahan lain yang kami implementasikan:

Kesimpulan

Dedikasi kami untuk meningkatkan kecepatan waktu kompilasi ART telah menghasilkan peningkatan yang signifikan, membuat Android lebih lancar dan efisien sekaligus berkontribusi pada masa pakai baterai dan termal perangkat yang lebih baik. Dengan mengidentifikasi dan mengimplementasikan pengoptimalan secara cermat, kami telah menunjukkan bahwa peningkatan waktu kompilasi yang substansial dapat dilakukan tanpa mengorbankan penggunaan memori atau kualitas kode.

Perjalanan kami melibatkan pembuatan profil dengan alat seperti pprof, kesediaan untuk melakukan iterasi, dan terkadang bahkan meninggalkan jalur yang kurang membuahkan hasil. Upaya kolektif tim ART tidak hanya mengurangi waktu kompilasi dengan persentase yang signifikan, tetapi juga meletakkan dasar bagi kemajuan di masa mendatang.

Semua peningkatan ini tersedia dalam update Android akhir tahun 2025, dan untuk Android 12 dan yang lebih baru melalui update utama. Kami harap pembahasan mendalam tentang proses pengoptimalan kami ini memberikan insight berharga tentang kompleksitas dan manfaat teknik compiler.

Lanjutkan membaca