2011-07-06 38 views
14

Tôi đã xem xét một số bộ sưu tập nguyên thủy java (trove, fastutil, hppc) và tôi đã nhận thấy một mẫu mà các biến lớp đôi khi được khai báo là final biến cục bộ. Ví dụ:Có thể truy cập các biến cục bộ cuối cùng nhanh hơn các biến lớp trong Java không?

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    final int[] key = this.key; 
    final int[] value = this.value; 
    for (int i = 0; i < used.length; i++) { 
     if (used[i]) { 
      p.apply(key[i],value[i]); 
     } 
    } 
} 

tôi đã thực hiện một số điểm chuẩn, và có vẻ như nó là hơi nhanh hơn khi làm điều này, nhưng tại sao đây là trường hợp? Tôi đang cố gắng hiểu Java sẽ làm gì khác nếu ba dòng đầu tiên của hàm được nhận xét.

Lưu ý: Điều này có vẻ tương tự như this question, nhưng đó là cho C++ và không giải quyết tại sao chúng được khai báo final.

+1

bạn có thể thử xem xét lắp ráp java được tạo để thấy sự khác biệt. –

+0

chỉ nhận ra rằng lý do có thể trong trình biên dịch HotSpot, không phải mã byte ... –

+0

Vui lòng đăng mã điểm chuẩn của bạn, có ít nhất một số cơ hội bạn đã đánh giá sai phương pháp và thực sự chỉ kiểm tra trình thông dịch chứ không phải trình biên dịch :) – Voo

Trả lời

8

Từ khóa final là một cá trích màu đỏ tại đây. Sự khác biệt về hiệu năng xuất hiện bởi vì họ đang nói hai điều khác nhau.

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    for (int i = 0; i < used.length; i++) { 
    ... 
    } 
} 

đang nói: "lấy một mảng boolean, và cho mỗi phần tử của rằng mảng làm điều gì đó."

Without final boolean[] used, hàm được nói ", trong khi chỉ số này ít hơn độ dài của giá trị hiện tại của used lĩnh vực của đối tượng hiện tại, lấy giá trị hiện tại của used lĩnh vực của đối tượng hiện tại và làm điều gì đó với phần tử tại chỉ mục i. "

JIT có thể dễ dàng hơn trong việc chứng minh các ràng buộc liên kết vòng lặp để loại bỏ các kiểm tra giới hạn thừa và do đó có thể dễ dàng xác định điều gì sẽ gây ra giá trị used để thay đổi. Thậm chí bỏ qua nhiều chủ đề, nếu p.apply có thể thay đổi giá trị của used thì JIT không thể loại bỏ kiểm tra giới hạn hoặc thực hiện các tối ưu hóa hữu ích khác.

+0

Tôi đang bối rối như những gì bạn có nghĩa là bởi 'final' là một cá trích đỏ. Bạn có nghĩa là nó không nhất thiết phải nhanh hơn để truy cập vào biến, nhưng trình biên dịch JIT có thể tối ưu hóa vòng lặp để loại bỏ kiểm tra phạm vi và tra cứu? – job

+0

"Thậm chí bỏ qua nhiều chủ đề" - chỉ để làm rõ điều này: JIT ** chỉ ** xem xét hành vi cục bộ của luồng. Điều này có nghĩa là ngay cả khi được sử dụng là công khai (hoặc có một phương thức setter) và có thể được thay đổi bởi một thread khác thì JIT có mọi quyền bỏ qua điều này. Vì vậy, JIT thực sự chỉ phải tìm ra nếu áp dụng() sẽ thay đổi tham chiếu hay không (trong thực tế: Nếu nó có thể nội tuyến cuộc gọi (và tất cả các subcalls) nó sẽ nhận thấy nó, nếu không bạn may mắn nhất) – Voo

+0

Cũng có một cơ hội tốt là hành vi "nhanh hơn" đến vì ai đó đã viết lại một lần nữa một điểm chuẩn java không hợp lệ (quá dễ dàng để làm điều đó và cách quá khó để làm cho nó đúng) - cần có sự khác biệt về hiệu suất trong trình thông dịch. khá đơn giản, thực sự không có bất kỳ sự khác biệt nào trong mã được biên dịch với Hotspot – Voo

2

nó cho biết thời gian chạy (jit) trong ngữ cảnh của cuộc gọi phương thức đó, 3 giá trị đó sẽ không bao giờ thay đổi, do đó thời gian chạy không cần phải tiếp tục tải các giá trị từ biến thành viên. điều này có thể giúp cải thiện tốc độ nhẹ.

tất nhiên, vì jit trở nên thông minh hơn và có thể tự mình tìm ra những điều này, những quy ước này trở nên ít hữu ích hơn.

lưu ý, tôi không nói rõ rằng việc tăng tốc là nhiều hơn từ việc sử dụng biến cục bộ hơn là phần cuối cùng.

+0

Này, tôi cũng đang gõ cái này! :-) Trừ khi tôi nghĩ rằng ngay cả trình biên dịch có thể được hưởng lợi từ việc biết phương pháp không quan tâm đến những thay đổi song song trong các tài liệu tham khảo này. – Szocske

25

Truy cập biến hoặc tham số cục bộ là thao tác một bước: lấy biến nằm ở độ lệch N trên ngăn xếp. Nếu bạn hoạt động có 2 đối số (giản thể):

  • N = 0 - this
  • N = 1 - lần đầu tiên tranh luận
  • N = 2 - số thứ hai
  • N = 3 - lần đầu tiên biến địa phương
  • N = 4 - biến địa phương thứ hai
  • ...

Vì vậy, khi bạn truy cập biến cục bộ, bạn có quyền truy cập bộ nhớ tại offset cố định (N được biết tại thời gian biên dịch). Đây là bytecode để truy cập vào tranh luận phương pháp đầu tiên (int):

iload 1 //N = 1 

Tuy nhiên khi bạn truy cập vào lĩnh vực, bạn đang thực sự thực hiện thêm một bước. Trước tiên, bạn đang đọc "biến cục bộ" this chỉ để xác định địa chỉ đối tượng hiện tại. Sau đó, bạn đang tải một trường (getfield) trong đó có một khoản bù đắp cố định từ this. Vì vậy, bạn thực hiện hai hoạt động bộ nhớ thay vì một (hoặc một phụ). Bytecode:

aload 0 //N = 0: this reference 
getfield total I //int total 

Vì vậy, kỹ thuật truy cập các biến và tham số cục bộ nhanh hơn trường đối tượng. Trong thực tế, nhiều yếu tố khác có thể ảnh hưởng đến hiệu suất (bao gồm các cấp độ khác nhau của bộ đệm CPU và tối ưu hóa JVM).

final là một câu chuyện khác. Về cơ bản nó là một gợi ý cho trình biên dịch/JIT rằng tham chiếu này sẽ không thay đổi để nó có thể làm cho một số tối ưu hóa nặng hơn. Nhưng điều này khó theo dõi hơn, như quy tắc sử dụng ngón tay cái final bất cứ khi nào có thể.

+5

Tôi cho rằng câu trả lời này (và đặc biệt là đoạn cuối cùng của nó) là tốt hơn câu trả lời được đánh dấu. –

+0

Tôi phải tự hỏi liệu một số tốc độ cuối cùng có thể là một JIT thông minh có thể biết sử dụng lại con trỏ trước khi đối tượng ra khỏi phạm vi và lưu trên alloc(), cộng thêm các lần truy cập bộ nhớ cache tốt hơn. dấu chân ... – Ajax

+0

Hoàn toàn đồng ý. Câu trả lời hữu ích nhất. – omniyo

1

Trong các biến VM được tạo, các biến cục bộ là các mục trên ngăn xếp hạng trong khi các tham chiếu trường phải được di chuyển đến ngăn xếp thông qua một lệnh truy lục giá trị thông qua tham chiếu đối tượng. Tôi tưởng tượng JIT có thể làm cho tài liệu tham khảo ngăn xếp đăng ký tài liệu tham khảo dễ dàng hơn.

+2

Không hoàn toàn đúng. Các biến cục bộ được đặt trên luồng * stack *, không phải trên * toán hạng *. khác nhau 'load' /' store' opcodes được sử dụng để di chuyển các biến địa phương từ stack để operand stack và ngược lại. Xem [hình ảnh này] (http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/fig01.gif). –

0

Tối ưu hóa đơn giản như vậy đã được bao gồm trong thời gian chạy JVM. Nếu JVM truy cập ngây thơ đến các biến mẫu, các ứng dụng Java của chúng ta sẽ là rùa chậm.

Điều chỉnh thủ công như vậy có thể đáng giá đối với các JVM đơn giản, ví dụ: Android.

+0

byexode dex (android) có lẽ thậm chí còn hiệu quả hơn ... .dx không nén là nhỏ hơn so với một lớp. Jar, và toàn bộ lý do cho dalvik trên java di động là hiệu suất (jvm tiêu chuẩn quá bloaty cho thiết bị di động) – Ajax

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