Apakah casting Java memperkenalkan overhead? Mengapa?

104

Apakah ada overhead saat kita mentransmisikan objek dari satu jenis ke jenis lainnya? Atau kompiler hanya menyelesaikan semuanya dan tidak ada biaya pada saat dijalankan?

Apakah ini hal yang umum, atau ada kasus yang berbeda?

Misalnya, kita memiliki larik Object [], di mana setiap elemen mungkin memiliki tipe yang berbeda. Tapi kita selalu tahu pasti bahwa, katakanlah, elemen 0 adalah Double, elemen 1 adalah String. (Saya tahu ini adalah desain yang salah, tetapi anggap saja saya harus melakukan ini.)

Apakah informasi tipe Java masih disimpan pada saat dijalankan? Atau semuanya dilupakan setelah kompilasi, dan jika kita melakukan elemen (Double) [0], kita hanya akan mengikuti pointer dan menafsirkan 8 byte tersebut sebagai double, apapun itu?

Saya sangat tidak jelas tentang bagaimana tipe dilakukan di Java. Jika Anda memiliki rekomendasi untuk buku atau artikel, terima kasih juga.

Phil
sumber
Performa instanceof dan casting cukup baik. Saya memposting beberapa waktu di Java7 di sekitar pendekatan berbeda untuk masalah di sini: stackoverflow.com/questions/16320014/…
Wheezil
Pertanyaan lain ini memiliki jawaban yang sangat bagus stackoverflow.com/questions/16741323/…
pengguna454322

Jawaban:

78

Ada 2 jenis casting:

Transmisi implisit , saat Anda mentransmisikan dari satu tipe ke tipe yang lebih luas, yang dilakukan secara otomatis dan tidak ada overhead:

String s = "Cast";
Object o = s; // implicit casting

Transmisi eksplisit , saat Anda beralih dari tipe yang lebih luas ke tipe yang lebih sempit. Untuk kasus ini, Anda harus secara eksplisit menggunakan transmisi seperti itu:

Object o = someObject;
String s = (String) o; // explicit casting

Dalam kasus kedua ini, ada overhead dalam waktu proses, karena kedua jenis tersebut harus diperiksa dan jika transmisi tidak memungkinkan, JVM harus menampilkan ClassCastException.

Diambil dari JavaWorld: Biaya casting

Pengecoran digunakan untuk mengonversi antar jenis - antara jenis referensi khususnya, untuk jenis operasi pengecoran yang kami minati di sini.

Operasi upcast (juga disebut konversi pelebaran dalam Spesifikasi Bahasa Java) mengonversi referensi subkelas menjadi referensi kelas leluhur. Operasi pengecoran ini biasanya otomatis, karena selalu aman dan dapat diterapkan secara langsung oleh penyusun.

Operasi downcast (juga disebut konversi penyempitan dalam Spesifikasi Bahasa Java) mengonversi referensi kelas leluhur menjadi referensi subkelas. Operasi casting ini menciptakan overhead eksekusi, karena Java mengharuskan cast diperiksa pada waktu proses untuk memastikannya valid. Jika objek yang direferensikan bukan instance dari salah satu jenis target untuk cast atau subclass dari jenis tersebut, percobaan cast tidak diizinkan dan harus menampilkan java.lang.ClassCastException.

Alex Ntousias
sumber
101
Artikel JavaWorld itu berusia 10+ tahun, jadi saya akan mengambil pernyataan apa pun yang dibuatnya tentang kinerja dengan butiran garam terbaik Anda yang sangat besar.
Skaffman
1
@skaffman, Sebenarnya saya akan mengambil pernyataan apa pun yang dibuatnya (terlepas dari kinerja tidak) dengan sebutir garam.
Pacerier
apakah itu akan menjadi kasus yang sama, jika saya tidak menetapkan objek yang dicor ke referensi dan hanya memanggil metode di atasnya? seperti((String)o).someMethodOfCastedClass()
Parth Vishvajit
3
Sekarang artikel itu hampir berumur 20 tahun. Dan jawabannya juga berumur bertahun-tahun. Pertanyaan ini membutuhkan jawaban modern.
Raslanove
Bagaimana dengan tipe primitif? Maksud saya, apakah, misalnya - casting dari int ke short menyebabkan overhead yang serupa?
Luke1985
44

Untuk implementasi Java yang wajar:

Setiap objek memiliki header yang berisi, antara lain, penunjuk ke jenis runtime (misalnya Doubleatau String, tetapi tidak akan pernah CharSequenceatau AbstractList). Dengan asumsi kompiler runtime (umumnya HotSpot dalam kasus Sun) tidak dapat menentukan tipe secara statis, beberapa pemeriksaan perlu dilakukan oleh kode mesin yang dihasilkan.

Pertama, penunjuk ke jenis runtime perlu dibaca. Ini diperlukan untuk memanggil metode virtual dalam situasi yang sama.

Untuk mentransmisikan ke tipe kelas, diketahui dengan tepat berapa banyak kelas super yang ada sampai Anda menekannya java.lang.Object, sehingga tipe tersebut dapat dibaca pada offset konstan dari penunjuk tipe (sebenarnya delapan pertama di HotSpot). Sekali lagi ini analog dengan membaca penunjuk metode untuk metode virtual.

Kemudian nilai baca hanya membutuhkan perbandingan dengan jenis cast statis yang diharapkan. Bergantung pada arsitektur set instruksi, instruksi lain perlu bercabang (atau kesalahan) pada cabang yang salah. ISA seperti ARM 32-bit memiliki instruksi bersyarat dan mungkin dapat membuat jalur sedih melewati jalur bahagia.

Antarmuka lebih sulit karena beberapa pewarisan antarmuka. Biasanya, dua cast terakhir ke antarmuka di-cache dalam tipe runtime. DI masa-masa awal (lebih dari satu dekade yang lalu), antarmuka agak lambat, tetapi itu tidak lagi relevan.

Mudah-mudahan Anda dapat melihat bahwa hal semacam ini sebagian besar tidak relevan dengan kinerja. Kode sumber Anda lebih penting. Dalam hal kinerja, hit terbesar dalam skenario Anda adalah kemungkinan cache luput dari pengejaran penunjuk objek di semua tempat (informasi jenis tentu saja akan umum).

Tom Hawtin - tackline
sumber
1
menarik - jadi apakah ini berarti bahwa untuk kelas non-antarmuka jika saya menulis subkelas Superclass sc = (Superclass); bahwa kompiler (jit ie: load time) akan "secara statis" dimasukkan ke dalam offset dari Object di masing-masing Superclass dan Subclass di header "Class" mereka dan kemudian melalui add + bandingkan sederhana dapat menyelesaikan masalah? - itu bagus dan cepat :) Untuk antarmuka saya akan berasumsi tidak lebih buruk dari hashtable kecil atau btree?
peterk
@peterk Untuk mentransmisikan antar kelas, baik alamat objek dan "vtbl" (tabel penunjuk metode, ditambah tabel hierarki kelas, cache antarmuka, dll) tidak berubah. Jadi pemeran [kelas] memeriksa jenisnya, dan jika cocok, tidak ada hal lain yang harus terjadi.
Tom Hawtin - tackline
8

Misalnya, kita memiliki larik Object [], di mana setiap elemen mungkin memiliki tipe yang berbeda. Tapi kita selalu tahu pasti bahwa, katakanlah, elemen 0 adalah Double, elemen 1 adalah String. (Saya tahu ini adalah desain yang salah, tetapi anggap saja saya harus melakukan ini.)

Kompilator tidak mencatat tipe elemen individual dari sebuah array. Ini hanya memeriksa bahwa tipe dari setiap ekspresi elemen dapat ditetapkan ke tipe elemen array.

Apakah informasi tipe Java masih disimpan pada saat dijalankan? Atau semuanya dilupakan setelah kompilasi, dan jika kita melakukan elemen (Double) [0], kita hanya akan mengikuti pointer dan menafsirkan 8 byte tersebut sebagai double, apapun itu?

Beberapa informasi disimpan pada waktu proses, tetapi bukan tipe statis dari masing-masing elemen. Anda dapat mengetahui ini dari melihat format file kelas.

Secara teoritis mungkin bahwa compiler JIT dapat menggunakan "escape analysis" untuk menghilangkan pemeriksaan jenis yang tidak perlu di beberapa tugas. Namun, melakukan ini hingga tingkat yang Anda sarankan akan berada di luar batas pengoptimalan realistis. Imbalan dari menganalisis jenis elemen individu akan terlalu kecil.

Selain itu, orang tidak boleh menulis kode aplikasi seperti itu.

Stephen C
sumber
1
Bagaimana dengan kaum primitif? (float) Math.toDegrees(theta)Apakah akan ada biaya tambahan yang signifikan di sini juga?
SD
2
Ada biaya tambahan untuk beberapa pemeran primitif. Apakah itu signifikan tergantung pada konteksnya.
Stephen C
6

Instruksi kode byte untuk melakukan casting saat runtime dipanggil checkcast. Anda dapat membongkar kode Java menggunakan javapuntuk melihat instruksi apa yang dihasilkan.

Untuk array, Java menyimpan informasi tipe pada waktu proses. Sebagian besar waktu, kompilator akan menangkap kesalahan tipe untuk Anda, tetapi ada kasus di mana Anda akan mengalami ArrayStoreExceptionketika mencoba menyimpan objek dalam array, tetapi jenisnya tidak cocok (dan kompilator tidak menangkapnya) . The bahasa Jawa spesifikasi memberikan contoh berikut:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        } catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
}

Point[] pa = cpavalid karena ColoredPointmerupakan subclass dari Point, tetapi pa[0] = new Point()tidak valid.

Ini berlawanan dengan tipe generik, di mana tidak ada informasi tipe yang disimpan pada waktu proses. Kompilator memasukkan checkcastinstruksi jika diperlukan.

Perbedaan dalam pengetikan untuk tipe dan larik generik ini membuatnya sering tidak cocok untuk mencampur larik dan tipe generik.

JesperE
sumber
2

Secara teori, ada overhead yang diperkenalkan. Namun, JVM modern itu cerdas. Setiap implementasi berbeda, tetapi bukan tidak masuk akal untuk mengasumsikan bahwa mungkin ada implementasi yang dioptimalkan JIT tanpa pengecekan ketika itu dapat menjamin bahwa tidak akan pernah ada konflik. Adapun JVM tertentu yang menawarkan ini, saya tidak bisa memberi tahu Anda. Saya harus mengakui bahwa saya sendiri ingin mengetahui secara spesifik pengoptimalan JIT, tetapi ini adalah hal yang perlu dikhawatirkan oleh teknisi JVM.

Moral dari cerita ini adalah menulis kode yang dapat dimengerti terlebih dahulu. Jika Anda mengalami pelambatan, buat profil dan identifikasi masalah Anda. Kemungkinannya bagus karena itu bukan karena casting. Jangan pernah mengorbankan kode yang bersih dan aman dalam upaya untuk mengoptimalkannya SAMPAI ANDA PERLU.

HesNotTheStig
sumber