Sự khác biệt lớn về tốc độ của các phương pháp tĩnh và không tĩnh tương đương


86

Trong đoạn mã này khi tôi tạo một Đối tượng trong mainphương thức và sau đó gọi phương thức đối tượng đó: ff.twentyDivCount(i)(chạy trong 16010 mili giây), nó chạy nhanh hơn nhiều so với việc gọi nó bằng cách sử dụng chú thích này: twentyDivCount(i)(chạy trong 59516 mili giây). Tất nhiên, khi tôi chạy nó mà không tạo một đối tượng, tôi làm cho phương thức tĩnh, vì vậy nó có thể được gọi trong main.

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {    // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way
                       // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

CHỈNH SỬA: Cho đến nay, có vẻ như các máy khác nhau tạo ra các kết quả khác nhau, nhưng sử dụng JRE 1.8. * Là kết quả ban đầu dường như được sao chép nhất quán.


4
Bạn đang chạy điểm chuẩn của mình như thế nào? Tôi cá rằng đây là một hiện vật của JVM không có đủ thời gian để tối ưu hóa mã.
Patrick Collins

2
Có vẻ như đã đủ thời gian để JVM biên dịch và thực hiện OSR cho phương thức chính như +PrintCompilation +PrintInlininghiển thị
Tagir Valeev

1
Tôi đã thử đoạn mã, nhưng tôi không nhận được bất kỳ chênh lệch múi giờ nào như Stabbz đã nói. Chúng 56282ms (sử dụng phiên bản) 54551ms (như phương thức tĩnh).
Don Chakkappan

1
@PatrickCollins Năm giây phải đủ. Tôi đã viết lại một chút để bạn có thể đo lường cả hai (một JVM được bắt đầu cho mỗi biến thể). Tôi biết rằng nó là một điểm chuẩn, nó vẫn còn thiếu sót, nhưng nó đủ thuyết phục: 1457 ms STATIC so với 5312 ms NON_STATIC.
maaartinus

1
Vẫn chưa điều tra chi tiết câu hỏi, nhưng điều này thể liên quan: shipilev.net/blog/2015/black-magic-method-dispatch (có thể Aleksey Shipilëv có thể khai sáng cho chúng tôi ở đây)
Marco 13

Câu trả lời:


72

Sử dụng JRE 1.8.0_45 tôi nhận được kết quả tương tự.

Cuộc điều tra:

  1. chạy java với các -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningtùy chọn VM cho thấy rằng cả hai phương pháp đều được biên dịch và nội tuyến
  2. Nhìn vào bản lắp ráp được tạo cho các phương pháp cho thấy không có sự khác biệt đáng kể
  3. Tuy nhiên, khi chúng được nội tuyến, assembly được tạo bên trong mainsẽ rất khác, với phương thức instance được tối ưu hóa mạnh mẽ hơn, đặc biệt là về khả năng giải nén vòng lặp

Sau đó, tôi đã chạy lại thử nghiệm của bạn nhưng với cài đặt mở vòng lặp khác nhau để xác nhận nghi ngờ ở trên. Tôi đã chạy mã của bạn với:

  • -XX:LoopUnrollLimit=0 và cả hai phương thức đều chạy chậm (tương tự như phương thức tĩnh với các tùy chọn mặc định).
  • -XX:LoopUnrollLimit=100 và cả hai phương thức đều chạy nhanh (tương tự như phương thức instance với các tùy chọn mặc định).

Như một kết luận, có vẻ như, với cài đặt mặc định, JIT của hotspot 1.8.0_45 không thể giải phóng vòng lặp khi phương thức là tĩnh (mặc dù tôi không chắc tại sao nó hoạt động theo cách đó). Các JVM khác có thể mang lại kết quả khác.


Giữa 52 và 71, hành vi ban đầu được khôi phục (ít nhất là trên máy tính của tôi, câu trả lời của tôi). Có vẻ như phiên bản tĩnh lớn hơn 20 đơn vị, nhưng tại sao? Điều này thật kỳ lạ.
maaartinus

3
@maaartinus Tôi thậm chí không chắc con số đó đại diện chính xác cho điều gì - tài liệu khá lảng tránh: " Bỏ cuộn các phần thân của vòng lặp với số nút đại diện trung gian của trình biên dịch máy chủ ít hơn giá trị này. Giới hạn mà trình biên dịch của máy chủ sử dụng là một hàm của giá trị này, không phải giá trị thực . Giá trị mặc định thay đổi tùy theo nền tảng mà JVM đang chạy. "...
assylias

Tôi không biết, nhưng suy đoán đầu tiên của tôi là các phương thức tĩnh sẽ lớn hơn một chút ở bất kỳ đơn vị nào và chúng tôi đã đạt được điểm quan trọng. Tuy nhiên, sự khác biệt là khá lớn, vì vậy dự đoán hiện tại của tôi là phiên bản tĩnh nhận được một số tối ưu hóa làm cho nó lớn hơn một chút. Tôi chưa nhìn vào asm được tạo ra.
maaartinus

33

Chỉ là một phỏng đoán không được chứng minh dựa trên câu trả lời của assylias.

JVM sử dụng ngưỡng để mở vòng lặp, giống như 70. Vì bất kỳ lý do gì, lệnh gọi tĩnh lớn hơn một chút và không bị hủy cuộn.

Cập nhật kết quả

  • Với LoopUnrollLimit52 dưới đây, cả hai phiên bản đều chậm.
  • Từ 52 đến 71, chỉ có phiên bản tĩnh là chậm.
  • Trên 71, cả hai phiên bản đều nhanh.

Điều này thật kỳ lạ khi tôi đoán là lệnh gọi tĩnh chỉ lớn hơn một chút trong biểu diễn bên trong và OP gặp phải một trường hợp kỳ lạ. Nhưng sự khác biệt dường như là khoảng 20, điều này không có ý nghĩa gì.

 

-XX:LoopUnrollLimit=51
5400 ms NON_STATIC
5310 ms STATIC
-XX:LoopUnrollLimit=52
1456 ms NON_STATIC
5305 ms STATIC
-XX:LoopUnrollLimit=71
1459 ms NON_STATIC
5309 ms STATIC
-XX:LoopUnrollLimit=72
1457 ms NON_STATIC
1488 ms STATIC

Đối với những người sẵn sàng thử nghiệm, phiên bản của tôi có thể hữu ích.


Có phải là thời gian '1456 ms' không? Nếu vậy, tại sao bạn nói tĩnh là chậm?
Tony

@Tony Tôi đã nhầm lẫn NON_STATICSTATIC, nhưng kết luận của tôi là đúng. Đã sửa ngay bây giờ, cảm ơn bạn.
maaartinus

0

Khi điều này được thực thi trong chế độ gỡ lỗi, các số giống nhau đối với trường hợp cá thể và trường hợp tĩnh. Điều đó có nghĩa là JIT lưỡng lự trong việc biên dịch mã thành mã gốc trong trường hợp tĩnh giống như cách nó làm trong trường hợp phương thức cá thể.

Tại sao nó làm như vậy? Thật khó để nói; có lẽ nó sẽ làm đúng nếu đây là một ứng dụng lớn hơn ...


"Tại sao nó lại làm như vậy? Thật khó để nói, có lẽ nó sẽ làm đúng nếu đây là một ứng dụng lớn hơn." Hoặc bạn chỉ gặp một vấn đề hiệu suất kỳ lạ quá lớn để thực sự gỡ lỗi. (Và nó không phải là khó để nói Bạn có thể nhìn vào lắp ráp JVM spits ra như assylias đã làm..)
tmyklebu

@tmyklebu Hoặc chúng tôi gặp sự cố hiệu suất kỳ lạ không cần thiết và tốn kém để gỡ lỗi hoàn toàn và có những cách giải quyết dễ dàng. Cuối cùng, chúng ta đang nói về JIT ở đây, các tác giả của nó không biết nó hoạt động chính xác như thế nào trong mọi tình huống. :) Nhìn vào các câu trả lời khác, chúng rất tốt và rất chặt chẽ để giải thích vấn đề, nhưng cho đến nay vẫn không ai biết chính xác tại sao điều này lại xảy ra.
Dragan Bozanovic

@DraganBozanovic: Nó ngừng "không cần thiết phải gỡ lỗi hoàn toàn" khi nó gây ra các vấn đề thực sự trong mã thực.
tmyklebu

0

Tôi chỉ chỉnh sửa bài kiểm tra một chút và tôi nhận được kết quả sau:

Đầu ra:

Dynamic Test:
465585120
232792560
232792560
51350 ms
Static Test:
465585120
232792560
232792560
52062 ms

GHI CHÚ

Trong khi thử nghiệm chúng riêng biệt, tôi nhận được ~ 52 giây đối với động và ~ 200 giây đối với tĩnh.

Đây là chương trình:

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {  // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    static int twentyDivCount2(int a) {
         int count = 0;
         for (int i = 1; i<21; i++) {

             if (a % i == 0) {
                 count++;
             }
         }
         return count;
    }

    public static void main(String[] args) {
        System.out.println("Dynamic Test: " );
        dynamicTest();
        System.out.println("Static Test: " );
        staticTest();
    }

    private static void staticTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        for (int i = start; i > 0; i--) {

            int temp = twentyDivCount2(i);

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }

    private static void dynamicTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

Tôi cũng đã thay đổi thứ tự của bài kiểm tra thành:

public static void main(String[] args) {
    System.out.println("Static Test: " );
    staticTest();
    System.out.println("Dynamic Test: " );
    dynamicTest();
}

Và tôi nhận được điều này:

Static Test:
465585120
232792560
232792560
188945 ms
Dynamic Test:
465585120
232792560
232792560
50106 ms

Như bạn thấy, nếu động được gọi trước khi tĩnh, tốc độ cho tĩnh giảm đáng kể.

Dựa trên điểm chuẩn này:

Tôi giả thuyết rằng tất cả phụ thuộc vào việc tối ưu hóa JVM. do đó, tôi chỉ khuyên bạn nên sử dụng quy tắc ngón tay cái để sử dụng các phương thức tĩnh và động.

QUY TẮC CỦA THUMB:

Java: khi nào sử dụng các phương thức tĩnh


"bạn phải tuân theo quy tắc ngón tay cái để sử dụng các phương pháp tĩnh và động." Quy tắc ngón tay cái này là gì? Và bạn đang trích dẫn từ ai / cái gì?
weston

@weston xin lỗi tôi đã không thêm liên kết mà tôi đang nghĩ đến :). thx
nafas

0

Vui lòng thử:

public class ProblemFive {
    public static ProblemFive PROBLEM_FIVE = new ProblemFive();

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();
        int start = 500000000;
        int result = start;


        for (int i = start; i > 0; i--) {
            int temp = PROBLEM_FIVE.twentyDivCount(i); // faster way
            // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
                System.out.println((System.currentTimeMillis() - startT) + " ms");
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();
        System.out.println((end - startT) + " ms");
    }

    int twentyDivCount(int a) {  // change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i < 21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }
}

20.273 ms để 23000+ ms, khác nhau cho mỗi lần chạy
Stabbz
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.