2016-10-20 23 views
15

Mã dưới đây gọi hai hàm đơn giản 10 tỷ lần mỗi đối số.Tại sao truyền hai đối số chuỗi hiệu quả hơn một đối số danh sách

public class PerfTest { 
    private static long l = 0; 

    public static void main(String[] args) { 
     List<String> list = Arrays.asList("a", "b"); 
     long time1 = System.currentTimeMillis(); 
     for (long i = 0; i < 1E10; i++) { 
      func1("a", "b"); 
     } 
     long time2 = System.currentTimeMillis(); 
     for (long i = 0; i < 1E10; i++) { 
      func2(list); 
     } 
     System.out.println((time2 - time1) + "/" + (System.currentTimeMillis() - time2)); 
    } 

    private static void func1(String s1, String s2) { l++; } 
    private static void func2(List<String> sl) { l++; } 
} 

Giả định của tôi là hiệu suất của hai cuộc gọi này sẽ gần giống nhau. Nếu bất cứ điều gì tôi đã có thể đoán rằng đi qua hai đối số sẽ hơi chậm hơn so với đi qua một. Cho tất cả các đối số là tham chiếu đối tượng Tôi đã không mong đợi một thực tế rằng một trong những là một danh sách để làm cho bất kỳ sự khác biệt.

Tôi đã chạy thử nghiệm nhiều lần và kết quả điển hình là "12781/30536". Nói cách khác, cuộc gọi sử dụng hai chuỗi mất 13 giây và cuộc gọi bằng danh sách mất 30 giây.

Giải thích cho sự khác biệt này về hiệu suất là gì? Hay đây là một thử thách không công bằng? Tôi đã cố gắng chuyển đổi hai cuộc gọi (trong trường hợp đó là do hiệu ứng khởi động) nhưng kết quả là như nhau.

Cập nhật

Đây không phải là một thử nghiệm công bằng vì nhiều lý do. Tuy nhiên nó thể hiện hành vi thực sự của trình biên dịch Java. Lưu ý hai bổ sung sau đây để chứng minh điều này:

  • Thêm biểu s1.getClass()sl.getClass() vào các chức năng làm cho hai chức năng cuộc gọi phí phạm cùng
  • Chạy thử nghiệm với -XX:-TieredCompilation cũng làm cho hai chức năng cuộc gọi thực hiện cùng

Giải thích cho hành vi này nằm trong câu trả lời được chấp nhận bên dưới. Bản tóm tắt ngắn gọn về câu trả lời của @ apangin là func2 không được trình biên dịch điểm nóng nêu rõ vì lớp đối số của nó (tức là List) không được giải quyết. Buộc phân giải lớp học (ví dụ: sử dụng getClass) làm cho nội dung được inlined giúp cải thiện đáng kể hiệu suất của lớp. Như đã chỉ ra trong câu trả lời, các lớp chưa được giải quyết có thể không xảy ra trong mã thực mà làm cho mã này trở thành một trường hợp không thực tế.

+0

Bạn có thể thêm những gì bạn mong đợi và tại sao không? – ChiefTwoPencils

+1

@ChiefTwoPencils đã thêm đoạn mã vào đó. – sprinter

+0

Tôi không bỏ phiếu để đóng, nhưng trừ khi ai đó sẵn sàng hack ngoài thời gian chạy để xem xét tối ưu hóa biên dịch cụ thể, hầu hết các câu hỏi hiệu suất không thực sự hữu ích (mặc dù chúng có thể thú vị/thú vị) - và câu trả lời có thể thay đổi từ bản phát hành sang bản phát hành. Trong trường hợp này, tôi chỉ giả định rằng JVM tìm thấy dễ dàng hơn để biên dịch hoặc ghi nhớ hai tham số cuộc gọi hơn là cuộc gọi mảng, nhưng nghiêm túc - chỉ cần viết bất cứ điều gì là dễ đọc nhất! Cũng lưu ý, phiên bản dễ đọc nhất thường là phiên bản mà JVM tối ưu hóa tốt nhất. –

Trả lời

19

Điểm chuẩn là unfair, tuy nhiên, nó đã tiết lộ một hiệu ứng thú vị.

Như Sotirios Delimanolis đã nhận thấy, sự khác biệt hiệu suất là do thực tế là func1 được inline bởi trình biên dịch HotSpot, trong khi func2 thì không. Lý do là đối số func2 của loại List, lớp chưa bao giờ được giải quyết trong khi thực hiện điểm chuẩn.

Lưu ý rằng List lớp không thực sự sử dụng : không có phương pháp gọi là Danh sách, không có lĩnh vực loại List tuyên bố, không có lớp phôi và không có những hành động khác được thực hiện mà thường gây ra lớp resolution. Nếu bạn thêm cách sử dụng List lớp ở bất kỳ đâu trong mã, func2 sẽ được gạch chân.

Cirumstance khác ảnh hưởng đến chiến lược biên dịch là sự đơn giản của phương pháp. Nó đơn giản như vậy mà JVM đã quyết định biên dịch nó trong Tier 1 (C1 không có tối ưu hóa thêm). Nếu nó được biên soạn với C2, List lớp học sẽ được giải quyết. Hãy thử chạy với -XX:-TieredCompilation và bạn sẽ thấy rằng func2 được sắp xếp thành công và hoạt động nhanh như func1.

Viết microbenchmarks thực tế theo cách thủ công là một công việc thực sự khó khăn. Có rất nhiều khía cạnh có thể dẫn đến kết quả khó hiểu, ví dụ: inlining, dead code elimination, on-stack thay thế, ô nhiễm hồ sơ, biên dịch lại vv Đó là lý do tại sao chúng tôi rất khuyến khích sử dụng các công cụ đo điểm chuẩn thích hợp như JMH. Một tiêu chuẩn viết tay có thể dễ dàng đánh lừa JVM. Đặc biệt, các ứng dụng thực sự rất khó có các phương thức với các lớp không bao giờ được sử dụng.

+0

Tôi đã thử thêm mã vào cả hai hàm để đảm bảo 'Danh sách' được giải quyết và cũng đã thử với tùy chọn' TieredCompliation'. Như bạn dự đoán cả hai làm cho 'func2' hoạt động nhanh như' func1'. Cảm ơn lời giải thích rõ ràng. Tôi sẽ cập nhật câu hỏi để làm cho thông tin đó rõ ràng cho người đọc trong tương lai. – sprinter

+0

Kể từ khi đặt câu hỏi tôi đã thực hiện khá nhiều nghiên cứu về microbenchmarks và đã thử JMH cũng như handcoding sau các đề xuất tại http://stackoverflow.com/questions/504103/how-do-i-write-a-correct -micro-benchmark-in-java. Kết luận của tôi sau một vài lần thử là miễn là bạn không muốn độ chính xác và thử nghiệm đã qua một khoảng thời gian đủ dài các bài kiểm tra dựa trên đồng hồ bấm giờ đạt được kết luận giống hệt nhau. Theo ý kiến ​​của bạn là tất cả các angst về xét nghiệm công bằng thực sự hợp lý hoặc là nó pendantry? – sprinter

+0

@sprinter Tôi có ** nhiều ví dụ khi điểm chuẩn dựa trên đồng hồ bấm giờ nguyên thủy tạo ra kết quả gây hiểu nhầm. Đây chỉ là một số ví dụ từ SO: [1] (http://stackoverflow.com/a/24889503/3448419), [2] (http: // stackoverflow.com/a/30745078/3448419), [3] (http://stackoverflow.com/a/38578777/3448419), [4] (http://stackoverflow.com/a/33858960/3448419), [5] (http://stackoverflow.com/a/24344114/3448419), [6] (http://stackoverflow.com/a/33193452/3448419), [7] (http: // stackoverflow. com/a/25242358/3448419), [8] (http://stackoverflow.com/a/39907607/3448419), [9] (http://stackoverflow.com/a/35671374/3448419) – apangin

Các vấn đề liên quan