Có tốn kém khi sử dụng các khối thử bắt ngay cả khi một ngoại lệ không bao giờ được ném không?


189

Chúng tôi biết rằng nó là đắt tiền để bắt ngoại lệ. Nhưng, có tốn kém không khi sử dụng khối thử bắt trong Java ngay cả khi một ngoại lệ không bao giờ được ném?

Tôi tìm thấy câu hỏi / câu trả lời Stack Overflow Tại sao các khối thử đắt tiền? , nhưng nó là dành cho .NET .


30
Thực sự không có điểm nào cho câu hỏi này. Hãy thử..catch có một mục đích rất cụ thể. Nếu bạn cần nó, bạn cần nó. Trong mọi trường hợp, điểm nào là thử mà không bắt?
JohnFx

49
try { /* do stuff */ } finally { /* make sure to release resources */ }là hợp pháp và hữu ích
A4L

4
Chi phí đó phải được cân nhắc với lợi ích. Nó không đứng một mình. Trong mọi trường hợp, đắt tiền là tương đối và cho đến khi bạn biết rằng bạn không thể làm điều đó, thì nên sử dụng phương pháp rõ ràng nhất thay vì không làm gì đó vì nó có thể giúp bạn tiết kiệm được một hoặc hai phần nghìn giây trong một giờ Thực hiện chương trình.
Joel

4
Tôi hy vọng điều này không dẫn đến tình huống loại "hãy phát minh lại mã lỗi" ...
mikołak

6
@SAFX: với Java7, bạn thậm chí có thể thoát khỏi finallykhối bằng cách sử dụng atry-with-resources
a_horse_with_no_name

Câu trả lời:


201

tryhầu như không có chi phí nào cả. Thay vì thực hiện công việc thiết lập trythời gian chạy, siêu dữ liệu của mã được cấu trúc vào thời gian biên dịch sao cho khi một ngoại lệ được ném ra, giờ đây nó thực hiện một thao tác tương đối tốn kém khi đi lên ngăn xếp và xem liệu có trykhối nào tồn tại sẽ bắt được điều này không ngoại lệ. Từ quan điểm của một giáo dân, trycũng có thể là miễn phí. Nó thực sự ném ngoại lệ khiến bạn phải trả giá - nhưng trừ khi bạn ném hàng trăm hoặc hàng nghìn ngoại lệ, bạn vẫn sẽ không nhận thấy chi phí.


trycó một số chi phí nhỏ liên quan đến nó. Java không thể thực hiện một số tối ưu hóa mã trong một trykhối mà nó sẽ làm. Ví dụ, Java thường sẽ sắp xếp lại các hướng dẫn trong một phương thức để làm cho nó chạy nhanh hơn - nhưng Java cũng cần đảm bảo rằng nếu một ngoại lệ được ném ra, việc thực thi phương thức được quan sát như thể các câu lệnh của nó, như được viết trong mã nguồn, được thực thi để lên đến một số dòng.

Bởi vì trong một trykhối, một ngoại lệ có thể được ném ra (tại bất kỳ dòng nào trong khối thử! Một số trường hợp ngoại lệ được ném không đồng bộ, chẳng hạn như bằng cách gọi stopvào một Chủ đề (không được chấp nhận), và thậm chí ngoài OutOfMemoryError có thể xảy ra ở hầu hết mọi nơi) có thể bị bắt và mã tiếp tục thực thi sau đó trong cùng một phương thức, khó khăn hơn để lý giải về việc tối ưu hóa có thể được thực hiện, vì vậy chúng ít có khả năng xảy ra. (Ai đó sẽ phải lập trình trình biên dịch để thực hiện chúng, lý do và đảm bảo tính chính xác, v.v. Nó sẽ là một nỗi đau lớn đối với một thứ gì đó có nghĩa là 'đặc biệt')


2
Một số trường hợp ngoại lệ được ném không đồng bộ , chúng không đồng bộ nhưng được ném vào các điểm an toàn. và phần này thử có một số chi phí nhỏ liên quan đến nó. Java không thể thực hiện một số tối ưu hóa mã trong một khối thử mà nếu không nó sẽ cần một tài liệu tham khảo nghiêm túc. Tại một số điểm, mã rất có thể nằm trong khối thử / bắt. Có thể đúng là khối thử / bắt sẽ khó được nội tuyến hơn và xây dựng mạng tinh thể phù hợp cho kết quả nhưng phần w / sắp xếp lại là mơ hồ.
bestsss

2
Có một try...finallykhối mà không catchngăn chặn một số tối ưu hóa?
dajood

5
@Patashu "Thực sự là ném ngoại lệ khiến bạn phải trả giá" Về mặt kỹ thuật, ném ngoại lệ không tốn kém; khởi tạo Exceptionđối tượng là những gì mất phần lớn thời gian.
Austin D

72

Chúng ta hãy đo nó, phải không?

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);
        }
    }
}

Trên máy tính của tôi, cái này in ra một cái gì đó như:

try     0.598 ns
no try  0.601 ns

Ít nhất trong ví dụ tầm thường này, tuyên bố thử không có tác động có thể đo lường được đối với hiệu suất. Hãy đo những cái phức tạp hơn.

Nói chung, tôi khuyên bạn không nên lo lắng về chi phí hiệu năng của các cấu trúc ngôn ngữ cho đến khi bạn có bằng chứng về vấn đề hiệu suất thực tế trong mã của mình. Hay như Donald Knuth nói : "tối ưu hóa sớm là gốc rễ của mọi tội lỗi".


4
trong khi thử / không thử rất có thể giống nhau trên hầu hết các JVM, thì microbenchmark lại rất thiếu sót.
bestsss

2
khá nhiều cấp độ: ý bạn là kết quả được tính dưới 1ns? Mã được biên dịch sẽ loại bỏ cả thử / bắt VÀ vòng lặp hoàn toàn (tổng số từ 1 đến n là tổng tiến trình số học tầm thường). Ngay cả khi mã chứa try / cuối cùng trình biên dịch có thể chứng minh, không có gì được ném vào đó. Mã trừu tượng chỉ có 2 trang gọi và nó sẽ được sao chép và nội tuyến. Có nhiều trường hợp hơn, chỉ cần tra cứu một số bài viết về microbenchmark và bạn quyết định viết một microbenchmark luôn kiểm tra lắp ráp được tạo ra.
bestsss

3
Thời gian báo cáo là mỗi lần lặp của vòng lặp. Vì phép đo sẽ chỉ được sử dụng nếu nó có tổng thời gian trôi qua> 0,1 giây (hoặc 2 tỷ lần lặp, không phải là trường hợp ở đây) Tôi thấy khẳng định của bạn rằng vòng lặp đã bị loại bỏ hoàn toàn khó tin - bởi vì nếu vòng lặp đã được gỡ bỏ, những gì mất 0,1 giây để thực hiện?
meriton

... và thực sự, theo -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, cả vòng lặp và phép cộng đều có trong mã gốc được tạo. Và không, các phương thức trừu tượng không được nội tuyến, bởi vì trình gọi của chúng không chỉ được biên dịch theo thời gian (có lẽ vì nó không được gọi đủ số lần).
meriton

Làm cách nào để tôi viết một điểm chuẩn vi mô chính xác trong Java: stackoverflow.com/questions/504103/ory
Vadzim

46

try/ catchcó thể có một số tác động đến hiệu suất. Điều này là do nó ngăn JVM thực hiện một số tối ưu hóa. Joshua Bloch, trong "Java hiệu quả", đã nói như sau:

• Việc đặt mã bên trong khối thử bắt sẽ ức chế một số tối ưu hóa nhất định mà các triển khai JVM hiện đại có thể thực hiện.


24
"Nó ngăn JVM thực hiện một số tối ưu hóa" ...? Bạn có thể giải thích gì cả?
Kraken

5
@ Mã Kraken bên trong các khối thử (thường là? Luôn luôn?) Không thể được đặt hàng lại với mã bên ngoài các khối thử, như một ví dụ.
Patashu

3
Lưu ý rằng câu hỏi là "nó có đắt không", chứ không phải "nó có ảnh hưởng gì đến hiệu suất không".
mikołak

3
tất nhiên đã thêm một đoạn trích từ Java hiệu quả và đó là kinh thánh của java; trừ khi có một tài liệu tham khảo, đoạn trích không nói lên điều gì. Thực tế, bất kỳ mã nào trong java đều nằm trong thử / cuối cùng ở một mức nào đó.
bestsss

29

Đúng, như những người khác đã nói, một trykhối ức chế một số tối ưu hóa trên các {}nhân vật xung quanh nó. Cụ thể, trình tối ưu hóa phải cho rằng một ngoại lệ có thể xảy ra tại bất kỳ điểm nào trong khối, vì vậy không có gì đảm bảo rằng các câu lệnh được thực thi.

Ví dụ:

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

Nếu không có try, giá trị được tính để gán cho xcó thể được lưu dưới dạng "biểu thức con chung" và được sử dụng lại để gán cho y. Nhưng do trykhông có gì đảm bảo rằng biểu thức đầu tiên đã được đánh giá, nên biểu thức phải được tính toán lại. Đây thường không phải là một vấn đề lớn trong mã "đường thẳng", nhưng có thể có ý nghĩa trong một vòng lặp.

Tuy nhiên, cần lưu ý rằng điều này chỉ áp dụng cho mã JITCed. javac chỉ thực hiện một lượng tối ưu hóa nhỏ và không có chi phí nào cho trình thông dịch mã byte để nhập / rời một trykhối. (Không có mã byte được tạo để đánh dấu các ranh giới khối.)

Và cho bestsss:

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

Đầu ra:

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

đầu ra 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
}

Không có "GOTO".


Không có mã byte nào được tạo để đánh dấu các ranh giới khối, điều này không nhất thiết - nó yêu cầu GOTO rời khỏi khối, nếu không nó sẽ rơi vào catch/finallykhung.
bestsss

@bestsss - Ngay cả khi GOTO được tạo ra (không phải là nhất định), chi phí của nó rất nhỏ và nó không phải là "điểm đánh dấu" cho ranh giới khối - GOTO có thể được tạo cho nhiều cấu trúc.
Hot Licks

Tôi không bao giờ đề cập đến chi phí, tuy nhiên không có mã byte được tạo ra là một tuyên bố sai. Đó là tất cả. Trên thực tế, không có khối nào trong mã byte, khung không có khối bằng nhau.
bestsss

Sẽ không có GOTO nếu lần thử rơi trực tiếp vào cuối cùng, và có những kịch bản khác sẽ không có GOTO. Vấn đề là không có gì theo thứ tự "nhập thử" / "thoát thử".
Hot Licks

Sẽ không có GOTO nếu lần thử rơi trực tiếp vào cuối cùng - Sai! không có finallymã byte, nó try/catch(Throwable any){...; throw any;}có câu lệnh bắt với khung và có thể ném mà PHẢI được định nghĩa (không null), v.v. Tại sao bạn cố gắng tranh luận về chủ đề này, bạn có thể kiểm tra ít nhất một số mã byte? Các hướng dẫn hiện tại cho impl. cuối cùng là sao chép các khối và tránh phần goto (impl trước đó) nhưng mã byte phải được sao chép tùy thuộc vào số lượng điểm thoát.
bestsss

8

Để hiểu tại sao tối ưu hóa không thể được thực hiện, rất hữu ích để hiểu các cơ chế cơ bản. Ví dụ ngắn gọn nhất mà tôi có thể tìm thấy đã được triển khai trong các macro C tại: 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)

Trình biên dịch thường gặp khó khăn trong việc xác định xem một bước nhảy có thể được định vị thành X, Y và Z hay không để họ bỏ qua các tối ưu hóa mà họ không thể đảm bảo an toàn, nhưng bản thân việc triển khai khá nhẹ.


4
Các macro C mà bạn đã tìm thấy để thử / bắt không tương đương với Java hoặc với triển khai C #, phát ra 0 hướng dẫn thời gian chạy.
Patashu

Việc triển khai java quá rộng để bao gồm đầy đủ, đây là một triển khai đơn giản hóa nhằm mục đích hiểu ý tưởng cơ bản về cách thực hiện các ngoại lệ. Nói rằng nó phát ra 0 hướng dẫn thời gian chạy là sai lệch. Ví dụ, một classcastexception đơn giản mở rộng runtimeexception, mở rộng ngoại lệ có thể mở rộng có thể ném được bao gồm: grepcode.com/file/reposective.grepcode.com/java/root/jdk/openjdk/, ... Điều đó giống như nói một trường hợp chuyển đổi trong C là miễn phí nếu chỉ có 1 trường hợp được sử dụng, vẫn có một chi phí khởi động nhỏ.
Technosaurus

1
@Patashu Tất cả các bit được biên dịch trước đó vẫn phải được tải khi khởi động cho dù chúng có được sử dụng hay không. Không có cách nào để biết liệu sẽ có ngoại lệ bộ nhớ trong thời gian chạy vào thời gian biên dịch hay không - đó là lý do tại sao chúng được gọi là ngoại lệ thời gian chạy - nếu không chúng sẽ là cảnh báo / lỗi của trình biên dịch, vì vậy không có gì tối ưu hóa mọi thứ , tất cả các mã để xử lý chúng được bao gồm trong mã được biên dịch và có chi phí khởi động.
Technosaurus

2
Tôi không thể nói về C. Trong C # và Java, thử được thực hiện bằng cách thêm siêu dữ liệu, không phải mã. Khi một khối thử được nhập, không có gì được thực thi để chỉ ra điều này - khi ném ngoại lệ, ngăn xếp được giải phóng và siêu dữ liệu được kiểm tra cho các trình xử lý của loại ngoại lệ đó (đắt tiền).
Patashu

1
Đúng, tôi đã thực sự triển khai trình thông dịch Java và trình biên dịch mã byte tĩnh và đã làm việc trên một JITC tiếp theo (đối với IBM iSeries) và tôi có thể nói với bạn rằng không có gì "đánh dấu" mục nhập / thoát của phạm vi thử trong mã byte, nhưng thay vào đó các phạm vi được xác định trong một bảng riêng biệt. Trình thông dịch không có gì đặc biệt cho phạm vi thử (cho đến khi có ngoại lệ được nêu ra). Một JITC (hoặc trình biên dịch mã byte tĩnh) phải nhận thức được các ranh giới để triệt tiêu tối ưu hóa như đã nêu trước đây.
Hot Licks

8

Một microbenchmark khác ( nguồn ).

Tôi đã tạo một thử nghiệm trong đó tôi đo phiên bản mã thử bắt và không thử bắt dựa trên tỷ lệ phần trăm ngoại lệ. 10% phần trăm có nghĩa là 10% các trường hợp thử nghiệm được chia cho các trường hợp bằng không. Trong một tình huống, nó được xử lý bởi một khối thử bắt, trong trường hợp khác bởi một toán tử có điều kiện. Đây là bảng kết quả của tôi:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
Tỷ lệ phần trăm | Kết quả (thử / nếu, ns)   
    0% | 88/90   
    1% | 89/87    
    10% | 86/97    
    90% | 85/83   

Mà nói rằng không có sự khác biệt đáng kể giữa bất kỳ trường hợp nào.


3

Tôi thấy việc bắt NullPointException khá tốn kém. Đối với các hoạt động 1,2 nghìn, thời gian là 200ms và 12ms khi tôi xử lý nó theo cùng một cách với if(object==null)đó là sự cải thiện khá lớn đối với tôi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.