Tips performa

Konten utama dokumen ini adalah pengoptimalan mikro yang dapat menyempurnakan performa aplikasi secara keseluruhan saat digabungkan, tetapi perubahan ini kemungkinan besar tidak akan berpengaruh secara signifikan pada 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 yang memiliki JIT dan yang tidak memilikinya: kode terbaik untuk perangkat dengan JIT tidak selalu 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 mengembalikan string dan menyadari bahwa hasilnya akan selalu ditambahkan ke StringBuffer, ubah signature dan implementasi Anda agar fungsi dapat melakukan penambahan secara langsung, serta tidak akan membuat objek sementara yang berumur pendek.
  • Saat mengekstraksi string dari serangkaian data input, coba kembalikan substring dari data asli, dan jangan buat salinan. Anda akan membuat objek String baru, tetapi tindakan ini akan membagikan char[] dengan data. (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 jauh lebih baik dibandingkan array objek Integer, tetapi hal ini juga menggeneralisasi fakta bahwa dua array paralel intent juga jauh lebih efisien daripada array (int,int). Hal yang sama berlaku untuk setiap kombinasi jenis primitif.
  • Jika Anda harus mengimplementasikan wadah yang menyimpan tupel objek (Foo,Bar), perlu diingat bahwa dua paralel Foo[] dan array Bar[] umumnya jauh lebih baik daripada satu array dari objek (Foo,Bar) kustom. (Tentunya, pengecualian untuk ini adalah ketika Anda sedang merancang API untuk 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 yang melakukan inisialisasi class yang disebut <clinit> dan dieksekusi saat class pertama kali digunakan. Metode ini menyimpan nilai 42 ke dalam intVal dan mengekstraksi referensi dari tabel konstanta string classfile untuk strVal. Saat nilai ini direferensikan, nilai akan diakses dengan pencarian bidang.

Masalah 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 akan masuk ke penginisialisasi bidang statis dalam file dex. Kode yang mengacu ke intVal akan menggunakan nilai integer 42 secara langsung, dan akses ke strVal akan menggunakan instruksi "string konstan" yang relatif murah daripada pencarian bidang.

Catatan: Pengoptimalan ini hanya berlaku untuk jenis primitif dan konstanta String, bukan untuk jenis referensi arbitrer. Namun, jika memungkinkan, praktik ini baik untuk menyatakan konstanta static final.

Gunakan sintaks enhanced for loop

Enhanced for loop (juga terkadang disebut sebagai loop "untuk masing-masing") dapat digunakan untuk pengumpulan yang mengimplementasikan antarmuka Iterable dan untuk array. Melalui pengumpulan, iterator dialokasikan untuk melakukan panggilan antarmuka ke hasNext() dan next(). Dengan ArrayList, hand-written loop yang dihitung akan 3x lebih cepat (dengan atau tanpa JIT). Namun, untuk pengumpulan lain, sintaks enhanced for loop akan sama persis 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 mengoptimalkan biaya untuk mendapatkan panjang array satu kali untuk setiap iterasi melalui loop.

one() adalah yang 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 satu() untuk perangkat dengan JIT. Itirasi ini menggunakan sintaks enhanced for loop yang diperkenalkan di dalam bahasa pemrograman Java versi 1.5.

Oleh karena itu, Anda harus menggunakan enhanced for loop secara default, tetapi pertimbangkan hand-written loop yang dihitung untuk iterasi ArrayList penting performa.

Tips: Lihat juga Effective Java oleh Josh Block, 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);
        }
    }

Intinya, class dalam pribadi didefinisikan sebagai (Foo$Inner) yang secara langsung mengakses metode pribadi dan bidang instance pribadi di class luar. Hal ini sah, lalu kode akan mencetak "Nilainya 27" seperti yang diharapkan.

Masalahnya adalah VM menganggap akses langsung ke anggota pribadi Foo dari Foo$Inner tidak sah karena Foo dan Foo$Inner merupakan class yang berbeda, meskipun bahasa Java memungkinkan class dalam untuk mengakses anggota pribadi class luar. Untuk menutup kesenjangan, 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 dalam memanggil metode statis ini setiap kali harus mengakses bidang mValue atau memanggil metode doStuff() di class luar. Artinya, kode di atas benar-benar menampilkan kasus saat Anda mengakses bidang anggota melalui metode aksesor. 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 sekitar 2x lebih lambat dari integer di perangkat yang didukung oleh Android.

Dari segi kecepatan, tidak ada perbedaan antara float dan double pada hardware yang lebih modern. Dari segi ruang, double berukuran 2x lebih besar. Seperti halnya mesin desktop, dengan asumsi ruang tidak menjadi masalah, Anda harus memilih double daripada float.

Bahkan untuk integer, beberapa prosesor memiliki pengganda hardware, tetapi tidak memiliki pembagi 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 perakit berkode tangan, yang mungkin lebih baik daripada kode terbaik yang bisa dihasilkan JIT untuk Java yang setara. Contoh khususnya di sini adalah String.indexOf() dan API terkait, yang diganti Dalvik dengan intrinsik inline. Begitu juga, metode System.arraycopy() sekitar 9x lebih cepat dari loop kode tangan pada Nexus One dengan JIT.

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

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 ketika Anda memiliki basis kode native yang sudah ada yang ingin di-porting ke Android, bukan untuk "mempercepat" bagian aplikasi Android yang ditulis dengan bahasa Java.

Jika Anda 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, memanggil metode melalui variabel dengan jenis yang lama lebih efisien daripada antarmuka. (Jadi, misalnya, memanggil metode pada HashMap map lebih murah daripada Map map, meskipun dalam kedua kasus, petanya adalah HashMap.) Masalahnya bukan karena ini 2x lebih lambat. Perbedaan yang sebenarnya justru karena ini 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 menemukan manfaat Traceview untuk profiling, tetapi perlu disadari bahwa Traceview menonaktfikan JIT saat ini, yang dapat menyebabkan kesalahan pemberian waktu untuk membuat kode yang mungkin dapat ditarik kembali oleh 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 melakukan profiling dan proses debug aplikasi Anda, lihat dokumen berikut: