Apakah mahal untuk menggunakan blok uji coba bahkan jika pengecualian tidak pernah dilempar?

189

Kita tahu mahal untuk menangkap pengecualian. Tapi, apakah terlalu mahal untuk menggunakan blok try-catch di Jawa bahkan jika pengecualian tidak pernah dilontarkan?

Saya menemukan pertanyaan / jawaban Stack Overflow. Mengapa coba blok mahal? , tetapi untuk .NET .

jsedano
sumber
30
Tidak ada gunanya untuk pertanyaan ini. Coba..catch memiliki tujuan yang sangat spesifik. Jika Anda membutuhkannya, Anda membutuhkannya. Bagaimanapun, apa gunanya mencoba tanpa menangkap?
JohnFx
49
try { /* do stuff */ } finally { /* make sure to release resources */ }legal dan bermanfaat
A4L
4
Biaya itu harus dibandingkan dengan manfaatnya. Itu tidak berdiri sendiri. Bagaimanapun, mahal itu relatif, dan sampai Anda tahu bahwa Anda tidak dapat melakukannya, masuk akal untuk menggunakan metode yang paling jelas daripada tidak melakukan sesuatu karena mungkin menghemat satu atau dua milidetik selama satu jam. pelaksanaan program.
Joel
4
Saya harap ini bukan petunjuk untuk situasi jenis "mari-temukan-ulang-kode" ...
mikołak
6
@SAFX: dengan Java7 Anda bahkan dapat menyingkirkan finallyblok menggunakantry-with-resources
a_horse_with_no_name

Jawaban:

201

tryhampir tidak ada biaya sama sekali. Alih-alih melakukan pekerjaan menyiapkan trysaat runtime, metadata kode ini disusun pada waktu kompilasi sehingga ketika pengecualian dilemparkan, sekarang melakukan operasi yang relatif mahal berjalan menumpuk dan melihat apakah ada tryblok yang akan menangkap ini pengecualian. Dari sudut pandang orang awam, trymungkin juga gratis. Ini sebenarnya membuang pengecualian yang dikenakan biaya - tetapi kecuali Anda melempar ratusan atau ribuan pengecualian, Anda tetap tidak akan menyadari biayanya.


trymemiliki beberapa biaya kecil yang terkait dengannya. Java tidak dapat melakukan beberapa optimasi pada kode di tryblok yang seharusnya dilakukan. Sebagai contoh, Java akan sering mengatur ulang instruksi dalam suatu metode untuk membuatnya berjalan lebih cepat - tetapi Java juga perlu menjamin bahwa jika pengecualian dilemparkan, eksekusi metode diamati seolah-olah pernyataannya, sebagaimana ditulis dalam kode sumber, dieksekusi untuk beberapa baris.

Karena dalam tryblok pengecualian dapat dilempar (pada baris apa pun di blok coba! Beberapa pengecualian dilemparkan secara tidak serempak, seperti dengan memanggil stopThread (yang sudah usang), dan bahkan selain itu OutOfMemoryError dapat terjadi hampir di mana saja) dan belum dapat ditangkap dan kode terus dieksekusi setelah itu dalam metode yang sama, lebih sulit untuk alasan tentang optimasi yang dapat dilakukan, sehingga mereka cenderung terjadi. (Seseorang harus memprogram kompiler untuk melakukannya, alasan dan menjamin kebenaran, dll. Ini akan sangat menyusahkan untuk sesuatu yang berarti 'luar biasa') Tapi sekali lagi, dalam praktiknya Anda tidak akan melihat hal-hal seperti ini.

Patashu
sumber
2
Beberapa pengecualian dilemparkan secara tidak sinkron , mereka bukan asinkron tetapi dilemparkan ke dalam titik aman. dan bagian ini coba memiliki beberapa biaya kecil yang terkait dengannya. Java tidak dapat melakukan beberapa optimasi pada kode dalam blok percobaan yang seharusnya dilakukan memang memerlukan referensi serius. Pada titik tertentu, kode tersebut kemungkinan besar berada dalam blok coba / tangkap. Mungkin benar bahwa blok coba / tangkap akan lebih sulit untuk digarisbawahi dan membangun kisi yang tepat untuk hasilnya tetapi bagian dengan pengaturan ulang ambigu.
bestsss
2
Apakah try...finallyblok tanpa catchjuga mencegah beberapa optimasi?
dajood
5
@ Patashu "Ini benar-benar melempar pengecualian yang membebani biaya Anda" Secara teknis, melempar pengecualian tidak mahal; instantiate Exceptionobjek adalah apa yang paling banyak memakan waktu.
Austin D
72

Mari kita ukur, ya?

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        try {
                            x += i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    return x;
                }
            }, new Benchmark("no try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += i;
                    }
                    return x;
                }
            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

Di komputer saya, ini mencetak sesuatu seperti:

try     0.598 ns
no try  0.601 ns

Setidaknya dalam contoh sepele ini, pernyataan coba tidak memiliki dampak yang terukur pada kinerja. Jangan ragu untuk mengukur yang lebih kompleks.

Secara umum, saya sarankan untuk tidak khawatir tentang biaya kinerja konstruksi bahasa sampai Anda memiliki bukti masalah kinerja aktual dalam kode Anda. Atau seperti Donald Knuth menempatkan itu: "optimasi prematur adalah akar dari segala kejahatan".

meriton
sumber
4
sementara try / no try sangat mungkin sama pada kebanyakan JVM, microbenchmark sangat cacat.
bestsss
2
beberapa level: maksud Anda hasilnya dihitung di bawah 1ns? Kode yang dikompilasi akan menghapus try / catch DAN loop secara bersamaan (menjumlahkan angka dari 1 menjadi n adalah jumlah perkembangan aritmatika yang sepele). Bahkan jika kodenya mengandung try / akhirnya kompiler dapat membuktikan, tidak ada yang harus dilemparkan ke sana. Kode abstrak hanya memiliki 2 situs panggilan dan akan diklon dan inline. Ada lebih banyak kasus, lihat saja beberapa artikel di microbenchmark dan Anda memutuskan untuk menulis microbenchmark selalu periksa perakitan yang dihasilkan.
bestsss
3
Waktu yang dilaporkan adalah per iterasi dari loop. Sebagai pengukuran hanya akan digunakan jika memiliki total waktu yang berlalu> 0,1 detik (atau 2 miliar iterasi, yang tidak terjadi di sini) Saya menemukan pernyataan Anda bahwa loop telah dihapus secara keseluruhan sulit dipercaya - karena jika loop telah dihapus, apa yang membutuhkan 0,1 detik untuk dieksekusi?
meriton
... dan memang, menurut -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, baik loop dan tambahan di dalamnya hadir dalam kode asli yang dihasilkan. Dan tidak, metode abstrak tidak digarisbawahi, karena pemanggil mereka tidak hanya dalam waktu dikompilasi (mungkin, karena itu tidak dipanggil cukup kali).
meriton
Bagaimana cara menulis patokan-mikro yang benar di Jawa: stackoverflow.com/questions/504103/…
Vadzim
47

trySaya catchmungkin memiliki dampak pada kinerja. Ini karena mencegah JVM dari melakukan beberapa optimasi. Joshua Bloch, dalam "Java Efektif," mengatakan yang berikut:

• Menempatkan kode di dalam blok uji coba menghambat optimasi tertentu yang mungkin dilakukan oleh implementasi JVM modern.

Evgeniy Dorofeev
sumber
25
"itu mencegah JVM dari melakukan beberapa optimasi" ...? Bisakah Anda menguraikannya sama sekali?
The Kraken
5
@Kode Kraken di dalam blok uji coba (biasanya? Selalu?) Tidak dapat dipesan ulang dengan kode di luar blok uji coba, sebagai salah satu contoh.
Patashu
3
Perhatikan bahwa pertanyaannya adalah apakah "itu mahal", bukan apakah "itu berdampak pada kinerja".
mikołak
3
menambahkan kutipan dari Java Efektif , dan itu adalah kitab java, tentu saja; kecuali ada referensi, kutipannya tidak memberi tahu apa-apa. Hampir semua kode dalam java berada dalam try / akhirnya pada tingkat tertentu.
bestsss
29

Yap, seperti yang dikatakan orang lain, satu tryblok menghambat beberapa optimisasi di seluruh {}karakter yang mengelilinginya. Secara khusus, pengoptimal harus mengasumsikan bahwa pengecualian dapat terjadi pada titik mana pun dalam blok, sehingga tidak ada jaminan bahwa pernyataan dieksekusi.

Sebagai contoh:

    try {
        int x = a + b * c * d;
        other stuff;
    }
    catch (something) {
        ....
    }
    int y = a + b * c * d;
    use y somehow;

Tanpa itu try, nilai yang dihitung untuk xditugaskan dapat disimpan sebagai "subekspresi umum" dan digunakan kembali untuk ditugaskan y. Tetapi karena trytidak ada jaminan bahwa ekspresi pertama pernah dievaluasi, maka ekspresi tersebut harus dihitung ulang. Ini biasanya bukan masalah besar dalam kode "garis lurus", tetapi bisa signifikan dalam satu lingkaran.

Namun perlu dicatat bahwa ini HANYA berlaku untuk kode JITCed. javac hanya melakukan sejumlah optimasi piddling, dan tidak ada biaya untuk penerjemah bytecode untuk memasuki / meninggalkan tryblok. (Tidak ada bytecode yang dihasilkan untuk menandai batas blok.)

Dan untuk bestsss:

public class TryFinally {
    public static void main(String[] argv) throws Throwable {
        try {
            throw new Throwable();
        }
        finally {
            System.out.println("Finally!");
        }
    }
}

Keluaran:

C:\JavaTools>java TryFinally
Finally!
Exception in thread "main" java.lang.Throwable
        at TryFinally.main(TryFinally.java:4)

output javap:

C:\JavaTools>javap -c TryFinally.class
Compiled from "TryFinally.java"
public class TryFinally {
  public TryFinally();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    Code:
       0: new           #2                  // class java/lang/Throwable
       3: dup
       4: invokespecial #3                  // Method java/lang/Throwable."<init>":()V
       7: athrow
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #5                  // String Finally!
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: aload_1
      18: athrow
    Exception table:
       from    to  target type
           0     9     8   any
}

Tidak ada "GOTO".

Hot Licks
sumber
Tidak ada bytecode yang dihasilkan untuk menandai batas blok ini tidak harus - itu memang mengharuskan GOTO untuk meninggalkan blok, jika tidak maka akan jatuh ke dalam catch/finallybingkai.
bestsss
@bestsss - Sekalipun GOTO dihasilkan (yang bukan merupakan pemberian) biaya yang sangat kecil, dan itu jauh dari menjadi "penanda" untuk batas blok - GOTO dapat dihasilkan untuk banyak konstruksi.
Hot Licks
Saya tidak pernah menyebutkan biaya, namun tidak ada bytecodes yang dihasilkan adalah pernyataan yang salah. Itu saja. Sebenarnya, tidak ada blok dalam bytecode, frame tidak sama dengan blok.
bestsss
Tidak akan ada GOTO jika percobaan jatuh langsung ke akhirnya, dan ada skenario lain di mana tidak akan ada GOTO. Intinya adalah bahwa tidak ada apa pun pada urutan bytecode "enter try" / "exit try".
Hot Licks
Tidak akan ada GOTO jika percobaan langsung jatuh ke akhirnya - Salah! tidak ada finallydalam bytecode, itu try/catch(Throwable any){...; throw any;}Dan Itu memang memiliki pernyataan menangkap w / a frame dan Throwable yang HARUS didefinisikan (bukan nol) dan sebagainya. Mengapa Anda mencoba berdebat tentang topik ini, Anda dapat memeriksa setidaknya beberapa bytecode? Pedoman untuk impl saat ini. dari akhirnya adalah menyalin blok dan menghindari bagian goto (imp sebelumnya) tetapi bytecodes harus disalin tergantung berapa banyak titik keluar yang ada.
bestsss
8

Untuk memahami mengapa optimasi tidak dapat dilakukan, penting untuk memahami mekanisme yang mendasarinya. Contoh paling ringkas yang dapat saya temukan diimplementasikan dalam makro C di: http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

Compiler sering mengalami kesulitan menentukan apakah lompatan dapat dilokalisasi ke X, Y dan Z sehingga mereka melewati optimisasi yang tidak dapat mereka yakini aman, tetapi implementasinya sendiri agak ringan.

technosaurus
sumber
4
Makro C yang Anda temukan untuk mencoba / menangkap ini tidak setara dengan Java atau untuk implementasi C #, yang memancarkan 0 instruksi waktu berjalan.
Patashu
Implementasi java terlalu luas untuk dimasukkan secara penuh, ini adalah implementasi yang disederhanakan untuk tujuan memahami ide dasar tentang bagaimana pengecualian dapat diimplementasikan. Mengatakan itu memancarkan 0 instruksi waktu berjalan adalah menyesatkan. Misalnya classcastexception sederhana memperluas runtimeexception yang memperluas pengecualian yang meluas yang dapat dilemparkan yang melibatkan: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/ ... ... Itu seperti mengatakan switch-case di C adalah gratis jika hanya 1 kasing yang pernah digunakan, masih ada overhead startup kecil.
technosaurus
1
@ Patashu Semua bit yang dikompilasi masih harus dimuat saat startup apakah mereka pernah digunakan atau tidak. Tidak ada cara untuk mengetahui apakah akan ada pengecualian kehabisan memori selama waktu berjalan pada waktu kompilasi - itulah sebabnya mereka disebut run time exception - jika tidak, mereka akan menjadi peringatan / kesalahan penyusun, jadi tidak ada yang tidak mengoptimalkan semuanya. , semua kode untuk menangani mereka termasuk dalam kode yang dikompilasi dan memiliki biaya awal.
technosaurus
2
Saya tidak dapat berbicara tentang C. Dalam C # dan Java, coba diimplementasikan dengan menambahkan metadata, bukan kode. Ketika blok percobaan dimasukkan, tidak ada yang dijalankan untuk menunjukkan ini - ketika pengecualian dilemparkan, tumpukan dilepas dan metadata memeriksa penangan jenis pengecualian (mahal).
Patashu
1
Yap, saya benar-benar telah mengimplementasikan Java interpreter & compiler bytecode statis dan bekerja pada JITC berikutnya (untuk IBM iSeries) dan saya dapat memberitahu Anda tidak ada yang "menandai" masuk / keluarnya rentang coba dalam bytecode, melainkan rentangnya diidentifikasi dalam tabel terpisah. Penerjemah tidak melakukan sesuatu yang khusus untuk rentang coba (sampai pengecualian dinaikkan). JITC (atau compiler bytecode statis) harus mengetahui batasan untuk menekan optimisasi seperti yang dinyatakan sebelumnya.
Hot Licks
8

Namun microbenchmark ( sumber ) lain.

Saya membuat tes di mana saya mengukur versi kode coba-tangkap dan tidak-coba-tangkap berdasarkan persentase pengecualian. Persentase 10% berarti bahwa 10% dari kasus uji dibagi dengan nol kasus. Dalam satu situasi itu ditangani oleh blok try-catch, yang lain oleh operator bersyarat. Ini tabel hasil saya:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
Persentase | Hasil (coba / jika, ns)   
    0% | 88/90   
    1% | 89/87    
    10% | 86/97    
    90% | 85/83   

Yang mengatakan bahwa tidak ada perbedaan yang signifikan antara semua kasus ini.

Andrey Chaschev
sumber
3

Saya menemukan penangkapan NullPointException cukup mahal. Untuk operasi 1.2k waktu adalah 200ms dan 12ms ketika saya mengatasinya dengan cara yang sama dengan if(object==null)yang merupakan peningkatan yang cukup bagi saya.

Mateusz Kaflowski
sumber