Apa efek dari pengecualian pada kinerja di Jawa?


496

Pertanyaan: Apakah penanganan pengecualian di Jawa sebenarnya lambat?

Kearifan konvensional, serta banyak hasil Google, mengatakan bahwa logika yang luar biasa tidak boleh digunakan untuk aliran program normal di Jawa. Dua alasan biasanya diberikan,

  1. itu sangat lambat - bahkan urutan besarnya lebih lambat dari kode biasa (alasan yang diberikan bervariasi),

dan

  1. itu berantakan karena orang hanya mengharapkan kesalahan ditangani dalam kode yang luar biasa.

Pertanyaan ini tentang # 1.

Sebagai contoh, halaman ini menjelaskan penanganan pengecualian Java sebagai "sangat lambat" dan menghubungkan kelambatan dengan pembuatan string pesan pengecualian - "string ini kemudian digunakan dalam membuat objek pengecualian yang dilemparkan. Ini tidak cepat." Artikel Penanganan Pengecualian Efektif di Jawa mengatakan bahwa "alasan untuk ini adalah karena aspek penciptaan objek penanganan pengecualian, yang dengan demikian membuat pengecualian melempar secara inheren lambat". Alasan lain di luar sana adalah bahwa generasi jejak tumpukan adalah apa yang memperlambatnya.

Pengujian saya (menggunakan Java 1.6.0_07, Java HotSpot 10.0, pada Linux 32 bit), menunjukkan bahwa penanganan pengecualian tidak lebih lambat dari kode biasa. Saya mencoba menjalankan metode dalam satu loop yang mengeksekusi beberapa kode. Di akhir metode, saya menggunakan boolean untuk menunjukkan apakah akan kembali atau melempar . Dengan cara ini, proses yang sebenarnya sama. Saya mencoba menjalankan metode dalam urutan yang berbeda dan rata-rata waktu pengujian saya, berpikir itu mungkin pemanasan JVM. Dalam semua pengujian saya, lemparan setidaknya secepat pengembalian, jika tidak lebih cepat (hingga 3,1% lebih cepat). Saya benar-benar terbuka dengan kemungkinan bahwa pengujian saya salah, tetapi saya belum melihat apa pun di luar sana dalam hal contoh kode, perbandingan pengujian, atau hasil dalam satu atau dua tahun terakhir yang menunjukkan penanganan pengecualian di Jawa untuk benar-benar menjadi lambat.

Apa yang menuntun saya ke jalan ini adalah API yang saya butuhkan untuk menggunakan itu melemparkan pengecualian sebagai bagian dari logika kontrol normal. Saya ingin memperbaikinya dalam penggunaannya, tetapi sekarang saya mungkin tidak dapat memperbaikinya. Akankah saya malah harus memuji mereka pada pemikiran ke depan mereka?

Dalam makalah penanganan Efisien Java dalam kompilasi just-in-time , penulis menyarankan bahwa kehadiran handler pengecualian saja, bahkan jika tidak ada pengecualian yang dilemparkan, cukup untuk mencegah kompiler JIT dari mengoptimalkan kode dengan benar, sehingga memperlambatnya . Saya belum menguji teori ini.


8
Saya tahu Anda tidak bertanya tentang 2), tetapi Anda harus benar-benar menyadari bahwa menggunakan pengecualian untuk aliran program tidak lebih baik daripada menggunakan GOTO. Beberapa orang membela goto, beberapa orang akan membela apa yang Anda bicarakan, tetapi jika Anda bertanya kepada seseorang yang telah menerapkan dan memelihara untuk jangka waktu tertentu, mereka akan memberi tahu Anda bahwa keduanya miskin sulit mempertahankan praktik desain (dan mungkin akan mengutuk nama orang yang berpikir mereka cukup pintar untuk membuat keputusan untuk menggunakannya).
Bill K

80
Bill, mengklaim bahwa menggunakan pengecualian untuk aliran program tidak lebih baik daripada menggunakan GOTO tidak lebih baik daripada mengklaim bahwa menggunakan kondisional dan loop untuk aliran program tidak lebih baik daripada menggunakan GOTO. Itu adalah herring merah. Jelaskan dirimu. Pengecualian dapat dan digunakan secara efektif untuk aliran program dalam bahasa lain. Kode Python idiomatik menggunakan pengecualian secara teratur, misalnya. Saya dapat dan telah memelihara kode yang menggunakan pengecualian dengan cara ini (bukan Java), dan saya tidak berpikir ada sesuatu yang salah dengannya.
mmalone

14
@mmalone menggunakan Pengecualian untuk aliran kontrol normal adalah ide yang buruk di Jawa karena pilihan paradigma dilakukan dengan cara itu . Baca Bloch EJ2 - dia dengan jelas menyatakan bahwa, kutipan, (Butir 57) exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow- memberikan penjelasan lengkap dan luas mengapa. Dan dia adalah orang yang menulis Java lib. Karena itu, dialah yang menentukan kontrak API kelas. Saya setuju dengan Bill K untuk hal ini.

8
@ OndraŽižka Jika beberapa kerangka melakukan ini (gunakan Pengecualian dalam kondisi tidak-luar biasa), itu cacat dan rusak oleh desain, melanggar kontrak kelas Pengecualian bahasa. Hanya karena beberapa orang menulis kode yang buruk tidak membuatnya kurang buruk.

8
Tidak lain dari pembuat stackoverflow.com yang salah tentang pengecualian. Aturan emas pengembangan perangkat lunak tidak pernah membuat hal yang kompleks dan sulit. Dia menulis: "Memang benar bahwa apa yang seharusnya menjadi program 3 baris sederhana sering berkembang menjadi 48 baris ketika Anda melakukan pengecekan kesalahan yang baik, tapi itu hidup, ..." Ini adalah pencarian untuk kemurnian, bukan kesederhanaan.
sf_jeff

Jawaban:


345

Itu tergantung bagaimana pengecualian diterapkan. Cara paling sederhana adalah menggunakan setjmp dan longjmp. Itu berarti semua register CPU dituliskan ke stack (yang sudah memakan waktu) dan mungkin beberapa data lain perlu dibuat ... semua ini sudah terjadi dalam pernyataan coba. Pernyataan melempar perlu melepaskan tumpukan dan mengembalikan nilai-nilai semua register (dan kemungkinan nilai-nilai lain di VM). Jadi coba dan lempar sama lambatnya, dan itu cukup lambat, namun jika tidak ada pengecualian yang dilemparkan, keluar dari blok uji coba tidak memerlukan waktu apa pun dalam kebanyakan kasus (karena semuanya diletakkan di tumpukan yang membersihkan secara otomatis jika metode itu ada).

Sun dan yang lainnya mengakui, bahwa ini mungkin suboptimal dan tentu saja VMs menjadi lebih cepat dan lebih cepat dari waktu ke waktu. Ada cara lain untuk menerapkan pengecualian, yang membuat coba itu sendiri cepat kilat (sebenarnya tidak ada yang terjadi untuk dicoba sama sekali secara umum - semua yang perlu terjadi sudah dilakukan ketika kelas dimuat oleh VM) dan itu membuat melempar tidak terlalu lambat . Saya tidak tahu JVM mana yang menggunakan teknik baru yang lebih baik ini ...

... tetapi apakah Anda menulis dalam Java sehingga kode Anda nanti hanya berjalan pada satu JVM pada satu sistem tertentu? Karena jika mungkin pernah berjalan di platform lain atau versi JVM lainnya (mungkin dari vendor lain), siapa bilang mereka juga menggunakan implementasi cepat? Yang cepat lebih rumit daripada yang lambat dan tidak mudah pada semua sistem. Anda ingin tetap portabel? Maka jangan mengandalkan pengecualian yang cepat.

Ini juga membuat perbedaan besar apa yang Anda lakukan dalam blok percobaan. Jika Anda membuka blok try dan tidak pernah memanggil metode apa pun dari dalam blok try ini, blok try akan sangat cepat, karena JIT kemudian dapat benar-benar memperlakukan lemparan seperti goto sederhana. Tidak perlu menyimpan tumpukan-negara juga tidak perlu melepas tumpukan jika pengecualian dilemparkan (hanya perlu melompat ke penangan tangkapan). Namun, ini bukan yang biasa Anda lakukan. Biasanya Anda membuka blok try dan kemudian memanggil metode yang mungkin membuang pengecualian, kan? Dan bahkan jika Anda hanya menggunakan blok percobaan dalam metode Anda, metode seperti apa ini, yang tidak memanggil metode lain? Apakah hanya menghitung angka? Lalu untuk apa Anda membutuhkan pengecualian? Ada banyak cara yang lebih elegan untuk mengatur aliran program. Untuk hal lain selain matematika sederhana,

Lihat kode tes berikut:

public class Test {
    int value;


    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    // Calculates without exception
    public void method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            System.out.println("You'll never see this!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            t.method1(i);
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

Hasil:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

Perlambatan dari blok uji coba terlalu kecil untuk mengesampingkan faktor pembaur seperti proses latar belakang. Tapi blok penahan membunuh semuanya dan membuatnya 66 kali lebih lambat!

Seperti yang saya katakan, hasilnya tidak akan seburuk itu jika Anda mencoba / menangkap dan melempar semua dalam metode yang sama (method3), tetapi ini adalah optimasi JIT khusus yang tidak akan saya andalkan. Dan bahkan ketika menggunakan optimasi ini, lemparannya masih sangat lambat. Jadi saya tidak tahu apa yang Anda coba lakukan di sini, tetapi pasti ada cara yang lebih baik untuk melakukannya daripada menggunakan try / catch / throw.


7
Jawaban yang bagus tetapi saya hanya ingin menambahkan bahwa sejauh yang saya tahu, System.nanoTime () harus digunakan untuk mengukur kinerja, bukan System.currentTimeMillis ().
Simon Forsberg

10
@ SimonAndréForsberg nanoTime()membutuhkan Java 1.5 dan saya hanya memiliki Java 1.4 yang tersedia pada sistem yang saya gunakan untuk menulis kode di atas. Juga tidak memainkan peran besar dalam latihan. Satu-satunya perbedaan antara keduanya adalah bahwa satu nanodetik dan milidetik lainnya dan yang nanoTimetidak dipengaruhi oleh manipulasi jam (yang tidak relevan, kecuali jika Anda atau proses sistem memodifikasi jam sistem tepat pada saat kode tes berjalan). Namun, secara umum Anda benar, nanoTimetentu saja merupakan pilihan yang lebih baik.
Mecki

2
Perlu dicatat bahwa tes Anda adalah kasus yang ekstrem. Anda menunjukkan kinerja yang sangat kecil hit untuk kode dengan tryblok, tetapi tidak throw. throwTes Anda melemparkan pengecualian 50% dari waktu ia melewati try. Itu jelas situasi di mana kegagalan itu tidak luar biasa . Memotong itu menjadi hanya 10% secara besar-besaran memotong hit kinerja. Masalah dengan tes semacam ini adalah tes ini mendorong orang untuk berhenti menggunakan pengecualian sama sekali. Menggunakan pengecualian, untuk penanganan kasus yang luar biasa, berkinerja jauh lebih baik daripada yang ditunjukkan oleh tes Anda.
Nate

1
@Nate Pertama-tama, saya mengatakan dengan sangat jelas bahwa semua ini tergantung pada bagaimana pengecualian diterapkan. Saya hanya menguji SATU implementasi spesifik, namun ada banyak dan Oracle dapat memilih yang sama sekali berbeda dengan setiap rilis. Kedua, jika pengecualian hanya luar biasa, seperti biasanya, tentu saja dampaknya lebih kecil, ini sangat jelas, sehingga saya benar-benar tidak berpikir seseorang harus menunjukkannya secara eksplisit dan karenanya saya tidak dapat memahami maksud Anda di sini sama sekali. Dan ketiga, pengecualian terlalu sering menggunakan itu buruk, semua orang setuju akan hal itu, jadi menggunakannya dengan sangat hati-hati adalah hal yang sangat baik.
Mecki

4
@ Glide Lemparan tidak seperti bersih return. Itu meninggalkan metode di suatu tempat di tengah tubuh, mungkin bahkan di tengah operasi (yang sejauh ini hanya diselesaikan oleh 50%) dan catchblok mungkin 20 tumpukan bingkai ke atas (metode memiliki tryblok, memanggil metode1, yang memanggil method2, yang memanggil mehtod3, ..., dan di method20 di tengah operasi pengecualian dilemparkan). Tumpukan harus dilepas 20 frame ke atas, semua operasi yang belum selesai harus dibatalkan (operasi tidak boleh setengah selesai) dan register CPU harus dalam keadaan bersih. Ini semua menghabiskan waktu.
Mecki

256

FYI, saya memperpanjang percobaan yang dilakukan Mecki:

method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2

3 yang pertama sama dengan Mecki (laptop saya jelas lebih lambat).

method4 identik dengan method3 kecuali bahwa ia menciptakan new Integer(1)daripada melakukan throw new Exception().

Method5 seperti method3 kecuali ia menciptakan new Exception()tanpa membuangnya.

method6 seperti method3 kecuali bahwa ia melempar pengecualian yang sudah dibuat sebelumnya (variabel instance) daripada membuat yang baru.

Di Jawa, banyak biaya untuk melempar eksepsi adalah waktu yang dihabiskan untuk mengumpulkan jejak stack, yang terjadi ketika objek eksepsi dibuat. Biaya aktual untuk melemparkan pengecualian, meskipun besar, jauh lebih kecil daripada biaya untuk membuat pengecualian.


49
+1 Jawaban Anda membahas masalah inti - waktu yang dibutuhkan untuk melepas dan melacak tumpukan, dan yang kedua adalah melempar kesalahan. Saya akan memilih ini sebagai jawaban akhir.
Insinyur

9
bagus. ~ 70% membuat pengecualian, ~ 30% membuangnya. info bagus
chaqke

1
@ Basil - Anda harus dapat mengetahui hal itu dari angka-angka di atas.
Hot Licks

2
@HotLicks dan inilah mengapa penting untuk mengetahui versi Java mana yang digunakan dalam postingan
Thorbjørn Ravn Andersen

3
Kita dapat berkomentar bahwa dalam kode standar, membuat dan melempar pengecualian terjadi dalam kasus yang jarang terjadi (saat runtime maksud saya), jika tidak demikian, kondisi runtime sangat buruk, atau desain itu sendiri masalahnya; dalam kedua kasus itu pertunjukan tidak menjadi perhatian ...
Jean-Baptiste Yunès

70

Aleksey Shipilëv melakukan analisis yang sangat menyeluruh di mana ia memberikan tolok ukur pengecualian Java dalam berbagai kombinasi kondisi:

  • Pengecualian yang baru dibuat vs. pengecualian yang telah dibuat sebelumnya
  • Stack trace diaktifkan vs dinonaktifkan
  • Jejak tumpukan yang diminta vs tidak pernah diminta
  • Terperangkap di tingkat atas vs dipecah kembali di setiap tingkat vs dirantai / dibungkus di setiap tingkat
  • Berbagai tingkat kedalaman tumpukan panggilan Java
  • Tidak ada optimisasi inlining vs inlining ekstrim vs pengaturan default
  • Bidang yang ditentukan pengguna dibaca vs tidak dibaca

Dia juga membandingkannya dengan kinerja memeriksa kode kesalahan pada berbagai tingkat frekuensi kesalahan.

Kesimpulannya (dikutip kata demi kata dari jabatannya) adalah:

  1. Pengecualian yang benar-benar luar biasa adalah penampil yang cantik. Jika Anda menggunakannya sebagaimana dirancang, dan hanya mengomunikasikan kasus-kasus yang benar-benar luar biasa di antara sejumlah besar kasus tidak-luar biasa yang ditangani oleh kode biasa, maka menggunakan pengecualian adalah kinerja yang menang.

  2. Biaya kinerja pengecualian memiliki dua komponen utama: konstruksi jejak tumpukan ketika Exception dipakai dan tumpukan tidak dibuka selama lemparan Exception.

  3. Biaya konstruksi jejak tumpukan sebanding dengan kedalaman tumpukan pada saat instantiasi pengecualian. Itu sudah buruk karena siapa di Bumi yang tahu kedalaman tumpukan di mana metode pelemparan ini akan disebut? Bahkan jika Anda mematikan pembuatan jejak stack dan / atau cache pengecualian, Anda hanya dapat menyingkirkan bagian ini dari biaya kinerja.

  4. Biaya pelonggaran tumpukan bergantung pada seberapa beruntungnya kita dengan mendekatkan penangan pengecualian dalam kode yang dikompilasi. Dengan hati-hati menyusun kode untuk menghindari pencarian penangan pengecualian yang dalam mungkin membantu kita menjadi lebih beruntung.

  5. Jika kita menghilangkan kedua efek, biaya kinerja pengecualian adalah biaya cabang lokal. Tidak peduli seberapa indah kedengarannya, itu tidak berarti Anda harus menggunakan Pengecualian sebagai aliran kontrol yang biasa, karena dalam kasus itu Anda berada di tangan mengoptimalkan kompiler! Anda hanya boleh menggunakannya dalam kasus yang benar-benar luar biasa, di mana frekuensi pengecualian mengamortisasi kemungkinan biaya tidak beruntung untuk meningkatkan pengecualian yang sebenarnya.

  6. Aturan praktis yang optimis tampaknya frekuensi 10 ^ -4 untuk pengecualian cukup luar biasa. Itu, tentu saja, tergantung pada bobot perkecualian itu sendiri, tindakan tepat yang diambil oleh penangan pengecualian, dll.

Hasilnya adalah bahwa ketika pengecualian tidak dilontarkan, Anda tidak membayar biaya, jadi ketika kondisi luar biasa cukup jarang, penanganan pengecualian lebih cepat daripada menggunakan ifsetiap waktu. Posting lengkap sangat layak dibaca.


41

Sayangnya, jawaban saya terlalu panjang untuk dikirim di sini. Jadi izinkan saya meringkas di sini dan merujuk Anda ke http://www.fuwjax.com/how-slow-are-java-exceptions/ untuk rincian lebih lanjut.

Pertanyaan sebenarnya di sini bukanlah "Seberapa lambat 'kegagalan dilaporkan sebagai pengecualian' dibandingkan dengan 'kode yang tidak pernah gagal'?" karena respons yang diterima mungkin membuat Anda percaya. Alih-alih, pertanyaannya adalah "Seberapa lambat 'kegagalan dilaporkan sebagai pengecualian' dibandingkan dengan kegagalan yang dilaporkan dengan cara lain?" Secara umum, dua cara lain untuk melaporkan kegagalan adalah dengan nilai sentinel atau dengan pembungkus hasil.

Nilai sentinel adalah upaya untuk mengembalikan satu kelas dalam hal kesuksesan dan yang lain dalam hal kegagalan. Anda dapat menganggapnya hampir seperti mengembalikan pengecualian daripada melemparkannya. Ini membutuhkan kelas induk bersama dengan objek sukses dan kemudian melakukan pemeriksaan "instanceof" dan pasangan berperan untuk mendapatkan informasi keberhasilan atau kegagalan.

Ternyata dengan risiko keamanan tipe, nilai Sentinel lebih cepat dari pengecualian, tetapi hanya dengan faktor sekitar 2x. Sekarang, itu mungkin tampak seperti banyak, tetapi 2x itu hanya menutupi biaya perbedaan implementasi. Dalam praktiknya, faktornya jauh lebih rendah karena metode kami yang mungkin gagal jauh lebih menarik daripada beberapa operator aritmatika seperti dalam kode contoh di tempat lain di halaman ini.

Pembungkus Hasil, di sisi lain, tidak mengorbankan keselamatan jenis sama sekali. Mereka membungkus informasi keberhasilan dan kegagalan dalam satu kelas. Jadi alih-alih "instanceof" mereka menyediakan "isSuccess ()" dan getter untuk objek keberhasilan dan kegagalan. Namun, objek hasil kira-kira 2x lebih lambat daripada menggunakan pengecualian. Ternyata membuat objek pembungkus baru setiap kali jauh lebih mahal daripada melemparkan pengecualian.

Selain itu, pengecualian adalah bahasa yang disediakan untuk menunjukkan bahwa suatu metode mungkin gagal. Tidak ada cara lain untuk mengetahui dari API mana metode yang diharapkan untuk selalu (kebanyakan) bekerja dan yang diharapkan untuk melaporkan kegagalan.

Pengecualian lebih aman daripada penjaga, lebih cepat dari objek hasil, dan kurang mengejutkan daripada keduanya. Saya tidak menyarankan bahwa coba / tangkap ganti jika / yang lain, tetapi pengecualian adalah cara yang tepat untuk melaporkan kegagalan, bahkan dalam logika bisnis.

Yang mengatakan, saya ingin menunjukkan bahwa dua cara yang paling sering mempengaruhi kinerja yang secara substansial saya jalankan adalah menciptakan objek yang tidak perlu dan loop bersarang. Jika Anda memiliki pilihan antara membuat pengecualian atau tidak membuat pengecualian, jangan membuat pengecualian. Jika Anda memiliki pilihan antara membuat pengecualian kadang-kadang atau membuat objek lain sepanjang waktu, maka buatlah pengecualian.


5
Saya memutuskan untuk menguji kinerja jangka panjang dari ketiga implementasi dibandingkan dengan implementasi kontrol yang memeriksa kegagalan tanpa pelaporan. Proses ini memiliki tingkat kegagalan sekitar 4%. Iterasi suatu tes melibatkan proses 10.000 kali terhadap salah satu strategi. Setiap strategi diuji 1000 kali dan 900 kali terakhir digunakan untuk menghasilkan statistik. Inilah rata-rata waktu dalam nanos: Kontrol 338 Pengecualian 429 Hasil 348 Sentinel 345
Fuwjax

2
Hanya untuk bersenang-senang saya menonaktifkan fillInStackTrace di tes pengecualian. Inilah saatnya sekarang: Kontrol 347 Pengecualian 351 Hasil 364 Sentinel 355
Fuwjax

1
Fuwjax, kecuali saya kehilangan sesuatu (dan saya akui saya hanya membaca posting SO Anda, bukan posting blog Anda), sepertinya dua komentar Anda di atas bertentangan dengan posting Anda. Saya kira angka yang lebih rendah lebih baik di benchmark Anda, kan? Dalam hal ini, menghasilkan pengecualian dengan fillInStackTrace diaktifkan (yang merupakan perilaku default dan biasa), menghasilkan kinerja yang lebih lambat daripada dua teknik lain yang Anda jelaskan. Apakah saya melewatkan sesuatu, atau apakah Anda benar-benar berkomentar untuk menyangkal posting Anda?
Felix GV

@Fuwjax - cara untuk menghindari pilihan "rock and hard place" yang Anda sajikan di sini, adalah dengan pra-alokasikan objek yang mewakili "sukses". Biasanya seseorang juga dapat melakukan pra-alokasi objek untuk kasus kegagalan umum. Maka hanya dalam kasus yang jarang melewati detail tambahan, adalah objek baru yang dibuat. (Ini setara dengan OO dari "kode kesalahan" bilangan bulat, ditambah panggilan terpisah untuk mendapatkan detail kesalahan terakhir - teknik yang telah ada selama beberapa dekade.)
ToolmakerSteve

@ Fuwjax Jadi melempar pengecualian tidak membuat objek dengan akun Anda? Tidak yakin saya mengerti alasan itu. Apakah Anda melempar pengecualian atau mengembalikan objek hasil, Anda membuat objek. Dalam hal ini, objek hasil tidak lebih lambat daripada melempar pengecualian.
Matthias

20

Saya telah memperluas jawaban yang diberikan oleh @Mecki dan @incarnate , tanpa mengisi stacktrace untuk Java.

Dengan Java 7+, kita bisa menggunakan Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace). Tetapi untuk Java6, lihat jawaban saya untuk pertanyaan ini

// This one will regularly throw one
public void method4(int i) throws NoStackTraceThrowable {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceThrowable();
    }
}

// This one will regularly throw one
public void method5(int i) throws NoStackTraceRuntimeException {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceRuntimeException();
    }
}

public static void main(String[] args) {
    int i;
    long l;
    Test t = new Test();

    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method4(i);
        } catch (NoStackTraceThrowable e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method5(i);
        } catch (RuntimeException e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
}

Output dengan Java 1.6.0_45, pada Core i7, 8GB RAM:

method1 took 883 ms, result was 2
method2 took 882 ms, result was 2
method3 took 32270 ms, result was 2 // throws Exception
method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException

Jadi, masih metode yang mengembalikan nilai lebih cepat, dibandingkan dengan metode melempar pengecualian. IMHO, kami tidak dapat mendesain API yang jelas hanya menggunakan tipe pengembalian untuk aliran kesuksesan & kesalahan. Metode yang melempar pengecualian tanpa stacktrace adalah 4-5 kali lebih cepat dari pengecualian normal.

Sunting: NoStackTraceThrowable.java Terima kasih @Greg

public class NoStackTraceThrowable extends Throwable { 
    public NoStackTraceThrowable() { 
        super("my special throwable", null, false, false);
    }
}

menarik, terima kasih. Berikut deklarasi kelas yang hilang:public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
Greg

di memohon Anda menulis With Java 7+, we can usetetapi kemudian Anda menulis Output with Java 1.6.0_45,jadi ini adalah hasil Java 6 atau 7?
WBAR

1
@WBAR dari Java 7, kita hanya perlu menggunakan Throwablekonstruktor yang memiliki boolean writableStackTracearg. Tapi itu tidak ada di Java 6 dan di bawah. Itu sebabnya saya telah memberikan implementasi kustom untuk Java 6 dan di bawah. Jadi kode di atas adalah untuk Java 6 & di bawah ini. Harap baca baris ke-1 para ke-2 dengan seksama.
manikanta

@manikanta "IMHO, kami tidak bisa mendesain API yang jelas hanya menggunakan tipe pengembalian untuk aliran kesuksesan & kesalahan." - kami bisa, jika kami menggunakan Opsional / Hasil / Mungkin seperti banyak bahasa.
Hejazzman

@Hejazzman saya setuju. Tapi Optionalatau serupa datang agak terlambat ke Jawa. Sebelumnya kami juga menggunakan objek wrapper dengan flag sukses / kesalahan. Tapi sepertinya ini sedikit retas dan tidak terasa alami bagi saya.
manikanta

8

Beberapa waktu yang lalu saya menulis sebuah kelas untuk menguji kinerja relatif dari mengubah string ke int menggunakan dua pendekatan: (1) panggil Integer.parseInt () dan tangkap pengecualian, atau (2) cocokkan string dengan regex dan panggil parseInt () hanya jika pertandingan berhasil. Saya menggunakan regex dengan cara paling efisien yang saya bisa (yaitu, membuat objek Pola dan Matcher sebelum intering loop), dan saya tidak mencetak atau menyimpan stacktraces dari pengecualian.

Untuk daftar sepuluh ribu string, jika semuanya adalah angka yang valid, pendekatan parseInt () empat kali lebih cepat dari pendekatan regex. Tetapi jika hanya 80% dari string yang valid, regex dua kali lebih cepat dari parseInt (). Dan jika 20% valid, artinya pengecualian dilemparkan dan ditangkap 80% dari waktu, regex sekitar dua puluh kali lebih cepat dari parseInt ().

Saya terkejut dengan hasilnya, mengingat bahwa pendekatan regex memproses string yang valid dua kali: sekali untuk pertandingan dan sekali lagi untuk parseInt (). Tapi melempar dan menangkap pengecualian lebih dari dibuat untuk itu. Situasi semacam ini tidak mungkin terjadi sangat sering di dunia nyata, tetapi jika itu terjadi, Anda pasti tidak harus menggunakan teknik penangkapan pengecualian. Tetapi jika Anda hanya memvalidasi input pengguna atau sesuatu seperti itu, tentu saja gunakan pendekatan parseInt ().


JVM mana yang Anda gunakan? apakah masih lambat dengan sun-jdk 6?
Benedikt Waldvogel

Saya menggali dan menjalankannya lagi di bawah JDK 1.6u10 sebelum mengirimkan jawaban itu, dan itulah hasil yang saya posting.
Alan Moore

Ini sangat, sangat berguna! Terima kasih. Untuk kasus penggunaan saya yang biasa, saya perlu mem-parsing input pengguna (menggunakan sesuatu seperti Integer.ParseInt()) dan saya berharap bahwa sebagian besar kali input pengguna akan benar, jadi untuk kasus penggunaan saya sepertinya mengambil pengecualian sesekali hit adalah cara untuk pergi .
markvgti

8

Saya pikir artikel pertama merujuk pada tindakan melintasi tumpukan panggilan dan membuat jejak tumpukan sebagai bagian yang mahal, dan sementara artikel kedua tidak mengatakannya, saya pikir itu adalah bagian paling mahal dari pembuatan objek. John Rose memiliki artikel di mana ia menjelaskan berbagai teknik untuk mempercepat pengecualian . (Preallocating dan menggunakan kembali pengecualian, pengecualian tanpa jejak tumpukan, dll)

Tapi tetap saja - saya pikir ini harus dianggap hanya kejahatan yang diperlukan, pilihan terakhir. Alasan John untuk melakukan ini adalah untuk meniru fitur-fitur dalam bahasa lain yang belum tersedia di JVM. Anda TIDAK boleh membiasakan diri menggunakan pengecualian untuk aliran kontrol. Terutama bukan karena alasan kinerja! Seperti yang Anda sebutkan di # 2, Anda berisiko menutupi bug serius dalam kode Anda dengan cara ini, dan akan lebih sulit untuk mempertahankannya untuk programmer baru.

Microbenchmark di Jawa secara mengejutkan sulit untuk mendapatkan yang benar (saya telah diberitahu), terutama ketika Anda masuk ke wilayah JIT, jadi saya benar-benar ragu bahwa menggunakan pengecualian lebih cepat daripada "kembali" dalam kehidupan nyata. Sebagai contoh, saya menduga Anda memiliki antara 2 dan 5 frame stack dalam pengujian Anda? Sekarang bayangkan kode Anda akan dipanggil oleh komponen JSF yang digunakan oleh JBoss. Sekarang Anda mungkin memiliki jejak tumpukan yang panjangnya beberapa halaman.

Mungkin Anda dapat memposting kode pengujian Anda?


7

Tidak tahu apakah topik ini berhubungan, tapi saya pernah ingin menerapkan satu trik dengan mengandalkan jejak tumpukan thread saat ini: Saya ingin menemukan nama metode ini, yang memicu instantiasi di dalam kelas instantiated (ya, idenya gila, Saya benar-benar menyerah). Jadi saya menemukan bahwa panggilan Thread.currentThread().getStackTrace()adalah sangat lambat (karena asli dumpThreadsmetode yang digunakan secara internal).

Jadi Java Throwable, dengan demikian, memiliki metode asli fillInStackTrace. Saya berpikir bahwa catchblok pembunuh yang dijelaskan sebelumnya entah bagaimana memicu eksekusi metode ini.

Tapi izinkan saya menceritakan kisah lain kepada Anda ...

Dalam Scala beberapa fitur fungsional dikompilasi menggunakan JVM ControlThrowable, yang memperluas Throwabledan menimpanya dengan fillInStackTracecara berikut:

override def fillInStackTrace(): Throwable = this

Jadi saya mengadaptasi tes di atas (jumlah siklus berkurang sepuluh, mesin saya sedikit lebih lambat :):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

Jadi, hasilnya adalah:

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

Anda lihat, satu-satunya perbedaan antara method3dan method4adalah bahwa mereka membuang berbagai jenis pengecualian. Ya, method4masih lebih lambat daripada method1dan method2, tetapi perbedaannya jauh lebih dapat diterima.


6

Saya telah melakukan beberapa pengujian kinerja dengan JVM 1.5 dan menggunakan pengecualian setidaknya 2x lebih lambat. Rata-rata: Waktu eksekusi pada metode kecil yang sepele lebih dari tiga kali lipat (3x) dengan pengecualian. Sebuah loop kecil yang sepele yang harus menangkap pengecualian melihat peningkatan 2x dalam waktu.

Saya telah melihat angka yang sama dalam kode produksi serta tolok ukur mikro.

Pengecualian harus TIDAK digunakan untuk apa pun yang sering disebut. Melemparkan ribuan pengecualian per detik akan menyebabkan leher botol besar.

Misalnya, menggunakan "Integer.ParseInt (...)" untuk menemukan semua nilai buruk dalam file teks yang sangat besar - ide yang sangat buruk. (Saya telah melihat metode utilitas ini mematikan kinerja pada kode produksi)

Menggunakan pengecualian untuk melaporkan nilai buruk pada formulir GUI pengguna, mungkin tidak terlalu buruk dari sudut pandang kinerja.

Baik itu praktik desain yang baik, saya akan mengikuti aturan: jika kesalahannya normal / diharapkan, maka gunakan nilai balik. Jika tidak normal, gunakan pengecualian. Misalnya: membaca input pengguna, nilai buruk adalah normal - gunakan kode kesalahan. Melewati nilai ke fungsi utilitas internal, nilai buruk harus difilter dengan memanggil kode - gunakan pengecualian.


Izinkan saya menyarankan beberapa hal yang baik untuk dilakukan: Jika Anda memerlukan angka dalam formulir, alih-alih menggunakan Integer.valueOf (String), Anda harus mempertimbangkan menggunakan pencocokan ekspresi reguler. Anda dapat mengkompilasi dan menggunakan kembali pola sehingga membuat korek api menjadi murah. Namun pada formulir GUI, memiliki isValid / validate / checkField atau apa yang sudah Anda mungkin lebih jelas. Juga, dengan Java 8 kami memiliki monad Opsional, jadi pertimbangkan untuk menggunakannya. (jawabannya adalah 9 tahun, tetapi tetap!: p)
Haakon Løtveit

4

Kinerja pengecualian di Java dan C # meninggalkan banyak hal yang diinginkan.

Sebagai programmer ini memaksa kita untuk hidup dengan aturan "pengecualian harus disebabkan jarang", hanya karena alasan kinerja praktis.

Namun, sebagai ilmuwan komputer, kita harus memberontak terhadap kondisi bermasalah ini. Orang yang menulis suatu fungsi seringkali tidak tahu seberapa sering akan dipanggil, atau apakah sukses atau gagal lebih mungkin. Hanya penelepon yang memiliki informasi ini. Mencoba untuk menghindari pengecualian mengarah pada idom API yang tidak jelas di mana dalam beberapa kasus kami hanya memiliki versi pengecualian yang bersih tapi lambat, dan dalam kasus lain kami memiliki kesalahan nilai pengembalian yang cepat tapi kikuk, dan dalam kasus lain kami berakhir dengan keduanya . Pelaksana perpustakaan mungkin harus menulis dan memelihara dua versi API, dan penelepon harus memutuskan mana dari dua versi yang akan digunakan dalam setiap situasi.

Ini agak berantakan. Jika pengecualian memiliki kinerja yang lebih baik, kita dapat menghindari idiom-idiom yang kikuk ini dan menggunakan pengecualian karena itu dimaksudkan untuk digunakan ... sebagai fasilitas pengembalian kesalahan yang terstruktur.

Saya benar-benar ingin melihat mekanisme pengecualian diimplementasikan menggunakan teknik yang lebih dekat ke nilai-kembali, sehingga kami dapat memiliki kinerja yang lebih dekat untuk mengembalikan nilai .. karena ini adalah apa yang kami kembalikan ke dalam kode sensitif kinerja.

Berikut ini adalah contoh kode yang membandingkan kinerja pengecualian dengan kinerja nilai pengembalian kesalahan.

TestIt kelas publik {

int value;


public int getValue() {
    return value;
}

public void reset() {
    value = 0;
}

public boolean baseline_null(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        return shouldfail;
    } else {
        return baseline_null(shouldfail,recurse_depth-1);
    }
}

public boolean retval_error(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            return false;
        } else {
            return true;
        }
    } else {
        boolean nested_error = retval_error(shouldfail,recurse_depth-1);
        if (nested_error) {
            return true;
        } else {
            return false;
        }
    }
}

public void exception_error(boolean shouldfail, int recurse_depth) throws Exception {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            throw new Exception();
        }
    } else {
        exception_error(shouldfail,recurse_depth-1);
    }

}

public static void main(String[] args) {
    int i;
    long l;
    TestIt t = new TestIt();
    int failures;

    int ITERATION_COUNT = 100000000;


    // (0) baseline null workload
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                t.baseline_null(shoulderror,recurse_depth);
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("baseline: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }


    // (1) retval_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                if (!t.retval_error(shoulderror,recurse_depth)) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("retval_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }

    // (2) exception_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                try {
                    t.exception_error(shoulderror,recurse_depth);
                } catch (Exception e) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("exception_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);              
        }
    }
}

}

Dan inilah hasilnya:

baseline: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 683 ms
baseline: recurse_depth 2, exception_freqeuncy 0.25 (0), time elapsed 790 ms
baseline: recurse_depth 2, exception_freqeuncy 0.5 (0), time elapsed 768 ms
baseline: recurse_depth 2, exception_freqeuncy 0.75 (0), time elapsed 749 ms
baseline: recurse_depth 2, exception_freqeuncy 1.0 (0), time elapsed 731 ms
baseline: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 923 ms
baseline: recurse_depth 5, exception_freqeuncy 0.25 (0), time elapsed 971 ms
baseline: recurse_depth 5, exception_freqeuncy 0.5 (0), time elapsed 982 ms
baseline: recurse_depth 5, exception_freqeuncy 0.75 (0), time elapsed 947 ms
baseline: recurse_depth 5, exception_freqeuncy 1.0 (0), time elapsed 937 ms
baseline: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1154 ms
baseline: recurse_depth 8, exception_freqeuncy 0.25 (0), time elapsed 1149 ms
baseline: recurse_depth 8, exception_freqeuncy 0.5 (0), time elapsed 1133 ms
baseline: recurse_depth 8, exception_freqeuncy 0.75 (0), time elapsed 1117 ms
baseline: recurse_depth 8, exception_freqeuncy 1.0 (0), time elapsed 1116 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 742 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 743 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 734 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 723 ms
retval_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 728 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 920 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 1121   ms
retval_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 1037 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 1141   ms
retval_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 1130 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1218 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 1334  ms
retval_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 1478 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 1637 ms
retval_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 1655 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 726 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 17487   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 33763   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 67367   ms
exception_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 66990 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 924 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 23775  ms
exception_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 46326 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 91707 ms
exception_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 91580 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1144 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 30440 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 59116   ms
exception_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 116678 ms
exception_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 116477 ms

Memeriksa dan menyebarkan kembali nilai tidak menambahkan beberapa biaya vs panggilan baseline-nol, dan biaya yang proporsional dengan kedalaman panggilan. Pada kedalaman rantai panggilan 8, versi pengecekan nilai-kembali-kesalahan sekitar 27% lebih lambat dari versi dasar yang tidak memeriksa nilai-nilai kembali.

Kinerja pengecualian, sebagai perbandingan, bukan fungsi dari kedalaman panggilan, tetapi dari frekuensi pengecualian. Namun, penurunan seiring meningkatnya frekuensi pengecualian jauh lebih dramatis. Pada frekuensi kesalahan hanya 25%, kode berjalan 24-TIMES lebih lambat. Pada frekuensi kesalahan 100%, versi pengecualian hampir 100 kali lebih lambat.

Ini menunjukkan kepada saya bahwa mungkin membuat tradeoff yang salah dalam implementasi pengecualian kami. Pengecualian bisa lebih cepat, baik dengan menghindari jalan setapak yang mahal, atau dengan langsung mengubahnya menjadi penyusun yang didukung pemeriksaan nilai pengembalian. Sampai mereka melakukannya, kita terjebak menghindarinya ketika kita ingin kode kita berjalan cepat.


3

HotSpot cukup mampu menghapus kode pengecualian untuk pengecualian yang dihasilkan sistem, asalkan semua itu sebaris. Namun, pengecualian yang dibuat secara eksplisit dan yang tidak dihapus menghabiskan banyak waktu membuat jejak stack. Ganti fillInStackTraceuntuk melihat bagaimana ini dapat memengaruhi kinerja.


2

Bahkan jika melempar pengecualian tidak lambat, itu masih ide yang buruk untuk membuang pengecualian untuk aliran program normal. Digunakan dengan cara ini analog dengan GOTO ...

Saya kira itu tidak benar-benar menjawab pertanyaan itu. Saya akan membayangkan bahwa kebijaksanaan 'konvensional' untuk melempar pengecualian menjadi lambat adalah benar dalam versi java sebelumnya (<1.4). Membuat pengecualian mengharuskan VM membuat seluruh jejak stack. Banyak yang telah berubah sejak saat itu di VM untuk mempercepat dan ini mungkin salah satu area yang telah ditingkatkan.


1
Akan lebih baik untuk mendefinisikan "aliran program normal". Banyak yang telah ditulis tentang menggunakan pengecualian yang diperiksa sebagai kegagalan proses bisnis dan pengecualian yang tidak diperiksa untuk kegagalan yang tidak dapat dipulihkan, sehingga dalam arti tertentu, kegagalan dalam logika bisnis masih dapat dianggap sebagai aliran normal.
Spencer Kormos

2
@Spencer K: Pengecualian, seperti namanya, berarti bahwa situasi luar biasa ditemukan (file hilang, jaringan tiba-tiba ditutup, ...). Ini menyiratkan bahwa situasinya tidak diharapkan. Jika DIHARAPKAN bahwa situasi akan terjadi, saya tidak akan menggunakan pengecualian untuk itu.
Mecki

2
@Mecki: benar. Saya baru-baru ini berdiskusi dengan seseorang tentang ini ... Mereka menulis kerangka kerja Validasi dan melemparkan pengecualian jika terjadi kegagalan validasi. Saya pikir ini adalah ide yang buruk karena ini akan sangat umum. Saya lebih suka melihat metode mengembalikan ValidationResult.
user38051

2
Dalam hal aliran kontrol, pengecualian analog dengan breakatau return, bukan a goto.
Hot Licks

3
Ada banyak paradigma pemrograman. Tidak mungkin ada satu "aliran normal", apa pun yang Anda maksud dengan itu. Pada dasarnya, mekanisme pengecualian hanyalah cara untuk dengan cepat meninggalkan frame saat ini dan melepaskan tumpukan hingga titik tertentu. Kata "pengecualian" tidak menyiratkan apa pun tentang sifat "tak terduga". Contoh cepat: sangat alami untuk "membuang" 404 dari aplikasi web ketika keadaan tertentu terjadi di sepanjang rute. Mengapa logika itu tidak diterapkan dengan pengecualian? Apa anti-polanya?
Menjelma

2

Bandingkan saja katakanlah Integer.parseInt dengan metode berikut ini, yang hanya mengembalikan nilai default dalam kasus data yang tidak dapat diparsing alih-alih melemparkan Pengecualian:

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

Selama Anda menerapkan kedua metode untuk data "valid", keduanya akan bekerja pada laju yang kira-kira sama (meskipun Integer.parseInt mengelola untuk menangani data yang lebih kompleks). Tetapi begitu Anda mencoba mem-parsing data yang tidak valid (mis. Untuk mem-parsing "abc" 1.000.000 kali), perbedaan kinerja haruslah penting.


2

Pos bagus tentang kinerja pengecualian adalah:

https://shipilev.net/blog/2014/exceptional-performance/

Instantiating vs menggunakan kembali yang ada, dengan jejak stack dan tanpa, dll:

Benchmark                            Mode   Samples         Mean   Mean error  Units

dynamicException                     avgt        25     1901.196       14.572  ns/op
dynamicException_NoStack             avgt        25       67.029        0.212  ns/op
dynamicException_NoStack_UsedData    avgt        25       68.952        0.441  ns/op
dynamicException_NoStack_UsedStack   avgt        25      137.329        1.039  ns/op
dynamicException_UsedData            avgt        25     1900.770        9.359  ns/op
dynamicException_UsedStack           avgt        25    20033.658      118.600  ns/op

plain                                avgt        25        1.259        0.002  ns/op
staticException                      avgt        25        1.510        0.001  ns/op
staticException_NoStack              avgt        25        1.514        0.003  ns/op
staticException_NoStack_UsedData     avgt        25        4.185        0.015  ns/op
staticException_NoStack_UsedStack    avgt        25       19.110        0.051  ns/op
staticException_UsedData             avgt        25        4.159        0.007  ns/op
staticException_UsedStack            avgt        25       25.144        0.186  ns/op

Bergantung pada kedalaman jejak tumpukan:

Benchmark        Mode   Samples         Mean   Mean error  Units

exception_0000   avgt        25     1959.068       30.783  ns/op
exception_0001   avgt        25     1945.958       12.104  ns/op
exception_0002   avgt        25     2063.575       47.708  ns/op
exception_0004   avgt        25     2211.882       29.417  ns/op
exception_0008   avgt        25     2472.729       57.336  ns/op
exception_0016   avgt        25     2950.847       29.863  ns/op
exception_0032   avgt        25     4416.548       50.340  ns/op
exception_0064   avgt        25     6845.140       40.114  ns/op
exception_0128   avgt        25    11774.758       54.299  ns/op
exception_0256   avgt        25    21617.526      101.379  ns/op
exception_0512   avgt        25    42780.434      144.594  ns/op
exception_1024   avgt        25    82839.358      291.434  ns/op

Untuk detail lainnya (termasuk assembler x64 dari JIT) baca posting blog asli.

Itu berarti Hibernate / Spring / etc-EE-shit lambat karena pengecualian (xD) dan menulis ulang aliran kontrol aplikasi menjauh dari pengecualian (ganti dengan continure/ breakdan mengembalikan booleanbendera seperti dalam C dari panggilan metode) meningkatkan kinerja aplikasi Anda 10x-100x , tergantung pada seberapa sering Anda melemparnya))


0

Saya mengubah @Mecki jawaban di atas untuk memiliki method1 mengembalikan boolean dan cek dalam metode panggilan, karena Anda tidak bisa hanya mengganti Pengecualian dengan apa pun. Setelah dua kali berjalan, method1 masih merupakan yang tercepat atau secepat metode2.

Ini adalah snapshot dari kode tersebut:

// Calculates without exception
public boolean method1(int i) {
    value = ((value + i) / i) << 1;
    // Will never be true
    return ((i & 0xFFFFFFF) == 1000000000);

}
....
   for (i = 1; i < 100000000; i++) {
            if (t.method1(i)) {
                System.out.println("Will never be true!");
            }
    }

dan hasil:

Jalankan 1

method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2

Jalankan 2

method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2

0

Sebuah pengecualian dimaksudkan untuk menangani kondisi yang tidak terduga pada saat dijalankan saja.

Menggunakan pengecualian menggantikan validasi sederhana yang dapat dilakukan dalam waktu kompilasi akan menunda validasi hingga waktu eksekusi. Ini pada gilirannya akan mengurangi efisiensi program.

Melempar pengecualian alih-alih menggunakan validasi if..else yang sederhana juga akan membuat kode menjadi kompleks untuk ditulis dan dipelihara.


-3

Pendapat saya tentang kecepatan Exception versus mengecek data secara terprogram.

Banyak kelas memiliki String to value converter (pemindai / pengurai), perpustakaan yang dihormati dan juga terkenal;)

biasanya memiliki bentuk

class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}

nama pengecualian hanya contoh, biasanya tidak dicentang (runtime), jadi melempar deklarasi hanya gambar saya

terkadang ada bentuk kedua:

public static Example Parse(String input, Example defaultValue)

tidak pernah melempar

Ketika insur kedua tidak tersedia (atau programmer membaca terlalu sedikit dokumen dan hanya menggunakan yang pertama), tulis kode seperti itu dengan ekspresi reguler. Ekspresi reguler itu keren, benar secara politis, dll:

Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
  Example v = Example.Parse(src);
}

dengan kode ini, pemrogram tidak memiliki biaya pengecualian. TAPI TELAH sebanding biaya yang sangat tinggi untuk ekspresi reguler SELALU versus biaya pengecualian kecil kadang-kadang.

Saya menggunakan hampir selalu dalam konteks seperti itu

try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}

tanpa menganalisis stacktrace dll, saya percaya setelah kuliah Anda cukup cepat.

Jangan Takut Pengecualian


-5

Mengapa pengecualian harus lebih lambat dari pengembalian normal?

Selama Anda tidak mencetak stacktrace ke terminal, menyimpannya ke dalam file atau sesuatu yang serupa, blok-catch tidak bekerja lebih dari blok-kode lainnya. Jadi, saya tidak bisa membayangkan mengapa "melempar my_cool_error ()" yang baru harus lambat.

Pertanyaan bagus dan saya menantikan informasi lebih lanjut tentang topik ini!


17
Pengecualian harus menangkap informasi tentang jejak stack, bahkan jika itu tidak benar-benar digunakan.
Jon Skeet
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.