1. Gambaran keseluruhan
Di Jawa, pengecualian umumnya dianggap mahal dan tidak boleh digunakan untuk pengendalian aliran. Tutorial ini akan membuktikan bahawa persepsi ini betul dan menunjukkan apa yang menyebabkan masalah prestasi.
2. Menetapkan Persekitaran
Sebelum menulis kod untuk menilai kos prestasi, kita perlu menyediakan persekitaran penanda aras.
2.1. Harness Tanda Mikro Java
Mengukur pengecualian overhead tidak semudah melaksanakan kaedah dalam satu gelung sederhana dan mencatat jumlah masa.
Sebabnya ialah penyusun tepat pada masanya dapat menghalangi dan mengoptimumkan kodnya. Pengoptimuman seperti itu dapat menjadikan kod berkinerja lebih baik daripada yang sebenarnya dilakukan di persekitaran produksi. Dengan kata lain, ia mungkin memberikan hasil positif palsu.
Untuk mewujudkan persekitaran terkawal yang dapat mengurangkan pengoptimuman JVM, kami akan menggunakan Java Microbenchmark Harness, atau JMH secara ringkas.
Bahagian-bahagian berikut akan melalui pengaturan persekitaran penanda aras tanpa membahas perincian JMH. Untuk maklumat lebih lanjut mengenai alat ini, sila periksa tutorial Mikrobenanda dengan Java.
2.2. Memperoleh Artefak JMH
Untuk mendapatkan artifak JMH, tambahkan dua kebergantungan ini ke POM:
org.openjdk.jmh jmh-core 1.21 org.openjdk.jmh jmh-generator-annprocess 1.21
Sila rujuk Maven Central untuk versi terbaru JMH Core dan JMH Annotation Processor.
2.3. Kelas Penanda Aras
Kami memerlukan kelas untuk mengadakan penanda aras:
@Fork(1) @Warmup(iterations = 2) @Measurement(iterations = 10) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class ExceptionBenchmark { private static final int LIMIT = 10_000; // benchmarks go here }
Mari kita baca penjelasan JMH seperti di atas:
- @Fork : Menentukan berapa kali JMH mesti menghasilkan proses baru untuk menjalankan penanda aras. Kami menetapkan nilainya menjadi 1 untuk menghasilkan hanya satu proses, mengelakkan menunggu terlalu lama untuk melihat hasilnya
- @Warmup : Menjalankan parameter pemanasan. The lelaran elemen yang 2 cara kedua-dua berjalan pertama diabaikan apabila mengira hasilnya
- @Pengukuran : Membawa parameter pengukuran. An lelaran nilai 10 menunjukkan JMH akan melaksanakan setiap kaedah 10 kali
- @BenchmarkMode : Ini adalah bagaimana JHM harus mengumpulkan hasil pelaksanaan. Nilai AverageTime memerlukan JMH untuk mengira purata masa kaedah yang diperlukan untuk menyelesaikan operasinya
- @OutputTimeUnit : Menunjukkan unit masa output, yang merupakan milisaat dalam kes ini
Selain itu, terdapat medan statik di dalam badan kelas, iaitu TERHAD . Ini adalah bilangan lelaran dalam setiap badan kaedah.
2.4. Melaksanakan Penanda Aras
Untuk melaksanakan penanda aras, kita memerlukan kaedah utama :
public class MappingFrameworksPerformance { public static void main(String[] args) throws Exception { org.openjdk.jmh.Main.main(args); } }
Kita dapat mengemas projek ke dalam fail JAR dan menjalankannya di baris perintah. Melakukannya sekarang tentu saja akan menghasilkan output kosong kerana kita belum menambah kaedah penanda aras.
Untuk kemudahan, kami boleh menambahkan plugin maven-jar ke POM. Plugin ini membolehkan kita menjalankan kaedah utama di dalam IDE:
org.apache.maven.plugins maven-jar-plugin 3.2.0 com.baeldung.performancetests.MappingFrameworksPerformance
Versi terbaru maven-jar-plugin boleh didapati di sini.
3. Pengukuran Prestasi
Sudah tiba masanya untuk mempunyai beberapa kaedah penanda aras untuk mengukur prestasi. Setiap kaedah ini mesti mengandungi penjelasan @Benchmark .
3.1. Kaedah Mengembalikan Biasanya
Mari mulakan dengan kaedah kembali normal; iaitu kaedah yang tidak memberikan pengecualian:
@Benchmark public void doNotThrowException(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { blackhole.consume(new Object()); } }
The blackhole parameter rujukan suatu unsur Blackhole . Ini adalah kelas JMH yang membantu mencegah penghapusan kod mati, pengoptimuman yang mungkin dilakukan penyusun tepat pada masanya.
Penanda aras, dalam kes ini, tidak memberikan pengecualian. Sebenarnya, kami akan menggunakannya sebagai rujukan untuk menilai prestasi mereka yang melakukan pengecualian.
Melaksanakan kaedah utama akan memberi kita laporan:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.doNotThrowException avgt 10 0.049 ± 0.006 ms/op
Tidak ada yang istimewa dalam hasil ini. Masa pelaksanaan purata penanda aras adalah 0.049 milisaat, yang sememangnya tidak bermakna.
3.2. Membuat dan Melemparkan Pengecualian
Inilah tanda aras lain yang melempar dan menangkap pengecualian:
@Benchmark public void throwAndCatchException(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { try { throw new Exception(); } catch (Exception e) { blackhole.consume(e); } } }
Mari lihat outputnya:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.doNotThrowException avgt 10 0.048 ± 0.003 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 17.942 ± 0.846 ms/op
Perubahan kecil dalam masa pelaksanaan kaedah doNotThrowException tidak penting. Ini hanya turun naik dalam keadaan OS yang mendasari dan JVM. Kuncinya adalah bahawa membuang pengecualian menjadikan kaedah berjalan ratusan kali lebih perlahan.
Beberapa bahagian seterusnya akan mengetahui apa sebenarnya yang membawa kepada perbezaan dramatik tersebut.
3.3. Menciptakan Pengecualian Tanpa Melemparkannya
Daripada membuat, membuang, dan menangkap pengecualian, kami hanya akan membuatnya:
@Benchmark public void createExceptionWithoutThrowingIt(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { blackhole.consume(new Exception()); } }
Now, let's execute the three benchmarks we've declared:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 17.601 ± 3.152 ms/op ExceptionBenchmark.doNotThrowException avgt 10 0.054 ± 0.014 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 17.174 ± 0.474 ms/op
The result may come as a surprise: the execution time of the first and the third methods are nearly the same, while that of the second is substantially smaller.
At this point, it's clear that the throw and catch statements themselves are fairly cheap. The creation of exceptions, on the other hand, produces high overheads.
3.4. Throwing an Exception Without Adding the Stack Trace
Let's figure out why constructing an exception is much more expensive than doing an ordinary object:
@Benchmark @Fork(value = 1, jvmArgs = "-XX:-StackTraceInThrowable") public void throwExceptionWithoutAddingStackTrace(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { try { throw new Exception(); } catch (Exception e) { blackhole.consume(e); } } }
The only difference between this method and the one in subsection 3.2 is the jvmArgs element. Its value -XX:-StackTraceInThrowable is a JVM option, keeping the stack trace from being added to the exception.
Let's run the benchmarks again:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 17.874 ± 3.199 ms/op ExceptionBenchmark.doNotThrowException avgt 10 0.046 ± 0.003 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 16.268 ± 0.239 ms/op ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.174 ± 0.014 ms/op
By not populating the exception with the stack trace, we reduced execution duration by more than 100 times. Apparently, walking through the stack and adding its frames to the exception bring about the sluggishness we've seen.
3.5. Throwing an Exception and Unwinding Its Stack Trace
Finally, let's see what happens if we throw an exception and unwind the stack trace when catching it:
@Benchmark public void throwExceptionAndUnwindStackTrace(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { try { throw new Exception(); } catch (Exception e) { blackhole.consume(e.getStackTrace()); } } }
Here's the outcome:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 16.605 ± 0.988 ms/op ExceptionBenchmark.doNotThrowException avgt 10 0.047 ± 0.006 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 16.449 ± 0.304 ms/op ExceptionBenchmark.throwExceptionAndUnwindStackTrace avgt 10 326.560 ± 4.991 ms/op ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.185 ± 0.015 ms/op
Hanya dengan melepaskan jejak tumpukan, kita melihat peningkatan yang besar sekitar 20 kali dalam jangka masa pelaksanaan. Dengan kata lain, prestasinya jauh lebih buruk jika kita mengeluarkan jejak timbunan dari pengecualian selain membuangnya.
4. Kesimpulan
Dalam tutorial ini, kami menganalisis kesan prestasi pengecualian. Secara khusus, ia mendapati bahawa kos prestasi kebanyakannya adalah penambahan jejak tumpukan kecuali. Sekiranya jejak timbunan ini dilepaskan selepas itu, overhead menjadi lebih besar.
Oleh kerana pengecualian membuang dan menangani adalah mahal, kita tidak boleh menggunakannya untuk aliran program biasa. Sebaliknya, seperti namanya, pengecualian hanya boleh digunakan untuk kes-kes yang luar biasa.
Kod sumber lengkap boleh didapati di GitHub.