Jadikan Java dengan pantas: Optimumkan!

Menurut saintis komputer perintis Donald Knuth, "Pengoptimuman pramatang adalah akar semua kejahatan." Setiap artikel mengenai pengoptimuman mesti dimulakan dengan menunjukkan bahawa biasanya ada lebih banyak alasan untuk tidak mengoptimumkan daripada mengoptimumkan.

  • Sekiranya kod anda sudah berfungsi, mengoptimumkannya adalah cara yang pasti untuk memperkenalkan bug baru, dan mungkin halus

  • Pengoptimuman cenderung menjadikan kod sukar difahami dan dijaga

  • Beberapa teknik yang disajikan di sini meningkatkan kelajuan dengan mengurangkan kepanjangan kod

  • Mengoptimumkan kod untuk satu platform sebenarnya boleh menjadikannya lebih buruk pada platform lain

  • Banyak masa dapat dihabiskan untuk mengoptimumkan, dengan sedikit peningkatan dalam prestasi, dan dapat menghasilkan kod yang tidak jelas

  • Sekiranya anda terlalu obses dengan mengoptimumkan kod, orang akan memanggil anda kutu buku di belakang anda

Sebelum mengoptimumkan, anda harus mempertimbangkan dengan teliti sama ada anda perlu mengoptimumkan sama sekali. Pengoptimuman di Java dapat menjadi sasaran yang sukar difahami kerana lingkungan pelaksanaannya bervariasi. Menggunakan algoritma yang lebih baik mungkin akan menghasilkan peningkatan prestasi yang lebih besar daripada jumlah pengoptimuman tahap rendah dan lebih cenderung memberikan peningkatan dalam semua keadaan pelaksanaan. Sebagai peraturan, pengoptimuman tahap tinggi harus dipertimbangkan sebelum melakukan pengoptimuman tahap rendah.

Jadi mengapa mengoptimumkan?

Sekiranya idea itu buruk, mengapa mengoptimumkan sama sekali? Nah, dalam dunia yang ideal anda tidak akan. Tetapi kenyataannya adalah kadang-kadang masalah terbesar dengan program adalah bahawa ia memerlukan terlalu banyak sumber daya, dan sumber daya ini (memori, kitaran CPU, lebar jalur rangkaian, atau gabungan) mungkin terhad. Fragmen kod yang berlaku berkali-kali di sepanjang program cenderung peka terhadap ukuran, sementara kod dengan banyak lelaran pelaksanaan mungkin sensitif terhadap kelajuan.

Jadikan Java dengan pantas!

Sebagai bahasa yang ditafsirkan dengan kod byk, kepantasan, atau kekurangannya, inilah yang paling sering muncul sebagai masalah di Jawa. Kami terutamanya akan melihat bagaimana membuat Java berjalan lebih cepat daripada membuatnya masuk ke ruang yang lebih kecil - walaupun kita akan menunjukkan di mana dan bagaimana pendekatan ini mempengaruhi memori atau lebar jalur rangkaian. Tumpuan akan diberikan pada bahasa inti dan bukan pada API Java.

By the way, satu perkara yang tidak akan kita bincangkan di sini adalah penggunaan kaedah asli yang ditulis dalam C atau pemasangan. Walaupun menggunakan kaedah asli dapat memberikan peningkatan prestasi tertinggi, ia menggunakan kos kebebasan platform Java. Anda boleh menulis kaedah Java versi dan versi asli untuk platform terpilih; ini membawa kepada peningkatan prestasi di beberapa platform tanpa melepaskan kemampuan untuk berjalan di semua platform. Tetapi ini yang akan saya katakan mengenai penggantian Java dengan kod C. (Lihat Tip Java, "Tulis metode asli" untuk informasi lebih lanjut mengenai topik ini.) Fokus kami dalam artikel ini adalah bagaimana membuat Java cepat.

90/10, 80/20, pondok, pondok, kenaikan!

Sebagai peraturan, 90 peratus masa pengecualian program dihabiskan dengan melaksanakan 10 persen kod. (Sebilangan orang menggunakan peraturan 80 persen / 20 peratus, tetapi pengalaman saya menulis dan mengoptimumkan permainan komersial dalam beberapa bahasa selama 15 tahun terakhir menunjukkan bahawa formula 90 peratus / 10 peratus adalah khas untuk program yang gemar prestasi kerana beberapa tugas cenderung dilakukan dengan kerap.) Mengoptimumkan 90 peratus program yang lain (di mana 10 peratus masa pelaksanaan dihabiskan) tidak memberi kesan ketara terhadap prestasi. Sekiranya anda dapat membuat 90 peratus kod tersebut dilaksanakan dua kali lebih cepat, program hanya akan 5 persen lebih cepat. Oleh itu, tugas pertama dalam mengoptimumkan kod adalah mengenal pasti 10 peratus (selalunya kurang dari ini) program yang menghabiskan sebahagian besar masa pelaksanaan. Ini tidaksentiasa di tempat yang anda harapkan.

Teknik pengoptimuman umum

Terdapat beberapa teknik pengoptimuman umum yang berlaku tanpa mengira bahasa yang digunakan. Beberapa teknik ini, seperti peruntukan daftar global, adalah strategi yang canggih untuk mengalokasikan sumber daya mesin (misalnya, daftar CPU) dan tidak berlaku untuk kode byte Java. Kami akan memberi tumpuan kepada teknik yang pada dasarnya melibatkan penyusunan semula kod dan penggantian operasi yang setara dalam satu kaedah.

Pengurangan kekuatan

Pengurangan kekuatan berlaku apabila operasi diganti dengan operasi yang setara yang dijalankan lebih cepat. Contoh pengurangan kekuatan yang paling biasa adalah menggunakan operator shift untuk mengalikan dan membahagi bilangan bulat dengan kekuatan 2. Contohnya, x >> 2boleh digunakan sebagai pengganti x / 4, dan x << 1menggantikan x * 2.

Penghapusan sub ekspresi biasa

Penghapusan sub ekspresi biasa menghilangkan pengiraan berlebihan. Daripada menulis

double x = d * (lim / max) * sx; double y = d * (lim / max) * sy;

sub ungkapan umum dikira sekali dan digunakan untuk kedua-dua pengiraan:

double depth = d * (lim / max); double x = depth * sx; double y = depth * sy;

Pergerakan kod

Pergerakan kod menggerakkan kod yang melakukan operasi atau mengira ungkapan yang hasilnya tidak berubah, atau tidak berubah . Kod tersebut dipindahkan sehingga hanya dapat dilaksanakan apabila hasilnya dapat berubah, dan bukan setiap kali hasilnya diperlukan. Ini paling biasa dengan gelung, tetapi juga boleh melibatkan kod yang diulang pada setiap pemanggilan kaedah. Berikut ini adalah contoh gerakan kod invarian dalam satu gelung:

for (int i = 0; i < x.length; i++) x [i] * = Math.PI * Math.cos (y); 

menjadi

berganda picosy = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i++)x [i] * = picosy;

Melancarkan gelung

Melonggarkan gelung mengurangkan overhead kod kawalan gelung dengan melakukan lebih daripada satu operasi setiap kali melalui gelung, dan akibatnya melakukan lebih sedikit lelaran. Mengikut contoh sebelumnya, jika kita tahu bahawa panjangnya x[]selalu merupakan gandaan dua, kita mungkin menulis semula gelung sebagai:

berganda picosy = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i += 2) {x [i] * = picosy; x [i + 1] * = picosy; }

Dalam praktiknya, melepaskan gelung seperti ini - di mana nilai indeks gelung digunakan dalam gelung dan mesti ditingkatkan secara berasingan - tidak menghasilkan peningkatan kelajuan yang sangat ketara dalam Java yang ditafsirkan kerana kod bytek kekurangan arahan untuk menggabungkan " +1"ke dalam indeks tatasusunan.

Semua petua pengoptimuman dalam artikel ini merangkumi satu atau lebih teknik umum yang disenaraikan di atas.

Menetapkan penyusun berfungsi

Penyusun C dan Fortran moden menghasilkan kod yang sangat dioptimumkan. Penyusun C ++ umumnya menghasilkan kod yang kurang cekap, tetapi masih dalam proses menghasilkan kod yang optimum. Semua penyusun ini telah melalui banyak generasi di bawah pengaruh persaingan pasaran yang kuat dan telah menjadi alat yang diasah dengan baik untuk menekan setiap penurunan prestasi terakhir dari kod biasa. Mereka hampir pasti menggunakan semua teknik pengoptimuman umum yang ditunjukkan di atas. Tetapi masih ada banyak helah untuk membuat penyusun menghasilkan kod yang cekap.

javac, JIT, dan penyusun kod asli

Tahap pengoptimuman yang javacdilakukan ketika menyusun kod pada tahap ini adalah minimum. Secara lalai melakukan perkara berikut:

  • Lipatan malar - penyusun menyelesaikan sebarang ungkapan malar sehingga dapat i = (10 *10)disusun i = 100.

  • Lipatan cawangan (hampir sepanjang masa) - gotokod byte yang tidak perlu dielakkan.

  • Penghapusan kod mati terhad - tidak ada kod yang dihasilkan untuk penyataan seperti if(false) i = 1.

Tahap pengoptimuman yang disediakan javac harus meningkat, mungkin secara dramatik, ketika bahasa matang dan vendor penyusun mula bersaing dengan bersungguh-sungguh berdasarkan penghasilan kod. Java sekarang mendapat penyusun generasi kedua.

Kemudian ada penyusun just-in-time (JIT) yang menukar kod bytava Java menjadi kod asli pada waktu berjalan. Beberapa sudah tersedia, dan sementara mereka dapat meningkatkan kecepatan pelaksanaan program anda secara dramatis, tingkat pengoptimuman yang dapat mereka lakukan terkendali kerana pengoptimuman terjadi pada waktu berjalan. Pengkompil JIT lebih mementingkan penghasilan kod dengan cepat berbanding dengan menghasilkan kod terpantas.

Penyusun kod asli yang menyusun Java secara langsung ke kod asli semestinya menawarkan prestasi terbaik tetapi dengan kos kebebasan platform. Nasib baik, banyak muslihat yang disajikan di sini akan dapat dicapai oleh penyusun masa depan, tetapi buat masa ini memerlukan sedikit usaha untuk mendapatkan hasil yang terbaik dari penyusun.

javactidak menawarkan satu pilihan prestasi yang boleh anda aktifkan: memohon -Opilihan untuk menyebabkan penyusun menyusun panggilan kaedah tertentu:

javac -O MyClass

Menyisipkan panggilan kaedah memasukkan kod untuk kaedah tersebut terus ke dalam kod membuat kaedah memanggil. Ini menghilangkan overhead panggilan kaedah. Untuk kaedah kecil overhead ini dapat mewakili peratusan yang signifikan dari masa pelaksanaannya. Perhatikan bahawa hanya kaedah yang dinyatakan sebagai salah satu private, staticatau finaldapat dipertimbangkan untuk sebaris, kerana hanya kaedah ini yang diselesaikan secara statistik oleh penyusun. Juga, synchronizedkaedah tidak akan diletak sebaris. Penyusun hanya akan menggunakan kaedah kecil yang biasanya terdiri daripada satu atau dua baris kod sahaja.

Sayangnya, versi 1.0 penyusun javac mempunyai bug yang akan menghasilkan kod yang tidak dapat melewati pengesahan bytecode apabila -Opilihan tersebut digunakan. Perkara ini telah diperbaiki dalam JDK 1.1. (Pengesahan bytecode memeriksa kod sebelum dibiarkan dijalankan untuk memastikan bahawa ia tidak melanggar peraturan Java.) Ini akan memasukkan kaedah yang merujuk ahli kelas tidak dapat diakses ke kelas panggilan. Sebagai contoh, jika kelas berikut disusun bersama menggunakan -Opilihan

kelas A {int statik persendirian x = 10; getX tidak sah statik awam () {return x; } kelas B {int y = A.getX (); }

panggilan ke A.getX () di kelas B akan sebaris di kelas B seolah-olah B telah ditulis sebagai:

kelas B {int y = Ax; }

Walau bagaimanapun, ini akan menyebabkan penjanaan kod bytes mengakses pemboleh ubah Ax peribadi yang akan dihasilkan dalam kod B. Kod ini akan dilaksanakan dengan baik, tetapi karena melanggar batasan akses Java, kod ini akan ditandai oleh pengesah dengan IllegalAccessErrorpertama kalinya kod tersebut dijalankan.

Bug ini tidak menjadikan -Opilihan itu tidak berguna, tetapi anda harus berhati-hati tentang bagaimana anda menggunakannya. Sekiranya dipanggil pada satu kelas, ia boleh merangkumi panggilan kaedah tertentu di dalam kelas tanpa risiko. Beberapa kelas boleh disatukan selagi tidak ada batasan akses yang berpotensi. Dan beberapa kod (seperti aplikasi) tidak dikenakan pengesahan bytecode. Anda boleh mengabaikan pepijat jika anda tahu kod anda hanya akan dilaksanakan tanpa dikenakan pengesah. Untuk maklumat tambahan, lihat Soalan Lazim javac-O saya.

Profiler

Nasib baik, JDK dilengkapi dengan profiler terbina dalam untuk membantu mengenal pasti masa yang dihabiskan dalam program. Ia akan melacak masa yang dihabiskan dalam setiap rutin dan menulis maklumat ke fail java.prof. Untuk menjalankan profiler, gunakan -profpilihan ketika memanggil jurubahasa Java:

java -prof myClass

Atau untuk digunakan dengan applet:

java -prof sun.applet.AppletViewer myApplet.html

Terdapat beberapa peringatan untuk menggunakan profiler. Output profiler tidak begitu mudah untuk diuraikan. Juga, dalam JDK 1.0.2, ia memendekkan nama kaedah menjadi 30 aksara, jadi tidak mungkin membezakan beberapa kaedah. Malangnya, dengan Mac tidak ada cara untuk memanggil profiler, jadi pengguna Mac tidak beruntung. Di atas semua ini, halaman dokumen Java Sun (lihat Sumber) tidak lagi menyertakan dokumentasi untuk -profpilihan tersebut). Walau bagaimanapun, jika platform anda menyokong -profpilihan tersebut, HyperProf Vladimir Bulatov atau ProfileViewer Greg White dapat digunakan untuk menafsirkan hasilnya (lihat Sumber).

Kod "profil" juga dimungkinkan dengan memasukkan masa yang jelas ke dalam kod:

long start = System.currentTimeMillis(); // do operation to be timed here long time = System.currentTimeMillis() - start;

System.currentTimeMillis()mengembalikan masa dalam 1/1000 saat. Walau bagaimanapun, beberapa sistem, seperti Windows PC, mempunyai pemasa sistem dengan resolusi kurang (lebih kurang) daripada 1/000 saat. Bahkan 1/1000 saat tidak cukup lama untuk tepat waktu banyak operasi. Dalam kes ini, atau pada sistem dengan pemasa beresolusi rendah, mungkin perlu waktu berapa lama untuk mengulangi operasi n kali dan kemudian bahagikan jumlah masa dengan n untuk mendapatkan masa sebenar. Walaupun profil tersedia, teknik ini dapat berguna untuk menetapkan tugas atau operasi tertentu.

Berikut adalah beberapa nota penutupan profil:

  • Selalu tetapkan kod sebelum dan selepas membuat perubahan untuk mengesahkan bahawa, sekurang-kurangnya di platform ujian, perubahan anda meningkatkan program

  • Cuba buat setiap ujian masa dalam keadaan yang sama

  • Sekiranya mungkin, lakukan ujian yang tidak bergantung pada input pengguna apa pun, kerana variasi tindak balas pengguna dapat menyebabkan hasilnya berubah-ubah

Applet Tanda Aras

Applet Benchmark mengukur masa yang diperlukan untuk melakukan operasi ribuan (atau bahkan berjuta-juta) kali, mengurangkan masa yang dihabiskan untuk melakukan operasi selain daripada ujian (seperti loop overhead), dan kemudian menggunakan maklumat ini untuk menghitung berapa lama setiap operasi mengambil. Ia menjalankan setiap ujian selama lebih kurang satu saat. Dalam usaha untuk menghilangkan kelewatan rawak dari operasi lain yang mungkin dilakukan komputer semasa ujian, setiap ujian dijalankan tiga kali dan menggunakan hasil yang terbaik. Ia juga berusaha untuk menghilangkan pengumpulan sampah sebagai faktor dalam ujian. Oleh kerana itu, semakin banyak memori yang tersedia untuk penanda aras, semakin tepat hasil penanda aras.