Pengaturcaraan prestasi Java, Bahagian 2: Kos pemutus

Untuk artikel kedua dalam siri ini mengenai prestasi Java, fokusnya beralih ke pemeran - apa itu, harganya, dan bagaimana kita (kadang-kadang) menghindarinya. Bulan ini, kita memulakan dengan tinjauan ringkas mengenai asas kelas, objek, dan rujukan, kemudian menindaklanjuti dengan melihat beberapa tokoh prestasi tegar (di bar sisi, agar tidak menyinggung perasaan!) Dan garis panduan mengenai jenis operasi yang kemungkinan besar akan menimbulkan gangguan pada Mesin Maya Java (JVM) anda. Akhirnya, kami menyelesaikan dengan melihat secara mendalam bagaimana kami dapat mengelakkan kesan penstrukturan kelas biasa yang boleh menyebabkan casting.

Pengaturcaraan prestasi Java: Baca keseluruhan siri!

  • Bahagian 1. Pelajari cara mengurangkan overhead program dan meningkatkan prestasi dengan mengawal penciptaan objek dan pengumpulan sampah
  • Bahagian 2. Mengurangkan kesalahan overhead dan pelaksanaan melalui kod jenis selamat
  • Bahagian 3. Lihat bagaimana alternatif koleksi meningkatkan prestasi, dan cari cara memanfaatkan sepenuhnya setiap jenis

Jenis objek dan rujukan di Jawa

Bulan lalu, kami membincangkan perbezaan asas antara jenis primitif dan objek di Jawa. Kedua-dua jenis jenis primitif dan hubungan di antara mereka (terutamanya penukaran antara jenis) ditentukan oleh definisi bahasa. Objek, sebaliknya, adalah jenis yang tidak terhad dan mungkin berkaitan dengan sebilangan jenis lain.

Setiap definisi kelas dalam program Java menentukan jenis objek baru. Ini termasuk semua kelas dari perpustakaan Java, jadi program yang diberikan mungkin menggunakan ratusan atau bahkan ribuan jenis objek. Beberapa jenis ini ditentukan oleh definisi bahasa Java sebagai penggunaan atau pengendalian khas tertentu (seperti penggunaan java.lang.StringBufferuntuk java.lang.Stringoperasi penggabungan). Selain dari beberapa pengecualian ini, semua jenis diperlakukan pada dasarnya sama oleh penyusun Java dan JVM yang digunakan untuk melaksanakan program.

Sekiranya definisi kelas tidak menentukan (dengan extendsklausa dalam tajuk definisi kelas) kelas lain sebagai ibu bapa atau superclass, ini secara tidak langsung akan melanjutkan java.lang.Objectkelas. Ini bermaksud bahawa setiap kelas akhirnya meluas java.lang.Object, sama ada secara langsung atau melalui urutan satu atau lebih tahap kelas induk.

Objek itu sendiri selalu merupakan contoh kelas, dan jenis objek adalah kelas yang merupakan contohnya. Di Jawa, kita tidak pernah berhubungan langsung dengan objek; kami bekerja dengan merujuk kepada objek. Contohnya, garis:

 java.awt.Component myComponent; 

tidak membuat java.awt.Componentobjek; ia mewujudkan pemboleh ubah rujukan jenis java.lang.Component. Walaupun rujukan mempunyai jenis seperti objek, tidak ada padanan yang tepat antara rujukan dan jenis objek - nilai rujukan mungkin null, objek dengan jenis yang sama dengan rujukan, atau objek dari setiap subkelas (iaitu, kelas turun dari) jenis rujukan. Dalam kes ini, java.awt.Componentadalah kelas abstrak, jadi kita tahu bahawa tidak boleh ada objek dari jenis yang sama dengan rujukan kita, tetapi pasti ada objek dari subkelas dari jenis rujukan itu.

Polimorfisme dan lakonan

Jenis rujukan menentukan bagaimana objek yang dirujuk - iaitu, objek yang merupakan nilai rujukan - dapat digunakan. Sebagai contoh, dalam contoh di atas, penggunaan kod myComponentdapat menggunakan salah satu kaedah yang ditentukan oleh kelas java.awt.Component, atau mana-mana kacamata supernya, pada objek yang dirujuk.

Walau bagaimanapun, kaedah yang sebenarnya dilaksanakan oleh panggilan ditentukan bukan oleh jenis rujukan itu sendiri, melainkan oleh jenis objek yang dirujuk. Ini adalah asas asas polimorfisme - subkelas dapat mengatasi kaedah yang ditentukan dalam kelas induk untuk melaksanakan tingkah laku yang berbeza. Dalam contoh pemboleh ubah contoh kami, jika objek yang dirujuk sebenarnya adalah contoh java.awt.Button, perubahan keadaan yang dihasilkan dari setLabel("Push Me")panggilan akan berbeza dari yang dihasilkan jika objek yang dirujuk adalah contoh dari java.awt.Label.

Selain definisi kelas, program Java juga menggunakan definisi antara muka. Perbezaan antara antara muka dan kelas adalah bahawa antara muka hanya menentukan sekumpulan tingkah laku (dan, dalam beberapa kes, pemalar), sementara kelas menentukan pelaksanaan. Oleh kerana antara muka tidak menentukan pelaksanaan, objek tidak boleh menjadi contoh antara muka. Akan tetapi, mereka dapat menjadi contoh kelas yang menerapkan antara muka. Rujukan boleh terdiri dari jenis antara muka, dalam hal ini objek yang dirujuk mungkin merupakan contoh dari kelas apa pun yang menerapkan antara muka (baik secara langsung atau melalui beberapa kelas leluhur).

Casting digunakan untuk menukar antara jenis - antara jenis rujukan khususnya, untuk jenis operasi casting yang kami minati di sini. Operasi upcast (juga disebut pelebaran penukaran dalam Spesifikasi Bahasa Java) menukar rujukan subkelas menjadi rujukan kelas leluhur. Operasi pemutus ini biasanya automatik, kerana selalu selamat dan dapat dilaksanakan secara langsung oleh penyusun.

Operasi Downcast (juga disebut penukaran penyempitan dalam Spesifikasi Bahasa Java) menukar rujukan kelas leluhur menjadi rujukan subkelas. Operasi casting ini menghasilkan overhead pelaksanaan, kerana Java mengharuskan pemeran diperiksa pada waktu berjalan untuk memastikannya sah. Sekiranya objek yang dirujuk bukanlah contoh dari jenis sasaran untuk pemeran atau subkelas dari jenis itu, percubaan pemeran tidak dibenarkan dan mesti membuang a java.lang.ClassCastException.

The instanceofoperator di Jawa membolehkan anda untuk menentukan sama ada atau tidak operasi pemutus tertentu dibenarkan tanpa benar-benar cuba operasi. Oleh kerana kos prestasi cek jauh lebih rendah daripada pengecualian yang dihasilkan oleh percubaan pemeran yang tidak diizinkan, pada amnya bijak menggunakan instanceofujian bila-bila masa anda tidak pasti bahawa jenis rujukan adalah yang anda mahukan . Sebelum melakukannya, bagaimanapun, anda harus memastikan bahawa anda mempunyai cara yang munasabah untuk menangani rujukan jenis yang tidak diingini - jika tidak, anda juga boleh membiarkan pengecualian dilemparkan dan mengatasinya pada tahap yang lebih tinggi dalam kod anda.

Berhati-hati dengan angin

Casting membolehkan penggunaan pengaturcaraan generik di Java, di mana kod ditulis untuk berfungsi dengan semua objek kelas yang turun dari beberapa kelas asas (selalunya java.lang.Object, untuk kelas utiliti). Walau bagaimanapun, penggunaan casting menyebabkan satu set masalah yang unik. Pada bahagian seterusnya kita akan melihat kesan pada prestasi, tetapi mari kita pertimbangkan terlebih dahulu kesan pada kod itu sendiri. Berikut adalah contoh yang menggunakan java.lang.Vectorkelas koleksi generik :

beberapa Nombor Vektor peribadi; ... public void doSomething () {... int n = ... Nombor integer = (Integer) someNumbers.elementAt (n); ...}

Kod ini menyajikan masalah yang berpotensi dari segi kejelasan dan pemeliharaan. Sekiranya seseorang selain daripada pembangun asal mengubah kod pada satu ketika, dia mungkin berfikir bahawa dia dapat menambahkan koleksi java.lang.Doublepada someNumberskoleksi, kerana ini adalah subkelas java.lang.Number. Segala sesuatu akan disusun dengan baik jika dia mencobanya, tetapi pada suatu tahap pelaksanaan yang tidak tentu dia mungkin akan java.lang.ClassCastExceptiondilemparkan ketika percubaan dilemparkan ke atas java.lang.Integerdilaksanakan untuk nilai tambahnya.

Masalahnya di sini adalah bahawa penggunaan pemutus memotong pemeriksaan keselamatan yang dibina ke dalam penyusun Java; pengaturcara akhirnya memburu kesalahan semasa pelaksanaan, kerana penyusun tidak akan menangkapnya. Ini tidak membimbangkan sendiri, tetapi jenis kesalahan penggunaan ini sering kali tersembunyi dengan bijak semasa anda menguji kod anda, hanya untuk menampakkan dirinya ketika program ini dimasukkan.

Tidak mengejutkan, sokongan untuk teknik yang membolehkan pengkompil untuk mengesan kesalahan penggunaan jenis ini adalah salah satu peningkatan yang sangat diperlukan untuk Java. Terdapat satu projek yang sedang berjalan dalam Proses Komuniti Java yang menyelidiki hanya menambahkan sokongan ini: nombor projek JSR-000014, Tambah Jenis Generik ke Bahasa Pengaturcaraan Java (lihat bahagian Sumber di bawah untuk lebih jelasnya.) Dalam kesinambungan artikel ini, akan datang bulan depan, kami akan melihat projek ini dengan lebih terperinci dan membincangkan bagaimana kemungkinan ia dapat membantu dan di mana kemungkinan ia akan menyebabkan kami menginginkan lebih banyak lagi.

Isu prestasi

Sudah lama diakui bahawa casting dapat memudaratkan prestasi di Java, dan anda dapat meningkatkan prestasi dengan meminimumkan casting dalam kod yang banyak digunakan. Panggilan kaedah, terutamanya panggilan melalui antara muka, juga sering disebut sebagai kemungkinan hambatan prestasi. Generasi JVM semasa telah jauh dari pendahulunya, dan perlu diperiksa untuk melihat seberapa baik prinsip-prinsip ini berlaku hingga kini.

Untuk artikel ini, saya membuat satu siri ujian untuk melihat betapa pentingnya faktor-faktor ini terhadap prestasi dengan JVM semasa. Hasil ujian diringkaskan menjadi dua jadual di bar sisi, Jadual 1 menunjukkan kaedah panggilan overhead dan Jadual 2 casting overhead. Kod sumber penuh untuk program ujian juga tersedia dalam talian (lihat bahagian Sumber di bawah untuk maklumat lebih lanjut).

Untuk merumuskan kesimpulan ini untuk pembaca yang tidak ingin membaca perincian dalam jadual, jenis panggilan kaedah tertentu dan pemeran masih cukup mahal, dalam beberapa kes memerlukan hampir sepanjang peruntukan objek sederhana. Sekiranya mungkin, jenis operasi ini harus dielakkan dalam kod yang perlu dioptimumkan untuk prestasi.

Khususnya, panggilan ke kaedah penggantian (kaedah yang diganti di mana-mana kelas yang dimuat, bukan hanya kelas sebenar objek) dan panggilan melalui antara muka jauh lebih mahal daripada kaedah panggilan sederhana. HotSpot Server JVM 2.0 beta yang digunakan dalam ujian ini malah akan menukar banyak panggilan kaedah mudah menjadi kod sebaris, mengelakkan sebarang overhead untuk operasi tersebut. Walau bagaimanapun, HotSpot menunjukkan prestasi terburuk di antara JVM yang diuji untuk kaedah dan panggilan yang diganti melalui antara muka.

Untuk casting (tentu saja downcasting), JVM yang diuji secara amnya memastikan prestasi mencapai tahap yang wajar. HotSpot melakukan pekerjaan yang luar biasa dengan ini dalam kebanyakan ujian penanda aras, dan, seperti kaedah panggilan, dalam banyak kes mudah hampir dapat menghilangkan overhead casting. Untuk situasi yang lebih rumit, seperti pemeran diikuti dengan kaedah panggilan untuk diganti, semua JVM yang diuji menunjukkan penurunan prestasi yang ketara.

Versi HotSpot yang diuji juga menunjukkan prestasi yang sangat buruk ketika suatu objek dilemparkan ke berbagai jenis rujukan berturut-turut (bukannya selalu dilemparkan ke jenis sasaran yang sama). Situasi ini kerap muncul di perpustakaan seperti Swing yang menggunakan hierarki kelas yang mendalam.

Dalam kebanyakan kes, overhead kedua-dua kaedah panggilan dan penghantaran kecil berbanding dengan masa peruntukan objek yang dilihat dalam artikel bulan lalu. Walau bagaimanapun, operasi ini akan sering digunakan lebih kerap daripada peruntukan objek, jadi mereka masih boleh menjadi sumber masalah prestasi yang signifikan.

Selebihnya dari artikel ini, kami akan membincangkan beberapa teknik khusus untuk mengurangkan keperluan penghantaran kod anda. Secara khusus, kita akan melihat bagaimana casting sering timbul dari cara subclass berinteraksi dengan kelas asas, dan meneroka beberapa teknik untuk menghilangkan jenis casting ini. Pada bulan depan, pada bahagian kedua melihat pemeran ini, kami akan mempertimbangkan satu lagi sebab utama pemeran, penggunaan koleksi generik.

Kelas asas dan pemutus

There are several common uses of casting in Java programs. For instance, casting is often used for the generic handling of some functionality in a base class that may be extended by a number of subclasses. The following code shows a somewhat contrived illustration of this usage:

 // simple base class with subclasses public abstract class BaseWidget { ... } public class SubWidget extends BaseWidget { ... public void doSubWidgetSomething() { ... } } ... // base class with subclasses, using the prior set of classes public abstract class BaseGorph { // the Widget associated with this Gorph private BaseWidget myWidget; ... // set the Widget associated with this Gorph (only allowed for subclasses) protected void setWidget(BaseWidget widget) { myWidget = widget; } // get the Widget associated with this Gorph public BaseWidget getWidget() { return myWidget; } ... // return a Gorph with some relation to this Gorph // this will always be the same type as it's called on, but we can only // return an instance of our base class public abstract BaseGorph otherGorph() { ... } } // Gorph subclass using a Widget subclass public class SubGorph extends BaseGorph { // return a Gorph with some relation to this Gorph public BaseGorph otherGorph() { ... } ... public void anyMethod() { ... // set the Widget we're using SubWidget widget = ... setWidget(widget); ... // use our Widget ((SubWidget)getWidget()).doSubWidgetSomething(); ... // use our otherGorph SubGorph other = (SubGorph) otherGorph(); ... } }