Google berkomitmen untuk mendorong terwujudnya keadilan rasial bagi komunitas Kulit Hitam. Lihat caranya.

Tips performa

Topik utama dokumen ini adalah pengoptimalan mikro yang dapat meningkatkan performa aplikasi secara keseluruhan saat digabungkan, tetapi perubahan ini kemungkinan besar tidak akan berpengaruh secara signifikan terhadap performa. Memilih algoritme dan struktur data yang tepat harus selalu menjadi pertimbangan utama Anda, meskipun hal tersebut tidak dicakup dalam dokumen ini. Sebaiknya gunakan tips dalam dokumen ini sebagai praktik coding umum yang dapat digabungkan ke dalam kebiasaan Anda untuk efisiensi coding umum.

Ada dua aturan dasar penulisan kode yang efisien:

  • Hindari melakukan pekerjaan yang tidak diperlukan.
  • Hindari mengalokasikan memori jika memungkinkan.

Salah satu masalah tersulit yang akan Anda hadapi saat melakukan pengoptimalan mikro aplikasi Android adalah aplikasi dipastikan akan berjalan di beberapa jenis hardware. Beberapa versi VM berjalan pada berbagai prosesor dengan kecepatan yang berbeda. Pada umumnya, Anda bahkan tidak dapat mengatakan "perangkat X lebih cepat/lebih lambat sebesar faktor F daripada perangkat Y", dan menerapkan hasil Anda dari satu perangkat ke perangkat lainnya. Khususnya, pengukuran pada emulator tidak memberikan banyak informasi terkait performa di perangkat apa saja. Selain itu, ada perbedaan besar antara perangkat dengan JIT dan perangkat tanpa JIT: kode terbaik untuk perangkat dengan JIT belum tentu menjadi kode terbaik untuk perangkat tanpa JIT.

Agar aplikasi Anda selalu berjalan dengan baik di berbagai perangkat, pastikan kode bekerja dengan efisien di semua level dan mengoptimalkan performa secara maksimal.

Hindari membuat objek yang tidak diperlukan

Pembuatan objek tidak pernah diberikan secara cuma-cuma. Pembersih sampah memori generasi dengan kumpulan alokasi per-thread untuk objek sementara dapat mengurangi biaya alokasi, tetapi mengalokasikan memori selalu lebih mahal daripada tidak mengalokasikan memori.

Saat Anda mengalokasikan lebih banyak objek di aplikasi, Anda akan memaksa pembersihan sampah memori secara berkala sehingga akan menciptakan sedikit masalah dalam pengalaman pengguna. Pembersihan sampah memori serentak yang diperkenalkan di Android 2.3 ini bermanfaat, tetapi pekerjaan yang tidak perlu harus selalu dihindari.

Oleh karena itu, sebaiknya hindari membuat instance objek yang tidak diperlukan. Beberapa contoh yang dapat membantu:

  • Jika Anda memiliki metode yang menampilkan string, dan tahu bahwa hasilnya akan selalu ditambahkan ke StringBuffer, ubah tanda tangan dan implementasi Anda agar fungsi tersebut melakukan penambahan secara langsung, bukan membuat objek sementara berjangka pendek.
  • Saat mengekstrak string dari rangkaian data input, cobalah untuk menampilkan substring data asli, bukan membuat salinan. Anda akan membuat objek String baru, tetapi objek tersebut akan menggunakan char[] yang sama dengan data tersebut. (Konsekuensinya adalah jika Anda hanya menggunakan sebagian kecil input asli, Anda akan tetap menyimpannya di dalam memori jika menggunakan rute ini.)

Tips umum lainnya adalah membagi array multidimensi menjadi array satu dimensi paralel tunggal:

  • Array int akan jauh lebih baik daripada array objek Integer, tetapi ini juga akan menggeneralisasi fakta bahwa dua array paralel int akan jauh lebih efisien daripada satu array objek (int,int). Hal yang sama berlaku untuk setiap kombinasi jenis primitif.
  • Jika Anda perlu mengimplementasikan container yang menampung tuple objek (Foo,Bar), ingat bahwa dua array Foo[] dan Bar[] paralel biasanya akan jauh lebih baik daripada satu array objek (Foo,Bar) kustom. (Tentunya, pengecualian untuk kasus ini adalah ketika Anda sedang merancang API yang dapat diakses oleh kode lain. Dalam hal ini, biasanya akan lebih baik untuk menyesuaikan kecepatan agar dapat memperoleh desain API yang baik. Namun, dalam kode internal Anda sendiri, sebaiknya coba untuk bekerja seefisien mungkin.)

Secara umum, hindari membuat objek sementara jangka pendek jika memungkinkan. Berkurangnya objek yang dibuat akan mengurangi pembersihan sampah memori, sehingga berdampak langsung pada pengalaman pengguna.

Pilih metode statis daripada virtual

Jika tidak perlu mengakses bidang objek, ubah metode Anda menjadi statis. Pemanggil perintah akan bekerja 15%-20% lebih cepat. Hal ini juga merupakan praktik yang baik karena Anda dapat mengetahui dari signature metode bahwa memanggil metode tidak dapat mengubah keadaan objek.

Gunakan final statis sebagai konstanta

Pertimbangkan deklarasi berikut di bagian atas class:

    static int intVal = 42;
    static String strVal = "Hello, world!";
    

Compiler menghasilkan metode penginisialisasi class, yang disebut <clinit>, yang dieksekusi pada penggunaan pertama class. Metode ini menyimpan nilai 42 ke intVal, dan mengekstrak referensi dari tabel konstanta string classfile untuk strVal. Saat nilai ini direferensikan nanti, nilai akan diakses dengan pencarian kolom.

Masalah ini dapat diatasi menggunakan kata kunci "final":

    static final int intVal = 42;
    static final String strVal = "Hello, world!";
    

Class tidak lagi memerlukan metode <clinit>, karena konstanta dipindahkan ke penginisialisasi kolom statis dalam file dex. Kode yang mengacu pada intVal akan menggunakan nilai bilangan bulat 42 secara langsung, dan akses ke strVal akan menggunakan petunjuk "konstanta string" yang relatif mudah, bukan pencarian kolom.

Catatan: Pengoptimalan ini hanya berlaku untuk konstanta String dan jenis primitif, bukan jenis referensi acak. Namun, sebaiknya tetap deklarasikan konstanta static final jika memungkinkan.

Gunakan sintaksis yang disempurnakan untuk loop

Loop for yang disempurnakan (terkadang juga disebut loop “for-each”) dapat digunakan untuk kumpulan yang mengimplementasikan antarmuka Iterable dan untuk array. Dengan kumpulan, iterator dialokasikan untuk melakukan panggilan antarmuka ke hasNext() dan next(). Dengan ArrayList, loop terhitung yang ditulis tangan kurang lebih akan 3x lebih cepat (dengan maupun tanpa JIT), tetapi untuk kumpulan lain, sintaksis yang disempurnakan untuk loop akan sebanding dengan penggunaan iterator eksplisit.

Ada beberapa alternatif untuk iterasi melalui array:

    static class Foo {
        int splat;
    }

    Foo[] array = ...

    public void zero() {
        int sum = 0;
        for (int i = 0; i < array.length; ++i) {
            sum += array[i].splat;
        }
    }

    public void one() {
        int sum = 0;
        Foo[] localArray = array;
        int len = localArray.length;

        for (int i = 0; i < len; ++i) {
            sum += localArray[i].splat;
        }
    }

    public void two() {
        int sum = 0;
        for (Foo a : array) {
            sum += a.splat;
        }
    }
    

zero() adalah yang paling lambat, karena JIT belum dapat meminimalkan konsekuensi untuk mendapatkan panjang array satu kali bagi setiap iterasi melalui loop.

one() akan lebih cepat. Iterasi ini menarik semuanya ke dalam variabel lokal dan menghindari pencarian. Hanya panjang array yang menawarkan manfaat performa.

two() adalah yang tercepat untuk perangkat tanpa JIT, dan tidak dapat dibedakan dengan one() untuk perangkat dengan JIT. Iterasi ini menggunakan sintaksis yang disempurnakan untuk loop, yang diperkenalkan dalam bahasa pemrograman Java versi 1.5.

Jadi, secara default, Anda harus menggunakan loop for yang disempurnakan, tetapi pertimbangkan loop terhitung yang ditulis tangan untuk iterasi ArrayList yang penting bagi performa.

Tips: Lihat juga Effective Java oleh Josh Bloch, item 46.

Pilih paket daripada akses pribadi dengan class dalam pribadi

Perhatikan definisi class berikut:

    public class Foo {
        private class Inner {
            void stuff() {
                Foo.this.doStuff(Foo.this.mValue);
            }
        }

        private int mValue;

        public void run() {
            Inner in = new Inner();
            mValue = 27;
            in.stuff();
        }

        private void doStuff(int value) {
            System.out.println("Value is " + value);
        }
    }

Hal yang cukup penting di sini adalah kita menentukan class internal pribadi (Foo$Inner) yang secara langsung mengakses metode pribadi dan kolom instance pribadi di class eksternal. Hal ini sah, lalu kode akan menampilkan "Nilainya 27" seperti yang diharapkan.

Masalahnya adalah, VM menganggap akses langsung dari Foo$Inner ke anggota pribadi Foo sebagai akses yang tidak sah karena Foo dan Foo$Inner adalah class yang berbeda, meskipun bahasa Java memungkinkan class internal mengakses anggota pribadi class eksternal. Untuk mengatasi celah ini, compiler menghasilkan beberapa metode sintetis:

    /*package*/ static int Foo.access$100(Foo foo) {
        return foo.mValue;
    }
    /*package*/ static void Foo.access$200(Foo foo, int value) {
        foo.doStuff(value);
    }

Kode class internal akan memanggil metode statis tersebut setiap kali perlu mengakses kolom mValue atau memanggil metode doStuff() di class eksternal. Hal ini berarti kode di atas benar-benar menjelaskan situasi ketika Anda mengakses kolom anggota melalui metode pengakses. Sebelumnya, kami membahas tentang bagaimana aksesor lebih lambat dari pengaksesan bidang langsung. Jadi, ini adalah contoh dari idiom bahasa tertentu yang menghasilkan kecocokan performa yang "tidak terlihat".

Jika Anda menggunakan kode seperti ini di titik performa, Anda dapat menghindari overhead dengan menyatakan bidang dan metode yang diakses oleh class dalam untuk mendapatkan akses paket, bukan akses pribadi. Sayangnya, hal ini berarti bidang dapat diakses secara langsung oleh class lain dalam paket yang sama, sehingga Anda tidak dapat menggunakannya di API publik.

Hindari menggunakan floating point

Secara umum, floating point akan sekitar 2x lebih lambat daripada bilangan bulat di perangkat yang didukung oleh Android.

Dalam hal kecepatan, tidak ada perbedaan antara float dan double di hardware yang lebih modern. Dalam hal ruang, double berukuran 2x lebih besar. Untuk perangkat desktop, dengan asumsi bahwa ruang bukanlah masalah, Anda harus lebih memilih double dibanding float.

Bahkan untuk bilangan bulat, beberapa prosesor memiliki penggandaan hardware, tetapi tidak memiliki pembagian hardware. Dalam hal ini, pembagian integer dan operasi modulus dilakukan di dalam software—hal ini perlu dipertimbangkan jika Anda mendesain tabel hash atau melakukan banyak penghitungan.

Kenali dan gunakan library

Selain semua alasan umum untuk lebih memilih kode library daripada meluncurkan kode Anda sendiri, perlu diingat bahwa sistem memiliki kebebasan untuk mengganti panggilan ke metode library dengan assembler yang kodenya ditulis manual, yang mungkin lebih baik daripada kode terbaik yang dapat dihasilkan JIT untuk Java yang setara. Contoh umumnya di sini adalah String.indexOf() dan API terkait, yang diganti oleh Dalvik dengan intrinsik inline. Demikian pula, metode System.arraycopy() akan sekitar 9x lebih cepat daripada loop yang kodenya ditulis manual di Nexus One dengan JIT.

Tips: Lihat juga Effective Java oleh Josh Bloch, item 46.

Gunakan metode native dengan hati-hati

Mengembangkan aplikasi Anda dengan kode native menggunakan Android NDK tidak selalu lebih efisien daripada menggunakan pemrograman dengan bahasa Java. Perlu diingat bahwa ada biaya yang terkait dengan transisi Java asli, dan JIT tidak dapat dioptimalkan di seluruh batas ini. Jika Anda mengalokasikan materi native (memori pada tumpukan native, deskriptor file, atau lainnya), mengatur pengumpulan materi ini secara tepat waktu akan jauh lebih sulit. Anda juga perlu mengkompilasi kode untuk setiap arsitektur yang ingin dijalankan (daripada mengandalkan JIT). Anda mungkin juga perlu mengompilasi beberapa versi untuk arsitektur yang dianggap sama: kode native yang dikompilasi untuk prosesor ARM di G1 tidak dapat memanfaatkan ARM sepenuhnya di Nexus One, dan kode yang dikompilasi untuk ARM di Nexus One tidak akan berjalan pada ARM di G1.

Kode native terutama berguna jika Anda sudah memiliki codebase native yang ingin ditransfer ke Android, bukan untuk "mempercepat" bagian aplikasi Android yang ditulis dengan bahasa Java.

Jika perlu menggunakan kode native, sebaiknya baca Tips JNI.

Tips: Lihat juga Effective Java oleh Josh Bloch, item 54.

Mitos terkait performa

Pada perangkat tanpa JIT, benar bahwa memanggil metode melalui variabel dengan jenis yang sama akan lebih efisien daripada melalui antarmuka. (Jadi, sebagai contoh, akan lebih mudah untuk memanggil metode pada HashMap map daripada Map map, meskipun petanya sama-sama HashMap dalam kedua situasi.) Bukan karena cara ini 2 kali lebih lambat; perbedaan sebenarnya adalah sekitar 6% lebih lambat. Selain itu, JIT membuat keduanya tidak dapat dibedakan secara efektif.

Pada perangkat tanpa JIT, akses bidang caching sekitar 20% lebih cepat daripada berulang kali mengakses bidang. Dengan JIT, biaya akses bidang hampir sama dengan akses lokal. Karena itu, pengoptimalan ini kurang bermanfaat, kecuali jika Anda merasa ini membuat kode Anda lebih mudah dibaca. (Hal ini juga berlaku untuk bidang final, statis, dan final-statis.)

Selalu lakukan pengukuran

Sebelum memulai pengoptimalan, pastikan Anda memiliki masalah yang perlu diselesaikan. Pastikan Anda dapat mengukur performa yang ada secara akurat, atau Anda tidak akan dapat mengukur manfaat dari upaya alternatif yang dicoba.

Anda juga dapat memanfaatkan Traceview untuk pembuatan profil, tetapi perlu diketahui bahwa Traceview saat ini menonaktifkan JIT sehingga mungkin akan salah mengalokasikan waktu pembuatan kode yang mungkin dapat diatasi dengan JIT. Setelah melakukan perubahan yang disarankan oleh data Traceview, sebaiknya pastikan bahwa kode yang dihasilkan benar-benar berjalan lebih cepat ketika dijalankan tanpa Traceview.

Untuk mendapatkan bantuan lebih lanjut tentang cara membuat profil dan men-debug aplikasi Anda, baca dokumen berikut: