Các tác động của các ngoại lệ đối với hiệu năng trong Java là gì?


496

Câu hỏi: Việc xử lý ngoại lệ trong Java có thực sự chậm không?

Sự khôn ngoan thông thường, cũng như rất nhiều kết quả của Google, nói rằng logic đặc biệt không nên được sử dụng cho luồng chương trình thông thường trong Java. Hai lý do thường được đưa ra,

  1. nó thực sự rất chậm - thậm chí một thứ tự cường độ chậm hơn so với mã thông thường (các lý do được đưa ra khác nhau),

  1. nó lộn xộn bởi vì mọi người chỉ mong đợi các lỗi được xử lý trong mã đặc biệt.

Câu hỏi này là về # 1.

Ví dụ, trang này mô tả việc xử lý ngoại lệ Java là "rất chậm" và liên quan đến sự chậm chạp trong việc tạo chuỗi thông báo ngoại lệ - "chuỗi này sau đó được sử dụng để tạo đối tượng ngoại lệ bị ném. Đây không phải là nhanh." Bài viết Xử lý ngoại lệ hiệu quả trong Java nói rằng "lý do cho điều này là do khía cạnh tạo đối tượng của xử lý ngoại lệ, do đó làm cho việc ném ngoại lệ vốn đã chậm". Một lý do khác ngoài kia là thế hệ dấu vết ngăn xếp là những gì làm chậm nó.

Thử nghiệm của tôi (sử dụng Java 1.6.0_07, Java HotSpot 10.0, trên Linux 32 bit), chỉ ra rằng việc xử lý ngoại lệ không chậm hơn mã thông thường. Tôi đã thử chạy một phương thức trong một vòng lặp thực thi một số mã. Khi kết thúc phương thức, tôi sử dụng boolean để cho biết nên quay lại hay ném . Cách này xử lý thực tế là như nhau. Tôi đã thử chạy các phương thức theo các thứ tự khác nhau và tính trung bình thời gian thử nghiệm của mình, nghĩ rằng nó có thể là JVM nóng lên. Trong tất cả các thử nghiệm của tôi, cú ném ít nhất cũng nhanh như quay trở lại, nếu không nhanh hơn (nhanh hơn tới 3,1%). Tôi hoàn toàn cởi mở với khả năng các thử nghiệm của mình sai, nhưng tôi chưa thấy bất cứ điều gì ngoài cách lấy mẫu mã, so sánh thử nghiệm hoặc kết quả trong năm ngoái hoặc hai trong số đó cho thấy việc xử lý ngoại lệ trong Java thực sự là chậm

Điều dẫn tôi xuống con đường này là một API tôi cần để sử dụng ngoại lệ đó như là một phần của logic điều khiển thông thường. Tôi muốn sửa chúng trong cách sử dụng của chúng, nhưng bây giờ tôi có thể không thể. Thay vào đó tôi sẽ phải khen ngợi họ về suy nghĩ về phía trước của họ?

Trong bài viết Xử lý ngoại lệ Java hiệu quả trong quá trình biên dịch đúng lúc , các tác giả đề xuất rằng chỉ có sự hiện diện của các trình xử lý ngoại lệ, ngay cả khi không có ngoại lệ nào được ném, cũng đủ để ngăn trình biên dịch JIT tối ưu hóa mã đúng cách, do đó làm chậm nó . Tôi chưa thử nghiệm lý thuyết này.


8
Tôi biết bạn đã không hỏi về 2), nhưng bạn thực sự nên nhận ra rằng sử dụng ngoại lệ cho luồng chương trình không tốt hơn sử dụng GOTO. Một số người bảo vệ gotos, một số người sẽ bảo vệ những gì bạn đang nói, nhưng nếu bạn hỏi ai đó đã thực hiện và duy trì trong một khoảng thời gian, họ sẽ nói với bạn rằng cả hai đều khó duy trì thực hành thiết kế (và có thể sẽ nguyền rủa tên của người nghĩ rằng họ đủ thông minh để đưa ra quyết định sử dụng chúng).
Bill K

80
Bill, tuyên bố rằng sử dụng ngoại lệ cho luồng chương trình không tốt hơn sử dụng GOTO không tốt hơn tuyên bố rằng sử dụng các điều kiện và vòng lặp cho luồng chương trình không tốt hơn sử dụng GOTO. Đó là một cá trích đỏ. Giải thích cho mình. Các ngoại lệ có thể và được sử dụng hiệu quả cho luồng chương trình trong các ngôn ngữ khác. Ví dụ, mã Python sử dụng các ngoại lệ thường xuyên. Tôi có thể và đã duy trì mã sử dụng các ngoại lệ theo cách này (không phải Java) và tôi không nghĩ rằng có bất kỳ điều gì vốn đã sai với nó.
mmsters

14
@mmopol sử dụng Ngoại lệ cho luồng điều khiển thông thường là một ý tưởng tồi trong Java vì lựa chọn mô hình đã được thực hiện theo cách đó . Đọc Bloch EJ2 - ông nói rõ rằng, trích dẫn, (Mục 57) exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow- đưa ra lời giải thích đầy đủ và rộng rãi về lý do tại sao. Và anh ta là người đã viết Java lib. Do đó, anh ấy là người xác định hợp đồng API của các lớp. / đồng ý Bill K về điều này.

8
@ OndraŽižka Nếu một số khung làm điều này (sử dụng Ngoại lệ trong điều kiện không đặc biệt), nó bị thiếu sót và bị phá vỡ bởi thiết kế, phá vỡ hợp đồng lớp Ngoại lệ của ngôn ngữ. Chỉ vì một số người viết mã tệ hại không làm cho nó bớt tệ hơn.

8
Không ai khác ngoài người tạo ra stackoverflow.com là sai về các ngoại lệ. Nguyên tắc vàng của phát triển phần mềm là không bao giờ làm cho đơn giản và khó sử dụng. Ông viết: "Đúng là chương trình 3 dòng đơn giản thường phát triển thành 48 dòng khi bạn kiểm tra lỗi tốt, nhưng đó là cuộc sống, ..." Đây là một tìm kiếm cho sự thuần khiết, không đơn giản.
sf_jeff

Câu trả lời:


345

Nó phụ thuộc vào cách các ngoại lệ được thực hiện. Cách đơn giản nhất là sử dụng setjmp và longjmp. Điều đó có nghĩa là tất cả các thanh ghi của CPU được ghi vào ngăn xếp (đã mất một thời gian) và có thể một số dữ liệu khác cần được tạo ... tất cả điều này đã xảy ra trong câu lệnh thử. Câu lệnh ném cần phải giải phóng ngăn xếp và khôi phục các giá trị của tất cả các thanh ghi (và các giá trị khác có thể có trong VM). Vì vậy, thử và ném đều chậm như nhau, và điều đó khá chậm, tuy nhiên nếu không có ngoại lệ nào được ném ra, việc thoát khỏi khối thử sẽ không mất thời gian trong hầu hết các trường hợp (vì mọi thứ được đặt trên ngăn xếp sẽ tự động dọn sạch nếu phương thức tồn tại).

Sun và những người khác nhận ra rằng điều này có thể là tối ưu và tất nhiên VM ngày càng nhanh hơn theo thời gian. Có một cách khác để thực hiện các ngoại lệ, đó là tự thử nhanh như chớp (thực tế không có gì xảy ra để thử cả - mọi thứ cần phải xảy ra đã được thực hiện khi lớp được VM tải) và nó khiến việc ném không hoàn toàn chậm như vậy . Tôi không biết JVM nào sử dụng kỹ thuật mới, tốt hơn này ...

... nhưng bạn có đang viết bằng Java để mã của bạn sau này chỉ chạy trên một JVM trên một hệ thống cụ thể không? Vì nếu nó có thể chạy trên bất kỳ nền tảng nào khác hoặc bất kỳ phiên bản JVM nào khác (có thể của bất kỳ nhà cung cấp nào khác), ai nói họ cũng sử dụng triển khai nhanh? Cái nhanh thì phức tạp hơn cái chậm và không dễ dàng có thể có trên tất cả các hệ thống. Bạn muốn ở lại di động? Sau đó, đừng dựa vào ngoại lệ là nhanh.

Nó cũng tạo ra sự khác biệt lớn những gì bạn làm trong một khối thử. Nếu bạn mở một khối thử và không bao giờ gọi bất kỳ phương thức nào trong khối thử này, khối thử sẽ cực nhanh, vì JIT thực sự có thể coi một cú ném như một goto đơn giản. Nó không cần lưu trạng thái ngăn xếp cũng như không cần phải giải phóng ngăn xếp nếu một ngoại lệ được ném ra (nó chỉ cần nhảy đến trình xử lý bắt). Tuy nhiên, đây không phải là điều bạn thường làm. Thông thường bạn mở một khối thử và sau đó gọi một phương thức có thể ném ngoại lệ, phải không? Và ngay cả khi bạn chỉ sử dụng khối thử trong phương thức của mình, loại phương thức này sẽ là gì, không gọi bất kỳ phương thức nào khác? Nó sẽ chỉ tính một con số? Vậy thì bạn cần ngoại lệ để làm gì? Có nhiều cách thanh lịch hơn để điều chỉnh lưu lượng chương trình. Đối với hầu hết mọi thứ khác, nhưng toán học đơn giản,

Xem mã kiểm tra sau:

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

Kết quả:

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

Sự chậm lại từ khối thử là quá nhỏ để loại trừ các yếu tố gây nhiễu như các quá trình nền. Nhưng khối bắt giết tất cả mọi thứ và làm cho nó chậm hơn 66 lần!

Như tôi đã nói, kết quả sẽ không tệ nếu bạn đặt thử / bắt và ném tất cả trong cùng một phương thức (phương thức 3), nhưng đây là một tối ưu hóa JIT đặc biệt mà tôi sẽ không dựa vào. Và ngay cả khi sử dụng tối ưu hóa này, cú ném vẫn khá chậm. Vì vậy, tôi không biết bạn đang cố gắng làm gì ở đây, nhưng chắc chắn có một cách tốt hơn là sử dụng thử / bắt / ném.


7
Câu trả lời tuyệt vời nhưng tôi chỉ muốn thêm rằng theo như tôi biết, System.nanoTime () nên được sử dụng để đo hiệu suất, chứ không phải System.cienTimeMillis ().
Simon Forsberg

10
@ SimonAndréForsberg nanoTime()yêu cầu Java 1.5 và tôi chỉ có sẵn Java 1.4 trên hệ thống tôi đã sử dụng để viết mã ở trên. Ngoài ra, nó không đóng một vai trò lớn trong thực tế. Sự khác biệt duy nhất giữa hai là một nano giây một mili giây khác và nanoTimekhông bị ảnh hưởng bởi các thao tác đồng hồ (không liên quan, trừ khi bạn hoặc quá trình hệ thống sửa đổi đồng hồ hệ thống chính xác thời điểm mã kiểm tra đang chạy). Nói chung, bạn đúng, tất nhiên, nanoTimelà sự lựa chọn tốt hơn.
Mecki

2
Nó thực sự cần lưu ý rằng thử nghiệm của bạn là một trường hợp cực đoan. Bạn hiển thị một cú đánh hiệu suất rất nhỏ cho mã với một trykhối, nhưng không throw. Bài throwkiểm tra của bạn đang ném ngoại lệ 50% thời gian nó đi qua try. Đó rõ ràng là một tình huống mà sự thất bại không phải là ngoại lệ . Cắt giảm xuống chỉ còn 10% ồ ạt cắt giảm hiệu suất. Vấn đề với loại thử nghiệm này là nó khuyến khích mọi người ngừng sử dụng ngoại lệ hoàn toàn. Sử dụng các ngoại lệ, để xử lý trường hợp đặc biệt, thực hiện tốt hơn rất nhiều so với những gì thử nghiệm của bạn cho thấy.
Nate

1
@Nate Trước hết, tôi đã nói rất rõ ràng rằng tất cả điều này phụ thuộc vào cách thực hiện các ngoại lệ. Tôi chỉ đang thử nghiệm MỘT triển khai cụ thể, nhưng có rất nhiều và Oracle có thể chọn một cách hoàn toàn khác với mỗi bản phát hành. Thứ hai, nếu các trường hợp ngoại lệ chỉ là ngoại lệ, thì chúng thường là gì, tất nhiên tác động nhỏ hơn, điều này quá rõ ràng, tôi thực sự không nghĩ rằng người ta phải chỉ ra một cách rõ ràng và do đó tôi không thể hiểu được quan điểm của bạn ở đây. Và thứ ba, ngoại trừ lạm dụng nó xấu, mọi người đều đồng ý về điều đó, vì vậy sử dụng chúng với rất nhiều sự chăm sóc là một điều rất tốt.
Mecki

4
@Glide Một cú ném không giống như một sự sạch sẽ return. Nó để lại một phương thức ở đâu đó ở giữa cơ thể, thậm chí có thể ở giữa một thao tác (cho đến nay chỉ hoàn thành 50%) và catchkhối có thể là 20 khung xếp chồng lên nhau (một phương thức có một trykhối, gọi phương thức1, gọi phương thức 2, gọi mehtod3, ... và trong phương thức 20 ở giữa một thao tác, một ngoại lệ được đưa ra). Ngăn xếp phải được thư giãn 20 khung hình trở lên, tất cả các hoạt động chưa hoàn thành phải được hoàn tác (các hoạt động không được thực hiện một nửa) và các thanh ghi CPU cần phải ở trạng thái sạch. Tất cả điều này tiêu tốn thời gian.
Mecki

255

FYI, tôi đã mở rộng thí nghiệm mà Mecki đã làm:

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 cái đầu giống như Mecki (máy tính xách tay của tôi rõ ràng là chậm hơn).

Phương thức 4 giống hệt với phương thức 3 ngoại trừ việc nó tạo ra new Integer(1)chứ không phải làm throw new Exception().

Phương thức 5 giống như phương thức 3 ngoại trừ việc nó tạo ra new Exception()mà không ném nó.

Phương thức 6 giống như phương thức 3 ngoại trừ việc nó ném một ngoại lệ được tạo trước (một biến thể hiện) thay vì tạo một cái mới.

Trong Java, phần lớn chi phí cho việc ném một ngoại lệ là thời gian dành cho việc thu thập dấu vết ngăn xếp, xảy ra khi đối tượng ngoại lệ được tạo. Chi phí thực tế của việc ném ngoại lệ, trong khi lớn, ít hơn đáng kể so với chi phí tạo ra ngoại lệ.


48
+1 Câu trả lời của bạn giải quyết vấn đề cốt lõi - thời gian để thư giãn và theo dõi ngăn xếp, và thứ hai là ném lỗi. Tôi đã chọn đây là câu trả lời cuối cùng.
Kỹ sư

8
đẹp. ~ 70% tạo ra ngoại lệ, ~ 30% ném nó. thông tin tốt.
chaqke

1
@Basil - Bạn sẽ có thể tìm ra điều đó từ những con số trên.
Licks nóng

1
Điều này có thể được thực hiện cụ thể. Phiên bản Java nào đã được sử dụng cho các điểm chuẩn này?
Thorbjørn Ravn Andersen

3
Chúng ta có thể nhận xét rằng trong mã tiêu chuẩn, việc tạo và ném ngoại lệ xảy ra trong các trường hợp hiếm gặp (ý tôi là trong thời gian chạy), nếu không phải là trường hợp, thì điều kiện thời gian chạy rất tệ hoặc chính thiết kế là vấn đề; trong cả hai trường hợp, màn trình diễn không phải là vấn đề đáng lo ngại ...
Jean-Baptiste Yunès

70

Aleksey Shipilëv đã phân tích rất kỹ lưỡng, trong đó ông đánh giá các ngoại lệ Java theo các điều kiện kết hợp khác nhau:

  • Các ngoại lệ mới được tạo so với các ngoại lệ được tạo trước
  • Theo dõi ngăn xếp được kích hoạt vs bị vô hiệu hóa
  • Dấu vết ngăn xếp được yêu cầu vs không bao giờ được yêu cầu
  • Bị bắt ở cấp cao nhất so với rút lui ở mọi cấp so với xích / bọc ở mọi cấp
  • Các mức độ sâu ngăn xếp cuộc gọi Java khác nhau
  • Không tối ưu hóa nội tuyến so với nội tuyến cực đoan so với cài đặt mặc định
  • Các trường do người dùng xác định đọc và không đọc

Ông cũng so sánh chúng với hiệu suất kiểm tra mã lỗi ở các mức tần số lỗi khác nhau.

Các kết luận (trích dẫn nguyên văn từ bài viết của mình) là:

  1. Ngoại lệ thực sự đặc biệt là hiệu suất đẹp. Nếu bạn sử dụng chúng như được thiết kế và chỉ truyền đạt các trường hợp thực sự đặc biệt trong số lượng lớn các trường hợp không đặc biệt được xử lý bằng mã thông thường, thì sử dụng ngoại lệ là chiến thắng về hiệu suất.

  2. Chi phí hiệu năng của các ngoại lệ có hai thành phần chính: xây dựng dấu vết ngăn xếp khi Ngoại lệ được khởi tạo và ngăn xếp ngăn xếp trong quá trình ném Ngoại lệ.

  3. Chi phí xây dựng theo dõi ngăn xếp tỷ lệ thuận với độ sâu của ngăn xếp tại thời điểm khởi tạo ngoại lệ. Điều đó thật tệ vì ai trên Trái đất biết độ sâu ngăn xếp mà phương thức ném này sẽ được gọi là gì? Ngay cả khi bạn tắt thế hệ theo dõi ngăn xếp và / hoặc lưu trữ các ngoại lệ, bạn chỉ có thể thoát khỏi phần chi phí hiệu năng này.

  4. Chi phí giải phóng ngăn xếp phụ thuộc vào mức độ may mắn của chúng tôi với việc đưa trình xử lý ngoại lệ đến gần hơn trong mã được biên dịch. Cấu trúc mã cẩn thận để tránh việc xử lý ngoại lệ sâu, có thể giúp chúng ta gặp may mắn hơn.

  5. Nếu chúng ta loại bỏ cả hai hiệu ứng, chi phí hiệu suất của các ngoại lệ là của chi nhánh địa phương. Cho dù âm thanh của nó có đẹp đến thế nào, điều đó không có nghĩa là bạn nên sử dụng Ngoại lệ như luồng điều khiển thông thường, bởi vì trong trường hợp đó, bạn phải chịu trách nhiệm tối ưu hóa trình biên dịch! Bạn chỉ nên sử dụng chúng trong các trường hợp thực sự đặc biệt, trong đó tần số ngoại lệ sẽ khấu hao chi phí không may có thể xảy ra khi tăng ngoại lệ thực tế.

  6. Quy tắc ngón tay cái lạc quan dường như là tần số 10 ^ -4 cho các trường hợp ngoại lệ là đủ đặc biệt. Điều đó, tất nhiên, phụ thuộc vào trọng lượng nặng của chính các ngoại lệ, các hành động chính xác được thực hiện trong xử lý ngoại lệ, v.v.

Kết quả cuối cùng là khi một ngoại lệ không bị ném, bạn không phải trả chi phí, vì vậy khi điều kiện ngoại lệ đủ xử lý ngoại lệ hiếm thì nhanh hơn so với sử dụng ifmỗi lần. Các bài viết đầy đủ là rất nhiều giá trị đọc.


41

Thật không may, câu trả lời của tôi chỉ là quá dài để đăng ở đây. Vì vậy, hãy để tôi tóm tắt ở đây và giới thiệu bạn đến http://www.fuwjax.com/how-slow-are-java-exceptions/ để biết chi tiết nghiệt ngã.

Câu hỏi thực sự ở đây không phải là "Làm thế nào chậm 'các báo cáo thất bại được coi là ngoại lệ' so với 'mã không bao giờ thất bại'?" như phản ứng được chấp nhận có thể khiến bạn tin tưởng. Thay vào đó, câu hỏi nên là "sự thất bại được báo cáo là ngoại lệ" chậm như thế nào so với thất bại được báo cáo theo những cách khác? " Nói chung, hai cách báo cáo lỗi khác là bằng các giá trị sentinel hoặc với các hàm bao kết quả.

Giá trị Sentinel là một nỗ lực để trả về một lớp trong trường hợp thành công và lớp khác trong trường hợp thất bại. Bạn có thể nghĩ về nó gần như trả lại một ngoại lệ thay vì ném một cái. Điều này đòi hỏi một lớp cha mẹ được chia sẻ với đối tượng thành công và sau đó thực hiện kiểm tra "thể hiện" và một cặp diễn viên để có được thông tin thành công hay thất bại.

Nó chỉ ra rằng có nguy cơ về an toàn loại, các giá trị Sentinel nhanh hơn các ngoại lệ, nhưng chỉ bằng một hệ số khoảng 2 lần. Bây giờ, điều đó có vẻ như rất nhiều, nhưng gấp 2 lần chỉ bao gồm chi phí chênh lệch thực hiện. Trong thực tế, hệ số này thấp hơn nhiều vì các phương thức của chúng tôi có thể thất bại thú vị hơn nhiều so với một vài toán tử số học như trong mã mẫu ở nơi khác trong trang này.

Mặt khác, Wrappers kết quả, không hy sinh loại an toàn nào cả. Họ bao bọc thông tin thành công và thất bại trong một lớp duy nhất. Vì vậy, thay vì "instanceof", họ cung cấp "isSuccess ()" và getters cho cả các đối tượng thành công và thất bại. Tuy nhiên, các đối tượng kết quả chậm hơn khoảng 2 lần so với sử dụng ngoại lệ. Nó chỉ ra rằng việc tạo ra một đối tượng trình bao bọc mới mỗi lần tốn kém hơn nhiều so với việc ném một ngoại lệ đôi khi.

Trên hết, các ngoại lệ là ngôn ngữ được cung cấp theo cách chỉ ra rằng một phương thức có thể thất bại. Không có cách nào khác để chỉ từ API, phương thức nào được dự kiến ​​sẽ luôn luôn (hầu hết) hoạt động và dự kiến ​​sẽ báo cáo thất bại.

Các ngoại lệ an toàn hơn so với sentinels, nhanh hơn các đối tượng kết quả và ít gây ngạc nhiên hơn. Tôi không đề xuất rằng thử / bắt thay thế nếu / khác, nhưng ngoại lệ là cách đúng để báo cáo thất bại, ngay cả trong logic nghiệp vụ.

Điều đó nói rằng, tôi muốn chỉ ra rằng hai cách thường xuyên nhất ảnh hưởng đến hiệu suất mà tôi đã chạy qua là tạo ra các đối tượng không cần thiết và các vòng lặp lồng nhau. Nếu bạn có lựa chọn giữa việc tạo ngoại lệ hoặc không tạo ngoại lệ, đừng tạo ngoại lệ. Nếu đôi khi bạn có lựa chọn giữa việc tạo một ngoại lệ hoặc tạo một đối tượng khác, thì hãy tạo ngoại lệ đó.


5
Tôi quyết định kiểm tra hiệu suất dài hạn của ba triển khai so với triển khai kiểm soát để kiểm tra lỗi mà không báo cáo. Quá trình có tỷ lệ thất bại khoảng 4%. Lặp lại một bài kiểm tra gọi quy trình 10000 lần so với một trong các chiến lược. Mỗi chiến lược được kiểm tra 1000 lần và 900 lần cuối cùng được sử dụng để tạo số liệu thống kê. Dưới đây là thời gian trung bình tính bằng nano: Kiểm soát 338 Ngoại lệ 429 Kết quả 348 Sentinel 345
Fuwjax

2
Để giải trí, tôi đã vô hiệu hóa fillInStackTrace trong bài kiểm tra ngoại lệ. Đây là thời điểm hiện tại: Kiểm soát 347 Ngoại lệ 351 Kết quả 364 Sentinel 355
Fuwjax

Fuwjax, trừ khi tôi thiếu một cái gì đó (và tôi thừa nhận tôi chỉ đọc bài viết SO của bạn, không phải bài đăng trên blog của bạn), có vẻ như hai bình luận của bạn ở trên mâu thuẫn với bài đăng của bạn. Tôi đoán số thấp hơn là tốt hơn trong điểm chuẩn của bạn, phải không? Trong trường hợp đó, tạo ngoại lệ với fillInStackTrace được bật (đó là hành vi mặc định và thông thường), dẫn đến hiệu suất chậm hơn so với hai kỹ thuật khác mà bạn mô tả. Tôi đang thiếu một cái gì đó, hoặc bạn thực sự bình luận để từ chối bài viết của bạn?
Felix GV

@Fuwjax - cách để tránh sự lựa chọn "đá và nơi khó khăn" mà bạn trình bày ở đây, là phân bổ trước một đối tượng đại diện cho "thành công". Thông thường người ta cũng có thể phân bổ trước các đối tượng cho các trường hợp thất bại phổ biến. Sau đó, chỉ trong trường hợp hiếm hoi chuyển lại chi tiết bổ sung, là một đối tượng mới được tạo. (Đây là tương đương OO của "mã lỗi" số nguyên, cộng với một lệnh gọi riêng để nhận thông tin chi tiết về lỗi cuối cùng - một kỹ thuật đã tồn tại trong nhiều thập kỷ.)
ToolmakerSteve

@Fuwjax Vì vậy, ném một ngoại lệ không tạo ra một đối tượng bằng tài khoản của bạn? Không chắc tôi hiểu lý do đó. Cho dù bạn ném một ngoại lệ hoặc trả về một đối tượng kết quả, bạn đang tạo các đối tượng. Theo nghĩa đó, các đối tượng kết quả không chậm hơn ném một ngoại lệ.
Matthias

20

Tôi đã mở rộng các câu trả lời được đưa ra bởi @Mecki@incarnate , mà không cần điền stacktrace cho Java.

Với Java 7+, chúng ta có thể sử dụng Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace). Nhưng đối với Java6, hãy xem câu trả lời của tôi cho câu hỏi này

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

Đầu ra với Java 1.6.0_45, trên Core i7, RAM 8GB:

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

Vì vậy, các phương thức trả về giá trị nhanh hơn so với các phương thức ném ngoại lệ. IMHO, chúng tôi không thể thiết kế API rõ ràng chỉ bằng cách sử dụng các loại trả về cho cả luồng thành công và lỗi. Các phương thức ném ngoại lệ mà không có stacktrace nhanh hơn 4-5 lần so với Ngoại lệ thông thường.

Chỉnh sửa: NoStackTraceThrowable.java Cảm ơn @Greg

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

thú vị, cảm ơn Đây là tuyên bố lớp bị thiếu:public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
Greg

tại beginging Bạn đã viết With Java 7+, we can usenhưng sau đó bạn đã viết Output with Java 1.6.0_45,vậy đây là kết quả Java 6 hay 7?
WBAR

1
@Webar từ Java 7, chúng ta chỉ cần sử dụng hàm Throwabletạo có boolean writableStackTracearg. Nhưng điều đó không có trong Java 6 trở xuống. Đó là lý do tại sao tôi đã đưa ra triển khai tùy chỉnh cho Java 6 trở xuống. Vì vậy, đoạn mã trên dành cho Java 6 & bên dưới. Xin vui lòng đọc dòng 1 của para 2 cẩn thận.
manikanta

@manikanta "IMHO, chúng tôi không thể thiết kế API rõ ràng bằng cách sử dụng các loại trả về cho cả luồng thành công và lỗi." - chúng tôi có thể, nếu chúng tôi sử dụng Tùy chọn / Kết quả / Có thể như nhiều ngôn ngữ đã làm.
Hejazzman

@Hejazzman Tôi đồng ý. Nhưng Optionaltương tự đã đến hơi muộn với Java. Trước đó, chúng tôi cũng đã sử dụng các đối tượng trình bao bọc với các cờ thành công / lỗi. Nhưng nó có vẻ là một chút hack và không cảm thấy tự nhiên đối với tôi.
manikanta

8

Cách đây không lâu, tôi đã viết một lớp để kiểm tra hiệu suất tương đối của việc chuyển đổi chuỗi thành int bằng hai cách tiếp cận: (1) gọi Integer.parseInt () và bắt ngoại lệ, hoặc (2) khớp chuỗi với regex và gọi parseInt () chỉ khi trận đấu thành công. Tôi đã sử dụng regex theo cách hiệu quả nhất có thể (nghĩa là tạo các đối tượng Mẫu và Đối sánh trước khi xen kẽ vòng lặp) và tôi đã không in hoặc lưu stacktraces khỏi các ngoại lệ.

Đối với một danh sách mười nghìn chuỗi, nếu chúng đều là các số hợp lệ, cách tiếp cận parseInt () nhanh gấp bốn lần so với cách tiếp cận regex. Nhưng nếu chỉ có 80% chuỗi là hợp lệ, regex nhanh gấp đôi so với parseInt (). Và nếu 20% là hợp lệ, có nghĩa là ngoại lệ đã bị ném và bị bắt 80% thời gian, regex nhanh gấp khoảng hai mươi lần so với parseInt ().

Tôi đã rất ngạc nhiên với kết quả, khi xem xét rằng cách tiếp cận regex xử lý các chuỗi hợp lệ hai lần: một lần cho trận đấu và một lần nữa cho parseInt (). Nhưng ném và bắt ngoại lệ nhiều hơn bù đắp cho điều đó. Loại tình huống này không có khả năng xảy ra rất thường xuyên trong thế giới thực, nhưng nếu có, bạn chắc chắn không nên sử dụng kỹ thuật bắt ngoại lệ. Nhưng nếu bạn chỉ xác nhận đầu vào của người dùng hoặc một cái gì đó tương tự, bằng mọi cách, hãy sử dụng phương pháp parseInt ().


bạn đã sử dụng JVM nào? vẫn còn chậm với sun-jdk 6 phải không?
Benedikt Waldvogel

Tôi đã đào nó lên và chạy lại nó dưới JDK 1.6u10 trước khi gửi câu trả lời đó, và đó là những kết quả tôi đã đăng.
Alan Moore

Điều này rất, rất hữu ích! Cảm ơn. Đối với các trường hợp sử dụng thông thường của tôi, tôi cần phân tích cú pháp đầu vào của người dùng (sử dụng một cái gì đó như Integer.ParseInt()) và tôi hy vọng rằng hầu hết các lần nhập liệu của người dùng sẽ chính xác, vì vậy đối với trường hợp sử dụng của tôi, có vẻ như việc sử dụng ngoại lệ không thường xuyên là cách để đi .
markvgti

8

Tôi nghĩ rằng bài viết đầu tiên đề cập đến hành động vượt qua ngăn xếp cuộc gọi và tạo ra dấu vết ngăn xếp là phần đắt tiền, và trong khi bài viết thứ hai không nói ra, tôi nghĩ đó là phần tốn kém nhất trong việc tạo đối tượng. John Rose có một bài viết trong đó ông mô tả các kỹ thuật khác nhau để tăng tốc độ ngoại lệ . (Phổ biến và sử dụng lại một ngoại lệ, ngoại lệ không có dấu vết ngăn xếp, v.v.)

Nhưng vẫn còn - tôi nghĩ rằng đây chỉ nên được coi là một điều ác cần thiết, là phương sách cuối cùng. Lý do của John để làm điều này là để mô phỏng các tính năng trong các ngôn ngữ khác chưa có (chưa) có sẵn trong JVM. Bạn KHÔNG nên tập thói quen sử dụng ngoại lệ cho luồng điều khiển. Đặc biệt không phải vì lý do hiệu suất! Như bạn đã đề cập ở mục số 2, bạn có nguy cơ che giấu các lỗi nghiêm trọng trong mã của mình theo cách này và sẽ khó duy trì hơn cho các lập trình viên mới.

Microbenchmark trong Java rất khó để hiểu đúng (tôi đã nói), đặc biệt là khi bạn vào lãnh thổ JIT, vì vậy tôi thực sự nghi ngờ rằng sử dụng ngoại lệ nhanh hơn "trở lại" trong cuộc sống thực. Chẳng hạn, tôi nghi ngờ bạn có khoảng 2 đến 5 khung xếp chồng trong bài kiểm tra của mình? Bây giờ hãy tưởng tượng mã của bạn sẽ được gọi bởi một thành phần JSF được JBoss triển khai. Bây giờ bạn có thể có một dấu vết ngăn xếp dài vài trang.

Có lẽ bạn có thể gửi mã kiểm tra của bạn?


7

Không biết các chủ đề này có liên quan hay không, nhưng tôi đã từng muốn thực hiện một mẹo dựa vào dấu vết ngăn xếp của luồng hiện tại: Tôi muốn khám phá tên của phương thức, điều này đã kích hoạt tính năng khởi tạo bên trong lớp tức thời (yeap, ý tưởng thật điên rồ, Tôi hoàn toàn từ bỏ nó). Vì vậy, tôi phát hiện ra rằng việc gọi Thread.currentThread().getStackTrace()cực kỳ chậm (do dumpThreadsphương thức gốc mà nó sử dụng nội bộ).

Vì vậy, Java Throwable, tương ứng, có một phương thức riêng fillInStackTrace. Tôi nghĩ rằng catchkhối kẻ giết người được mô tả trước đó bằng cách nào đó kích hoạt việc thực hiện phương pháp này.

Nhưng hãy để tôi kể cho bạn một câu chuyện khác ...

Trong Scala, một số tính năng chức năng được biên dịch trong JVM bằng cách sử dụng ControlThrowable, nó mở rộng Throwablevà ghi đè lên nó fillInStackTracetheo cách sau:

override def fillInStackTrace(): Throwable = this

Vì vậy, tôi đã điều chỉnh thử nghiệm ở trên (số lượng chu kỳ giảm mười, máy của tôi chậm hơn một chút :):

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)

}

Vì vậy, kết quả là:

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

Bạn thấy đấy, sự khác biệt duy nhất giữa method3method4là họ ném các loại ngoại lệ khác nhau. Yeap, method4vẫn chậm hơn method1method2, nhưng sự khác biệt là dễ chấp nhận hơn nhiều.


6

Tôi đã thực hiện một số thử nghiệm hiệu năng với JVM 1.5 và sử dụng các ngoại lệ chậm hơn ít nhất 2 lần. Trung bình: Thời gian thực hiện trên một phương pháp nhỏ không đáng kể gấp ba lần (3x) với các ngoại lệ. Một vòng lặp nhỏ tầm thường phải bắt ngoại lệ đã thấy thời gian tự tăng lên gấp 2 lần.

Tôi đã thấy những con số tương tự trong mã sản xuất cũng như điểm chuẩn vi mô.

Các ngoại lệ chắc chắn KHÔNG được sử dụng cho bất cứ thứ gì được gọi là thường xuyên. Ném hàng ngàn trường hợp ngoại lệ một giây sẽ gây ra một cổ chai lớn.

Ví dụ: sử dụng "Integer.PudeInt (...)" để tìm tất cả các giá trị xấu trong tệp văn bản rất lớn - ý tưởng rất xấu. (Tôi đã thấy phương thức tiện ích này giết hiệu suất trên mã sản xuất)

Sử dụng một ngoại lệ để báo cáo giá trị xấu trên biểu mẫu GUI của người dùng, có thể không quá tệ từ quan điểm hiệu suất.

Cho dù đó có phải là một thực tiễn thiết kế tốt hay không, tôi sẽ tuân theo quy tắc: nếu lỗi là bình thường / dự kiến, thì hãy sử dụng giá trị trả về. Nếu nó bất thường, sử dụng một ngoại lệ. Ví dụ: đọc đầu vào của người dùng, các giá trị xấu là bình thường - sử dụng mã lỗi. Truyền một giá trị cho một hàm tiện ích nội bộ, các giá trị xấu cần được lọc bằng cách gọi mã - sử dụng một ngoại lệ.


Hãy để tôi đề xuất một số điều cần làm: Nếu bạn cần một số trong một biểu mẫu, thay vì sử dụng Integer.valueOf (Chuỗi), bạn nên xem xét sử dụng một công cụ khớp biểu thức thông thường thay thế. Bạn có thể biên dịch trước và sử dụng lại mô hình để làm cho que diêm rẻ. Tuy nhiên, trên một biểu mẫu GUI, có isValid / validate / checkField hoặc những gì bạn có thể rõ ràng hơn. Ngoài ra, với Java 8, chúng tôi có các đơn nguyên Tùy chọn, vì vậy hãy xem xét sử dụng chúng. (câu trả lời là 9 tuổi, nhưng vẫn vậy !: p)
Haakon Løtveit

4

Hiệu suất ngoại lệ trong Java và C # khiến nhiều người mong muốn.

Khi lập trình viên, điều này buộc chúng ta phải sống theo quy tắc "các trường hợp ngoại lệ nên được gây ra không thường xuyên", đơn giản là vì lý do hiệu suất thực tế.

Tuy nhiên, là các nhà khoa học máy tính, chúng ta nên nổi dậy chống lại tình trạng có vấn đề này. Người tác giả một chức năng thường không biết nó sẽ được gọi thường xuyên như thế nào, hoặc liệu thành công hay thất bại có nhiều khả năng. Chỉ người gọi có thông tin này. Cố gắng tránh các ngoại lệ dẫn đến các thành phần API không rõ ràng trong một số trường hợp chúng tôi chỉ có các phiên bản ngoại lệ sạch nhưng chậm và trong các trường hợp khác, chúng tôi có các lỗi giá trị trả về nhanh nhưng khó hiểu và trong các trường hợp khác, chúng tôi vẫn kết thúc bằng cả hai . Người triển khai thư viện có thể phải viết và duy trì hai phiên bản API và người gọi phải quyết định sử dụng phiên bản nào trong hai phiên bản trong mỗi tình huống.

Đây là một loại hỗn độn. Nếu các ngoại lệ có hiệu suất tốt hơn, chúng ta có thể tránh các thành ngữ vụng về này và sử dụng các ngoại lệ vì chúng được sử dụng ... như một phương tiện trả lại lỗi có cấu trúc.

Tôi thực sự muốn thấy các cơ chế ngoại lệ được triển khai bằng cách sử dụng các kỹ thuật gần hơn với giá trị trả về, vì vậy chúng tôi có thể có hiệu suất gần hơn với giá trị trả về .. vì đây là những gì chúng tôi hoàn nguyên trong mã nhạy cảm hiệu năng.

Dưới đây là một mẫu mã so sánh hiệu suất ngoại lệ với hiệu suất trả về giá trị lỗi.

lớp công khai TestIt {

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

}

Và đây là kết quả:

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

Kiểm tra và tuyên truyền các giá trị trả về sẽ thêm một số chi phí so với cuộc gọi cơ bản-null và chi phí đó tỷ lệ thuận với độ sâu cuộc gọi. Ở độ sâu chuỗi cuộc gọi là 8, phiên bản kiểm tra giá trị trả về lỗi chậm hơn khoảng 27% so với phiên bản cơ sở không kiểm tra giá trị trả về.

Hiệu suất ngoại lệ, so sánh, không phải là một chức năng của độ sâu cuộc gọi, mà là tần số ngoại lệ. Tuy nhiên, sự suy giảm khi tần số ngoại lệ tăng mạnh hơn nhiều. Chỉ với tần suất lỗi 25%, mã chạy chậm hơn 24 LẦN. Ở tần số lỗi 100%, phiên bản ngoại lệ chậm hơn gần 100 LẦN.

Điều này gợi ý cho tôi rằng có lẽ đang tạo ra sự đánh đổi sai lầm trong việc triển khai ngoại lệ của chúng tôi. Các ngoại lệ có thể nhanh hơn, bằng cách tránh đi bộ tốn kém hoặc bằng cách hoàn toàn biến chúng thành trình biên dịch hỗ trợ kiểm tra giá trị trả về. Cho đến khi họ làm điều đó, chúng tôi vẫn cố gắng tránh họ khi chúng tôi muốn mã của mình chạy nhanh.


3

HotSpot hoàn toàn có khả năng loại bỏ mã ngoại lệ cho các ngoại lệ do hệ thống tạo ra, miễn là tất cả được nội tuyến. Tuy nhiên, ngoại lệ được tạo rõ ràng và những trường hợp khác không được loại bỏ sẽ dành nhiều thời gian để tạo theo dõi ngăn xếp. Ghi đè fillInStackTraceđể xem làm thế nào điều này có thể ảnh hưởng đến hiệu suất.


2

Ngay cả khi ném một ngoại lệ không chậm, thì vẫn nên ném ngoại lệ cho luồng chương trình bình thường. Được sử dụng theo cách này tương tự như một GOTO ...

Tôi đoán rằng không thực sự trả lời câu hỏi mặc dù. Tôi tưởng tượng rằng sự khôn ngoan 'thông thường' của việc ném ngoại lệ bị chậm là đúng trong các phiên bản java trước đó (<1.4). Tạo một ngoại lệ yêu cầu VM tạo toàn bộ dấu vết ngăn xếp. Nhiều thứ đã thay đổi kể từ đó trong VM để tăng tốc mọi thứ và đây có thể là một lĩnh vực đã được cải thiện.


1
Sẽ là tốt để định nghĩa "dòng chương trình bình thường". Phần lớn đã được viết về việc sử dụng các ngoại lệ được kiểm tra là một thất bại trong quy trình kinh doanh và một ngoại lệ không được kiểm soát đối với các thất bại không thể phục hồi, do đó, theo một nghĩa nào đó, một thất bại trong logic kinh doanh vẫn có thể được coi là dòng chảy bình thường.
Spencer Kormos

2
@Spencer K: Một ngoại lệ, như tên của nó ngụ ý, có nghĩa là một tình huống đặc biệt đã được phát hiện (một tệp bị mất, một mạng đột nhiên đóng lại, ...). Điều này ngụ ý rằng tình hình đã KHÔNG HỀ. Nếu nó bị MỞ rằng tình huống sẽ xảy ra, tôi sẽ không sử dụng ngoại lệ cho nó.
Mecki

2
@Mecki: đúng rồi. Gần đây tôi đã có một cuộc thảo luận với ai đó về điều này ... Họ đang viết một khung Xác thực và đang đưa ra một ngoại lệ trong trường hợp không xác thực. Tôi nghĩ rằng đây là một ý tưởng tồi vì điều này sẽ khá phổ biến. Tôi muốn thấy phương thức trả về một Xác nhận hợp lệ.
38051

2
Về mặt điều khiển, một ngoại lệ tương tự như a breakhoặc return, không phải là a goto.
Hot Licks

3
Có rất nhiều mô hình lập trình. Không thể có một dòng chảy bình thường duy nhất, bất kể bạn có ý gì. Về cơ bản, cơ chế ngoại lệ chỉ là một cách để nhanh chóng rời khỏi khung hiện tại và thư giãn ngăn xếp cho đến một điểm nhất định. Từ ngoại lệ, có nghĩa là không có gì về bản chất bất ngờ của nó. Một ví dụ nhanh: rất tự nhiên khi ném ném 404 404 từ các ứng dụng web khi một số trường hợp xảy ra dọc theo đường định tuyến. Tại sao logic đó không được thực hiện với các ngoại lệ? Những gì chống mẫu?
hóa thân vào

2

Chỉ cần so sánh giả sử Integer.parseInt với phương thức sau, phương thức này chỉ trả về giá trị mặc định trong trường hợp dữ liệu không thể xem được thay vì ném Ngoại lệ:

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

Miễn là bạn áp dụng cả hai phương pháp cho dữ liệu "hợp lệ", cả hai sẽ hoạt động với tốc độ xấp xỉ nhau (ngay cả khi Integer.parseInt quản lý để xử lý dữ liệu phức tạp hơn). Nhưng ngay khi bạn cố phân tích dữ liệu không hợp lệ (ví dụ: phân tích "abc" 1.000.000 lần), sự khác biệt về hiệu suất là rất cần thiết.


2

Bài đăng tuyệt vời về hiệu suất ngoại lệ là:

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

Khởi tạo và tái sử dụng hiện có, với dấu vết ngăn xếp và không có, v.v .:

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

Tùy thuộc vào độ sâu của dấu vết ngăn xếp:

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

Để biết các chi tiết khác (bao gồm trình biên dịch x64 từ JIT), hãy đọc bài đăng trên blog gốc.

Điều đó có nghĩa là Hibernate / Spring / etc-EE-shit bị chậm do ngoại lệ (xD) và viết lại luồng điều khiển ứng dụng khỏi các ngoại lệ (thay thế bằng continure/ breakvà trả lại các booleancờ như trong C từ lệnh gọi phương thức) cải thiện hiệu suất của ứng dụng của bạn 10 x 100 , tùy thuộc vào tần suất bạn ném chúng))


0

Tôi đã thay đổi câu trả lời của @Mecki ở trên để có phương thức1 trả về boolean và kiểm tra phương thức gọi, vì bạn không thể thay thế Ngoại lệ bằng không có gì. Sau hai lần chạy, phương thức 1 vẫn nhanh nhất hoặc nhanh nhất như phương thức 2.

Dưới đây là ảnh chụp mã:

// 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!");
            }
    }

và kết quả:

Chạy 1

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

Chạy 2

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

0

Một ngoại lệ có nghĩa là để xử lý các điều kiện bất ngờ chỉ trong thời gian chạy .

Sử dụng một ngoại lệ thay cho xác nhận đơn giản có thể được thực hiện trong thời gian biên dịch sẽ trì hoãn xác thực cho đến thời gian thực hiện. Điều này sẽ lần lượt làm giảm hiệu quả của chương trình.

Ném một ngoại lệ thay vì sử dụng một xác nhận if..else đơn giản cũng sẽ làm cho mã phức tạp để viết và duy trì.


-3

Ý kiến ​​của tôi về tốc độ ngoại lệ so với việc kiểm tra dữ liệu theo chương trình.

Nhiều lớp có trình chuyển đổi String thành giá trị (trình quét / trình phân tích cú pháp), các thư viện nổi tiếng và cũng được biết đến;)

thường có hình thức

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

tên ngoại lệ chỉ là ví dụ, thường không được chọn (thời gian chạy), vì vậy ném khai báo chỉ là hình ảnh của tôi

đôi khi tồn tại dạng thứ hai:

public static Example Parse(String input, Example defaultValue)

không bao giờ ném

Khi thứ hai không có sẵn (hoặc lập trình viên đọc quá ít tài liệu và chỉ sử dụng đầu tiên), hãy viết mã đó với biểu thức chính quy. Biểu hiện thông thường là mát mẻ, chính xác, vv:

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

với mã này, các lập trình viên không có chi phí ngoại lệ. NHƯNG CÓ đôi khi chi phí rất cao của biểu thức thông thường LUÔN so với chi phí ngoại lệ nhỏ đôi khi.

Tôi sử dụng hầu như luôn luôn trong bối cảnh như vậy

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

không phân tích stacktrace, v.v., tôi tin sau những bài giảng về bạn khá nhanh.

Đừng sợ Ngoại lệ


-5

Tại sao các ngoại lệ nên chậm hơn so với lợi nhuận bình thường?

Miễn là bạn không in stacktrace đến thiết bị đầu cuối, hãy lưu nó vào một tệp hoặc một cái gì đó tương tự, khối bắt không thực hiện bất kỳ công việc nào nhiều hơn các khối mã khác. Vì vậy, tôi không thể tưởng tượng được tại sao "ném my_cool_error ()" lại chậm như vậy.

Câu hỏi hay và tôi mong được biết thêm thông tin về chủ đề này!


17
Ngoại lệ phải nắm bắt thông tin về dấu vết ngăn xếp, ngay cả khi nó không thực sự được sử dụng.
Jon Skeet
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.