Khai báo nhiều mảng có 64 phần tử nhanh hơn 1000 lần so với khai báo mảng 65 phần tử


91

Gần đây, tôi nhận thấy việc khai báo một mảng chứa 64 phần tử nhanh hơn rất nhiều (> 1000 lần) so với việc khai báo cùng một kiểu mảng có 65 phần tử.

Đây là mã tôi đã sử dụng để kiểm tra điều này:

public class Tests{
    public static void main(String args[]){
        double start = System.nanoTime();
        int job = 100000000;//100 million
        for(int i = 0; i < job; i++){
            double[] test = new double[64];
        }
        double end = System.nanoTime();
        System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
    }
}

Quá trình này chạy trong khoảng 6 mili giây, nếu tôi thay thế new double[64]bằng new double[65]thì mất khoảng 7 giây. Vấn đề này trở nên nghiêm trọng hơn theo cấp số nhân nếu công việc được trải rộng trên ngày càng nhiều chủ đề, đó là nguyên nhân bắt nguồn của vấn đề của tôi.

Sự cố này cũng xảy ra với các loại mảng khác nhau như int[65]hoặc String[65]. Sự cố này không xảy ra với chuỗi lớn:, String test = "many characters";nhưng bắt đầu xảy ra khi điều này được thay đổi thànhString test = i + "";

Tôi đã tự hỏi tại sao lại như vậy và nếu có thể giải quyết vấn đề này.


3
Lưu ý: System.nanoTime()nên được ưu tiên hơn System.currentTimeMillis()cho điểm chuẩn.
rocketboy

4
Tôi chỉ tò mò? Bạn đang sử dụng Linux? Hành vi có thay đổi với hệ điều hành không?
bsd

9
Làm thế quái nào mà câu hỏi này lại nhận được một Downvote ??
Rohit Jain

2
FWIW, tôi thấy sự khác biệt về hiệu suất tương tự nếu tôi chạy mã này với bytethay vì double.
Oliver Charlesworth

3
@ThomasJungblut: Vậy điều gì giải thích sự khác biệt trong thí nghiệm của OP?
Oliver Charlesworth

Câu trả lời:


88

Bạn đang quan sát một hành vi gây ra bởi các tối ưu hóa được thực hiện bởi trình biên dịch JIT của máy ảo Java của bạn. Hành vi này có thể tái tạo được kích hoạt với mảng vô hướng lên đến 64 phần tử và không được kích hoạt với mảng lớn hơn 64.

Trước khi đi vào chi tiết, chúng ta hãy xem xét kỹ hơn phần thân của vòng lặp:

double[] test = new double[64];

Cơ thể không có tác dụng (hành vi quan sát được) . Điều đó có nghĩa là nó không tạo ra sự khác biệt nào bên ngoài việc thực thi chương trình cho dù câu lệnh này có được thực thi hay không. Điều này cũng đúng cho toàn bộ vòng lặp. Vì vậy, nó có thể xảy ra, trình tối ưu hóa mã dịch vòng lặp thành một cái gì đó (hoặc không có gì) có cùng chức năng và hành vi thời gian khác nhau.

Đối với điểm chuẩn, ít nhất bạn nên tuân thủ hai nguyên tắc sau. Nếu bạn đã làm như vậy, sự khác biệt sẽ nhỏ hơn đáng kể.

  • Khởi động trình biên dịch JIT (và trình tối ưu hóa) bằng cách thực thi điểm chuẩn nhiều lần.
  • Sử dụng kết quả của mọi biểu thức và in nó vào cuối điểm chuẩn.

Bây giờ chúng ta hãy đi vào chi tiết. Không có gì ngạc nhiên khi có một tối ưu hóa được kích hoạt cho các mảng vô hướng không lớn hơn 64 phần tử. Việc tối ưu hóa là một phần của phân tích Escape . Nó đặt các đối tượng nhỏ và mảng nhỏ vào ngăn xếp thay vì phân bổ chúng trên đống - hoặc thậm chí tốt hơn là tối ưu hóa chúng hoàn toàn. Bạn có thể tìm thấy một số thông tin về nó trong bài báo sau của Brian Goetz được viết vào năm 2005:

Việc tối ưu hóa có thể bị vô hiệu hóa bằng tùy chọn dòng lệnh -XX:-DoEscapeAnalysis. Giá trị ma thuật 64 cho mảng vô hướng cũng có thể được thay đổi trên dòng lệnh. Nếu bạn thực hiện chương trình của mình như sau, sẽ không có sự khác biệt giữa các mảng có 64 và 65 phần tử:

java -XX:EliminateAllocationArraySizeLimit=65 Tests

Đã nói rằng, tôi thực sự không khuyến khích sử dụng các tùy chọn dòng lệnh như vậy. Tôi nghi ngờ rằng nó tạo ra sự khác biệt rất lớn trong một ứng dụng thực tế. Tôi sẽ chỉ sử dụng nó, nếu tôi hoàn toàn bị thuyết phục về sự cần thiết - và không dựa trên kết quả của một số điểm chuẩn giả.


9
Nhưng tại sao trình tối ưu hóa phát hiện ra rằng mảng kích thước 64 có thể tháo rời được nhưng không phải là 65
ug_

10
@nosid: Mặc dù mã của OP có thể không thực tế, nhưng rõ ràng nó đang kích hoạt một hành vi thú vị / bất ngờ trong JVM, có thể có ý nghĩa trong các tình huống khác. Tôi nghĩ rằng thật hợp lệ khi hỏi tại sao điều này lại xảy ra.
Oliver Charlesworth

1
@ThomasJungblut Tôi không nghĩ rằng vòng lặp bị loại bỏ. Bạn có thể thêm "int total" bên ngoài vòng lặp và thêm "total + = test [0];" vào ví dụ trên. Sau đó in kết quả, bạn sẽ thấy tổng số đó = 100 triệu và nó sẽ chạy trong vòng chưa đầy một giây.
Sipko

1
Thay thế trên ngăn xếp là thay thế mã được thông dịch bằng được biên dịch nhanh chóng, thay vì thay thế phân bổ đống bằng phân bổ ngăn xếp. EliminateAllocationArraySizeLimit là kích thước giới hạn của các mảng được coi là có thể thay thế vô hướng trong phân tích thoát. Vì vậy, điểm chính mà hiệu quả là do tối ưu hóa trình biên dịch là đúng, nhưng nó không phải là do cấp phát ngăn xếp, mà do giai đoạn phân tích thoát không thông báo cấp phát là không cần thiết.
kiheru

2
@Sipko: Bạn đang viết rằng ứng dụng không mở rộng với số lượng chủ đề. Đó là một dấu hiệu, rằng vấn đề không liên quan đến các tối ưu hóa vi mô mà bạn đang yêu cầu. Tôi khuyên bạn nên nhìn vào bức tranh lớn thay vì những phần nhỏ.
nosid

2

Có bất kỳ cách nào có thể có sự khác biệt, dựa trên kích thước của một đối tượng.

Như nosid đã nêu, JITC có thể (rất có thể là) phân bổ các đối tượng "cục bộ" nhỏ trên ngăn xếp và giới hạn kích thước cho các mảng "nhỏ" có thể là 64 phần tử.

Phân bổ trên ngăn xếp nhanh hơn đáng kể so với phân bổ trong đống và hơn thế nữa, ngăn xếp không cần phải được thu gom rác, do đó chi phí GC được giảm đáng kể. (Và đối với trường hợp thử nghiệm này, chi phí GC có thể là 80-90% tổng thời gian thực thi.)

Hơn nữa, khi giá trị được phân bổ theo ngăn xếp, JITC có thể thực hiện "loại bỏ mã chết", xác định rằng kết quả của giá trị newnày không bao giờ được sử dụng ở bất cứ đâu và sau khi đảm bảo không có tác dụng phụ nào bị mất, hãy loại bỏ toàn bộ newhoạt động, và sau đó là chính vòng lặp (bây giờ trống).

Ngay cả khi JITC không phân bổ ngăn xếp, các đối tượng nhỏ hơn một kích thước nhất định hoàn toàn có thể được phân bổ trong một đống khác nhau (ví dụ: từ một "không gian" khác) với các đối tượng lớn hơn. (Tuy nhiên, thông thường điều này sẽ không tạo ra sự khác biệt về thời gian quá ấn tượng.)


Trễ chủ đề này. Tại sao phân bổ trên ngăn xếp nhanh hơn phân bổ trên đống? Theo một số bài báo, việc phân bổ trên heap mất ~ 12 hướng dẫn. Không có nhiều chỗ để cải thiện.
Vortex

@Vortex - Việc phân bổ cho ngăn xếp cần 1-2 hướng dẫn. Nhưng đó là phân bổ toàn bộ khung ngăn xếp. Dù sao thì khung ngăn xếp cũng phải được cấp phát để có vùng lưu đăng ký cho quy trình, vì vậy bất kỳ biến nào khác được cấp phát cùng lúc đều là "miễn phí". Và như tôi đã nói, ngăn xếp không yêu cầu GC. Chi phí GC cho một mục trong đống lớn hơn nhiều so với chi phí của hoạt động phân bổ theo đống.
Hot Licks
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.