Có bất kỳ lợi ích hiệu suất theo cách này hay cách khác? Nó có cụ thể là trình biên dịch / VM không? Tôi đang sử dụng Hotspot.
Có bất kỳ lợi ích hiệu suất theo cách này hay cách khác? Nó có cụ thể là trình biên dịch / VM không? Tôi đang sử dụng Hotspot.
Câu trả lời:
Đầu tiên: bạn không nên lựa chọn tĩnh và không tĩnh trên cơ sở hiệu suất.
Thứ hai: trong thực tế, nó sẽ không tạo ra bất kỳ sự khác biệt nào. Hotspot có thể chọn tối ưu hóa theo những cách làm cho các cuộc gọi tĩnh nhanh hơn cho một phương thức, các cuộc gọi không tĩnh nhanh hơn cho một phương thức khác.
Thứ ba: nhiều huyền thoại xung quanh tĩnh và không tĩnh đều dựa trên các JVM rất cũ (không thực hiện được ở bất kỳ đâu gần với sự tối ưu hóa mà Hotspot thực hiện) hoặc một số câu đố đã nhớ về C ++ (trong đó một lệnh gọi động sử dụng thêm một quyền truy cập bộ nhớ hơn một cuộc gọi tĩnh).
Bốn năm sau...
Được rồi, với hy vọng giải quyết câu hỏi này một lần và mãi mãi, tôi đã viết một điểm chuẩn cho thấy các loại lệnh gọi khác nhau (ảo, không ảo, tĩnh) so sánh với nhau như thế nào.
Tôi đã chạy nó trên Ideone , và đây là những gì tôi nhận được:
(Số lần lặp càng lớn thì càng tốt.)
Success time: 3.12 memory: 320576 signal:0
Name | Iterations
VirtualTest | 128009996
NonVirtualTest | 301765679
StaticTest | 352298601
Done.
Như mong đợi, các cuộc gọi phương thức ảo là chậm nhất, các cuộc gọi phương thức không phải ảo nhanh hơn và các cuộc gọi phương thức tĩnh thậm chí còn nhanh hơn.
Điều tôi không ngờ là sự khác biệt rõ rệt như vậy: Các cuộc gọi phương thức ảo được đo để chạy với tốc độ thấp hơn một nửa so với các lệnh gọi phương thức không ảo, lần lượt được đo để chạy chậm hơn 15% so với các lệnh gọi tĩnh. Đó là những gì các phép đo này cho thấy; sự khác biệt thực tế trên thực tế phải rõ ràng hơn một chút, vì đối với mỗi lệnh gọi phương thức ảo, không ảo và tĩnh, mã điểm chuẩn của tôi có thêm một chi phí không đổi bổ sung là tăng một biến số nguyên, kiểm tra một biến boolean và lặp lại nếu không đúng.
Tôi cho rằng kết quả sẽ khác nhau giữa các CPU và từ JVM sang JVM, vì vậy hãy thử và xem bạn nhận được gì:
import java.io.*;
class StaticVsInstanceBenchmark
{
public static void main( String[] args ) throws Exception
{
StaticVsInstanceBenchmark program = new StaticVsInstanceBenchmark();
program.run();
}
static final int DURATION = 1000;
public void run() throws Exception
{
doBenchmark( new VirtualTest( new ClassWithVirtualMethod() ),
new NonVirtualTest( new ClassWithNonVirtualMethod() ),
new StaticTest() );
}
void doBenchmark( Test... tests ) throws Exception
{
System.out.println( " Name | Iterations" );
doBenchmark2( devNull, 1, tests ); //warmup
doBenchmark2( System.out, DURATION, tests );
System.out.println( "Done." );
}
void doBenchmark2( PrintStream printStream, int duration, Test[] tests ) throws Exception
{
for( Test test : tests )
{
long iterations = runTest( duration, test );
printStream.printf( "%15s | %10d\n", test.getClass().getSimpleName(), iterations );
}
}
long runTest( int duration, Test test ) throws Exception
{
test.terminate = false;
test.count = 0;
Thread thread = new Thread( test );
thread.start();
Thread.sleep( duration );
test.terminate = true;
thread.join();
return test.count;
}
static abstract class Test implements Runnable
{
boolean terminate = false;
long count = 0;
}
static class ClassWithStaticStuff
{
static int staticDummy;
static void staticMethod() { staticDummy++; }
}
static class StaticTest extends Test
{
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
ClassWithStaticStuff.staticMethod();
}
}
}
static class ClassWithVirtualMethod implements Runnable
{
int instanceDummy;
@Override public void run() { instanceDummy++; }
}
static class VirtualTest extends Test
{
final Runnable runnable;
VirtualTest( Runnable runnable )
{
this.runnable = runnable;
}
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
runnable.run();
}
}
}
static class ClassWithNonVirtualMethod
{
int instanceDummy;
final void nonVirtualMethod() { instanceDummy++; }
}
static class NonVirtualTest extends Test
{
final ClassWithNonVirtualMethod objectWithNonVirtualMethod;
NonVirtualTest( ClassWithNonVirtualMethod objectWithNonVirtualMethod )
{
this.objectWithNonVirtualMethod = objectWithNonVirtualMethod;
}
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
objectWithNonVirtualMethod.nonVirtualMethod();
}
}
}
static final PrintStream devNull = new PrintStream( new OutputStream()
{
public void write(int b) {}
} );
}
Cần lưu ý rằng sự khác biệt về hiệu suất này chỉ áp dụng cho mã không làm gì khác ngoài việc gọi các phương thức không tham số. Bất kỳ mã nào khác mà bạn có giữa các lệnh gọi sẽ làm loãng sự khác biệt và điều này bao gồm cả việc truyền tham số. Trên thực tế, sự khác biệt 15% giữa các cuộc gọi tĩnh và không ảo có lẽ đã được giải thích đầy đủ bởi thực tế là this
con trỏ không cần phải được chuyển đến phương thức tĩnh. Vì vậy, sẽ chỉ cần một lượng mã khá nhỏ thực hiện những việc vặt vãnh giữa các lệnh gọi để sự khác biệt giữa các loại lệnh gọi khác nhau được pha loãng đến mức không có tác động thực nào.
Ngoài ra, các cuộc gọi phương thức ảo tồn tại vì một lý do; chúng có mục đích phục vụ và chúng được thực hiện bằng cách sử dụng các phương tiện hiệu quả nhất do phần cứng bên dưới cung cấp. (Tập lệnh CPU.) Nếu bạn muốn loại bỏ chúng bằng cách thay thế chúng bằng các lệnh gọi tĩnh hoặc không ảo, cuối cùng bạn phải thêm càng nhiều càng tốt iota mã bổ sung để mô phỏng chức năng của chúng, thì chi phí ròng kết quả của bạn sẽ bị ràng buộc không ít hơn, nhưng nhiều hơn. Rất có thể, nhiều, rất nhiều, không thể tin được nhiều, hơn thế nữa.
VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198
về tốc độ: khi cài đặt OpenJDK của tôi. FTR: Điều đó thậm chí đúng nếu tôi loại bỏ công cụ final
sửa đổi. Btw. Tôi phải ra terminate
sân volatile
, nếu không bài kiểm tra sẽ không kết thúc.
VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170
. Không chỉ vậy OpenJDK trên máy tính xách tay của tôi còn thực hiện nhiều lần lặp hơn 40 lần, thử nghiệm tĩnh luôn có thông lượng ít hơn khoảng 30%. Đây có thể là một hiện tượng ART cụ thể, vì tôi nhận được một kết quả mong đợi trên một máy tính bảng Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Chà, không thể ghi đè các cuộc gọi tĩnh (vì vậy luôn là ứng cử viên cho nội dòng) và không yêu cầu bất kỳ kiểm tra tính rỗng nào. HotSpot thực hiện một loạt các tối ưu hóa thú vị cho các phương thức ví dụ có thể phủ nhận những ưu điểm này, nhưng chúng có thể là lý do tại sao một cuộc gọi tĩnh có thể nhanh hơn.
Tuy nhiên, điều đó sẽ không ảnh hưởng đến thiết kế của bạn - mã theo cách dễ đọc, tự nhiên nhất - và chỉ lo lắng về loại tối ưu hóa vi mô này nếu bạn có lý do (điều mà bạn gần như không bao giờ làm).
Nó là trình biên dịch / VM cụ thể.
Do đó, nó có lẽ không đáng bận tâm trừ khi bạn đã xác định đây là một vấn đề hiệu suất thực sự nghiêm trọng trong ứng dụng của mình. Tối ưu hóa sớm là gốc rễ của mọi điều xấu xa, v.v.
Tuy nhiên, tôi đã thấy sự tối ưu hóa này làm tăng hiệu suất đáng kể trong tình huống sau:
Nếu những điều trên áp dụng cho bạn, nó có thể đáng để thử nghiệm.
Ngoài ra còn có một lý do tốt khác (và thậm chí có khả năng quan trọng hơn!) Để sử dụng phương thức tĩnh - nếu phương thức thực sự có ngữ nghĩa tĩnh (nghĩa là về mặt logic không được kết nối với một phiên bản nhất định của lớp) thì bạn nên làm cho nó tĩnh để phản ánh thực tế này. Các lập trình viên Java có kinh nghiệm sau đó sẽ chú ý đến công cụ sửa đổi tĩnh và ngay lập tức nghĩ rằng "aha! Phương thức này là tĩnh nên nó không cần thể hiện và có lẽ không thao tác trạng thái cụ thể của đối tượng". Vì vậy, bạn sẽ truyền đạt bản chất tĩnh của phương pháp một cách hiệu quả ....
Như các áp phích trước đây đã nói: Đây có vẻ như là một sự tối ưu hóa quá sớm.
Tuy nhiên, có một điểm khác biệt (một phần từ thực tế là các lệnh gọi không tĩnh yêu cầu đẩy thêm một đối tượng callee vào ngăn xếp toán hạng):
Vì không thể ghi đè các phương thức tĩnh, nên sẽ không có bất kỳ tra cứu ảo nào trong thời gian chạy cho một cuộc gọi phương thức tĩnh. Điều này có thể dẫn đến sự khác biệt có thể quan sát được trong một số trường hợp.
Sự khác biệt ở mức mã byte là một lệnh gọi phương thức không tĩnh được thực hiện thông qua INVOKEVIRTUAL
, INVOKEINTERFACE
hoặc INVOKESPECIAL
trong khi một lệnh gọi phương thức tĩnh được thực hiện thông qua INVOKESTATIC
.
invokespecial
vì nó không phải là ảo.
Không thể tin được rằng bất kỳ sự khác biệt nào về hiệu suất của các cuộc gọi tĩnh so với không tĩnh đang tạo ra sự khác biệt trong ứng dụng của bạn. Hãy nhớ rằng "tối ưu hóa quá sớm là gốc rễ của mọi điều ác".
7 năm sau ...
Tôi không tin tưởng lắm vào kết quả mà Mike Nakis đã tìm thấy vì chúng không giải quyết một số vấn đề phổ biến liên quan đến tối ưu hóa Hotspot. Tôi đã công cụ điểm chuẩn bằng cách sử dụng JMH và nhận thấy chi phí của một phương thức phiên bản là khoảng 0,75% trên máy của tôi so với một cuộc gọi tĩnh. Với chi phí thấp, tôi nghĩ rằng ngoại trừ trong các hoạt động nhạy cảm với độ trễ nhất, nó được cho là không phải là mối quan tâm lớn nhất trong thiết kế ứng dụng. Kết quả tóm tắt từ điểm chuẩn JMH của tôi như sau;
java -jar target/benchmark.jar
# -- snip --
Benchmark Mode Cnt Score Error Units
MyBenchmark.testInstanceMethod thrpt 200 414036562.933 ± 2198178.163 ops/s
MyBenchmark.testStaticMethod thrpt 200 417194553.496 ± 1055872.594 ops/s
Bạn có thể xem mã ở đây trên Github;
https://github.com/nfisher/svsi
Bản thân điểm chuẩn khá đơn giản nhưng nhằm mục đích giảm thiểu việc loại bỏ mã chết và việc gấp liên tục. Có thể có những cách tối ưu khác mà tôi đã bỏ qua / bỏ qua và những kết quả này có thể sẽ khác nhau tùy theo bản phát hành JVM và hệ điều hành.
package ca.junctionbox.svsi;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;
class InstanceSum {
public int sum(final int a, final int b) {
return a + b;
}
}
class StaticSum {
public static int sum(final int a, final int b) {
return a + b;
}
}
public class MyBenchmark {
private static final InstanceSum impl = new InstanceSum();
@State(Scope.Thread)
public static class Input {
public int a = 1;
public int b = 2;
}
@Benchmark
public void testStaticMethod(Input i, Blackhole blackhole) {
int sum = StaticSum.sum(i.a, i.b);
blackhole.consume(sum);
}
@Benchmark
public void testInstanceMethod(Input i, Blackhole blackhole) {
int sum = impl.sum(i.a, i.b);
blackhole.consume(sum);
}
}
ops/s
chủ yếu trong môi trường ART (ví dụ: sử dụng bộ nhớ, giảm kích thước tệp .oat, v.v.). Bạn có biết về bất kỳ công cụ / cách tương đối đơn giản nào mà người ta có thể thử để chuẩn các chỉ số này không?
Đối với quyết định nếu một phương thức phải là tĩnh, khía cạnh hiệu suất sẽ không liên quan. Nếu bạn gặp vấn đề về hiệu suất, việc tạo nhiều phương thức tĩnh sẽ không giúp bạn tiết kiệm thời gian. Điều đó nói rằng, các phương thức tĩnh gần như chắc chắn không chậm hơn bất kỳ phương thức phiên bản nào, trong hầu hết các trường hợp, nhanh hơn một chút :
1.) Các phương thức tĩnh không phải là đa hình, vì vậy JVM có ít quyết định hơn để tìm mã thực để thực thi. Đây là một điểm tranh luận trong Age of Hotspot, vì Hotspot sẽ tối ưu hóa các lệnh gọi phương thức phiên bản chỉ có một trang web triển khai, vì vậy chúng sẽ hoạt động giống nhau.
2.) Một sự khác biệt nhỏ khác là các phương thức tĩnh rõ ràng là không có tham chiếu "this". Điều này dẫn đến một khung ngăn xếp nhỏ hơn một khe của một phương thức thể hiện có cùng chữ ký và nội dung ("cái này" được đặt trong vị trí 0 của các biến cục bộ ở mức bytecode, trong khi đối với phương thức tĩnh, vị trí 0 được sử dụng cho đầu tiên tham số của phương thức).
Có thể có sự khác biệt và nó có thể xảy ra theo cả hai cách đối với bất kỳ đoạn mã cụ thể nào và nó có thể thay đổi ngay cả khi một bản phát hành nhỏ của JVM.
Đây chắc chắn là một phần trong số 97% hiệu quả nhỏ mà bạn nên quên .
TableView
hàng triệu bản ghi.
Về lý thuyết, ít tốn kém hơn.
Khởi tạo tĩnh sẽ được thực hiện ngay cả khi bạn tạo một thể hiện của đối tượng, trong khi các phương thức tĩnh sẽ không thực hiện bất kỳ khởi tạo nào thường được thực hiện trong một phương thức khởi tạo.
Tuy nhiên, tôi chưa thử nghiệm điều này.
Như Jon lưu ý, các phương thức tĩnh không thể bị ghi đè, vì vậy chỉ cần gọi một phương thức tĩnh có thể - trên thời gian chạy Java đủ ngây thơ - nhanh hơn so với việc gọi một phương thức thể hiện.
Nhưng sau đó, ngay cả khi bạn đang ở thời điểm mà bạn quan tâm đến việc xáo trộn thiết kế của mình để tiết kiệm một vài nano giây, điều đó chỉ đưa ra một câu hỏi khác: bạn có cần phương pháp ghi đè chính mình không? Nếu bạn thay đổi mã của mình để biến một phương thức phiên bản thành một phương thức tĩnh để lưu một nano giây ở đây và ở đó, sau đó quay lại và triển khai trình điều phối của riêng bạn trên đó, điều phối viên của bạn gần như chắc chắn sẽ kém hiệu quả hơn so với phương thức được xây dựng vào thời gian chạy Java của bạn rồi.
Tôi muốn thêm vào các câu trả lời tuyệt vời khác ở đây rằng nó cũng phụ thuộc vào luồng của bạn, ví dụ:
Public class MyDao {
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, new MyRowMapper());
};
};
Hãy chú ý rằng bạn tạo một đối tượng MyRowMapper mới cho mỗi cuộc gọi.
Thay vào đó, tôi đề nghị sử dụng ở đây một trường tĩnh.
Public class MyDao {
private static RowMapper myRowMapper = new MyRowMapper();
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, myRowMapper);
};
};