Bộ nhớ đệm tham chiếu phương thức có phải là một ý tưởng hay trong Java 8 không?


81

Hãy xem xét tôi có mã như sau:

class Foo {

   Y func(X x) {...} 

   void doSomethingWithAFunc(Function<X,Y> f){...}

   void hotFunction(){
        doSomethingWithAFunc(this::func);
   }

}

Giả sử điều đó hotFunctionđược gọi rất thường xuyên. Sau đó có nên lưu vào bộ nhớ cache không this::func, có thể như thế này:

class Foo {
     Function<X,Y> f = this::func;
     ...
     void hotFunction(){
        doSomethingWithAFunc(f);
     }
}

Theo như hiểu biết của tôi về các tham chiếu phương thức java, Máy ảo tạo một đối tượng của một lớp ẩn danh khi một tham chiếu phương thức được sử dụng. Do đó, bộ nhớ đệm tham chiếu sẽ tạo đối tượng đó chỉ một lần trong khi cách tiếp cận đầu tiên tạo nó trên mỗi lần gọi hàm. Điều này có chính xác?

Các tham chiếu phương thức xuất hiện tại các vị trí nóng trong mã có nên được lưu vào bộ nhớ đệm không hay máy ảo có thể tối ưu hóa điều này và làm cho bộ nhớ đệm thừa? Có một cách thực hành tốt nhất chung về điều này hoặc là quá trình ghép máy ảo cao này cụ thể cho dù bộ nhớ đệm như vậy có tác dụng gì không?


Tôi muốn nói rằng bạn sử dụng chức năng đó nhiều đến mức mức độ điều chỉnh này là cần thiết / mong muốn, có lẽ bạn nên bỏ lambdas và triển khai trực tiếp chức năng, điều này mang lại nhiều chỗ cho các tối ưu hóa khác.
SJuan76

@ SJuan76: Tôi không chắc về điều này! Nếu một tham chiếu phương thức được biên dịch thành một lớp ẩn danh, nó sẽ nhanh như một lệnh gọi giao diện thông thường. Vì vậy, tôi không nghĩ rằng phong cách chức năng nên tránh cho mã nóng.
gexicide

4
Tham chiếu phương pháp được thực hiện với invokedynamic. Tôi nghi ngờ rằng bạn sẽ thấy sự cải thiện hiệu suất bằng cách lưu vào bộ nhớ đệm đối tượng hàm. Ngược lại: Nó có thể hạn chế tối ưu hóa trình biên dịch. Bạn đã so sánh hiệu suất của hai biến thể?
nosid

@nosid: Không, không so sánh. Nhưng tôi đang sử dụng phiên bản rất sớm của OpenJDK, vì vậy các con số của tôi có thể không quan trọng gì vì tôi đoán rằng phiên bản đầu tiên chỉ triển khai các tính năng mới nhanh chóng và điều này không thể so sánh với hiệu suất khi các tính năng đã hoàn thiện tăng ca. Đặc điểm kỹ thuật có thực sự bắt buộc invokedynamicphải được sử dụng không? Tôi không thấy lý do gì cho điều này ở đây!
gexicide

4
Nó sẽ được lưu trữ tự động (không tương đương với việc tạo một lớp ẩn danh mới mỗi lần), vì vậy bạn không cần phải lo lắng về việc tối ưu hóa đó.
assylias

Câu trả lời:


82

Bạn phải phân biệt giữa việc thực thi thường xuyên của cùng một trang web gọi , đối với lambda không trạng thái hoặc lambda có trạng thái và việc sử dụng thường xuyên một phương thức tham chiếu đến cùng một phương thức (bởi các trang web gọi khác nhau).

Hãy xem các ví dụ sau:

    Runnable r1=null;
    for(int i=0; i<2; i++) {
        Runnable r2=System::gc;
        if(r1==null) r1=r2;
        else System.out.println(r1==r2? "shared": "unshared");
    }

Ở đây, cùng một trang web gọi được thực thi hai lần, tạo ra một lambda không trạng thái và việc triển khai hiện tại sẽ được in "shared".

Runnable r1=null;
for(int i=0; i<2; i++) {
  Runnable r2=Runtime.getRuntime()::gc;
  if(r1==null) r1=r2;
  else {
    System.out.println(r1==r2? "shared": "unshared");
    System.out.println(
        r1.getClass()==r2.getClass()? "shared class": "unshared class");
  }
}

Trong ví dụ thứ hai này, cùng một trang web gọi được thực thi hai lần, tạo ra một lambda chứa tham chiếu đến một Runtimethể hiện và việc triển khai hiện tại sẽ in ra "unshared"nhưng "shared class".

Runnable r1=System::gc, r2=System::gc;
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
    r1.getClass()==r2.getClass()? "shared class": "unshared class");

Ngược lại, trong ví dụ cuối cùng là hai trang web cuộc gọi khác nhau tạo ra một tham chiếu phương thức tương đương nhưng khi 1.8.0_05nó sẽ in "unshared""unshared class".


Đối với mỗi tham chiếu phương thức hoặc biểu thức lambda, trình biên dịch sẽ phát ra một invokedynamiclệnh tham chiếu đến phương thức bootstrap do JRE cung cấp trong lớp LambdaMetafactoryvà các đối số tĩnh cần thiết để tạo ra lớp triển khai lambda mong muốn. Nó được để lại cho JRE thực tế mà meta factory tạo ra nhưng nó là một hành vi được chỉ định của invokedynamiclệnh để ghi nhớ và sử dụng lại CallSitethể hiện được tạo trong lần gọi đầu tiên.

JRE hiện tại tạo ra một ConstantCallSitechứa một MethodHandleđối tượng không đổi cho các lambdas không trạng thái (và không có lý do gì có thể tưởng tượng để làm điều đó theo cách khác). Và các tham chiếu phương thức đến staticphương thức luôn không trạng thái. Vì vậy, đối với lambdas không trạng thái và các trang web gọi đơn lẻ, câu trả lời phải là: không lưu vào bộ nhớ cache, JVM sẽ làm được và nếu không, nó phải có những lý do chính đáng mà bạn không nên phản đối.

Đối với this::funclambda có tham số và là lambda có tham chiếu đến thiscá thể, mọi thứ hơi khác một chút. JRE được phép lưu vào bộ nhớ cache của chúng nhưng điều này có nghĩa là duy trì một số loại Mapgiữa các giá trị tham số thực tế và lambda kết quả có thể tốn kém hơn so với việc chỉ tạo lại cá thể lambda có cấu trúc đơn giản đó. JRE hiện tại không lưu vào bộ nhớ cache các cá thể lambda có trạng thái.

Nhưng điều này không có nghĩa là lớp lambda được tạo ra mọi lúc. Nó chỉ có nghĩa là call-site được giải quyết sẽ hoạt động giống như một cấu trúc đối tượng bình thường khởi tạo lớp lambda đã được tạo trong lần gọi đầu tiên.

Những điều tương tự áp dụng cho các tham chiếu phương thức đến cùng một phương pháp đích được tạo bởi các trang web gọi khác nhau. JRE được phép chia sẻ một cá thể lambda duy nhất giữa chúng nhưng trong phiên bản hiện tại thì không, hầu hết có thể là do không rõ liệu bảo trì bộ nhớ cache có thành công hay không. Ở đây, ngay cả các lớp được tạo có thể khác nhau.


Vì vậy, bộ nhớ đệm như trong ví dụ của bạn có thể giúp chương trình của bạn làm những việc khác so với khi không có. Nhưng không nhất thiết phải hiệu quả hơn. Đối tượng được lưu trong bộ nhớ cache không phải lúc nào cũng hiệu quả hơn đối tượng tạm thời. Trừ khi bạn thực sự đo lường tác động hiệu suất do tạo lambda gây ra, bạn không nên thêm bất kỳ bộ nhớ đệm nào.

Tôi nghĩ, chỉ có một số trường hợp đặc biệt mà bộ nhớ đệm có thể hữu ích:

  • chúng ta đang nói về rất nhiều trang web cuộc gọi khác nhau đề cập đến cùng một phương pháp
  • lambda được tạo trong khởi tạo phương thức khởi tạo / lớp vì sau này trang sử dụng sẽ
    • được gọi bởi nhiều chủ đề đồng thời
    • bị hiệu suất thấp hơn của lời kêu gọi đầu tiên

5
Làm rõ: thuật ngữ "call-site" đề cập đến việc thực thi invokedynamiclệnh sẽ tạo lambda. Nó không phải là nơi mà phương thức giao diện chức năng sẽ được thực thi.
Holger

1
Tôi nghĩ rằng this-capturing lambdas là các singlelet phạm vi cá thể (một biến thể hiện tổng hợp trên đối tượng). Đây không phải là như vậy?
Marko Topolnik

2
@Marko Topolnik: đó sẽ là một chiến lược biên dịch tuân thủ nhưng không, đối với jdk của Oracle cho đến nay 1.8.0_40thì không phải như vậy. Những con lambdas này không được nhớ do đó có thể được thu gom. Nhưng hãy nhớ rằng khi một trang invokedynamicweb gọi đã được liên kết, nó có thể được tối ưu hóa giống như mã thông thường, tức là Phân tích thoát hoạt động cho các trường hợp lambda như vậy.
Holger

2
Dường như không có một lớp thư viện tiêu chuẩn nào được đặt tên MethodReference. Ý bạn là MethodHandleở đây?
Lii

2
@Lii: bạn nói đúng, đó là lỗi đánh máy. Điều thú vị là dường như không ai nhận ra trước đây.
Holger

11

Thật không may, một tình huống mà nó là một lý tưởng tốt là nếu lambda được chuyển thành một trình lắng nghe mà bạn muốn xóa vào một thời điểm nào đó trong tương lai. Tham chiếu được lưu trong bộ nhớ cache sẽ cần thiết vì việc chuyển một tham chiếu phương thức this :: khác sẽ không được xem là cùng một đối tượng trong quá trình xóa và bản gốc sẽ không bị xóa. Ví dụ:

public class Example
{
    public void main( String[] args )
    {
        new SingleChangeListenerFail().listenForASingleChange();
        SingleChangeListenerFail.observableValue.set( "Here be a change." );
        SingleChangeListenerFail.observableValue.set( "Here be another change that you probably don't want." );

        new SingleChangeListenerCorrect().listenForASingleChange();
        SingleChangeListenerCorrect.observableValue.set( "Here be a change." );
        SingleChangeListenerCorrect.observableValue.set( "Here be another change but you'll never know." );
    }

    static class SingleChangeListenerFail
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();

        public void listenForASingleChange()
        {
            observableValue.addListener(this::changed);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(this::changed);
        }
    }

    static class SingleChangeListenerCorrect
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();
        ChangeListener<String> lambdaRef = this::changed;

        public void listenForASingleChange()
        {
            observableValue.addListener(lambdaRef);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(lambdaRef);
        }
    }
}

Sẽ rất tuyệt nếu không cần lambdaRef trong trường hợp này.


Ah, tôi thấy vấn đề bây giờ. Nghe có vẻ hợp lý, mặc dù có lẽ không phải là kịch bản mà OP nói đến. Tuy nhiên, đã ủng hộ.
Tagir Valeev

8

Theo như tôi hiểu về đặc tả ngôn ngữ, nó cho phép loại tối ưu hóa này ngay cả khi nó thay đổi hành vi có thể quan sát được. Xem các trích dẫn sau từ phần JSL8 §15.13.3 :

§15.13.3 Đánh giá thời gian chạy của các tham chiếu phương pháp

Tại thời điểm chạy, việc đánh giá biểu thức tham chiếu phương thức tương tự như đánh giá biểu thức tạo phiên bản lớp, trong chừng mực khi hoàn thành bình thường tạo ra một tham chiếu đến một đối tượng. [..]

[..] Một thể hiện mới của một lớp có các thuộc tính bên dưới được cấp phát và khởi tạo, hoặc một thể hiện hiện có của một lớp có các thuộc tính bên dưới được tham chiếu.

Một thử nghiệm đơn giản cho thấy, các tham chiếu phương thức cho các phương thức tĩnh (có thể) dẫn đến cùng một tham chiếu cho mỗi đánh giá. Chương trình sau in ra ba dòng, trong đó hai dòng đầu tiên giống hệt nhau:

public class Demo {
    public static void main(String... args) {
        foobar();
        foobar();
        System.out.println((Runnable) Demo::foobar);
    }
    public static void foobar() {
        System.out.println((Runnable) Demo::foobar);
    }
}

Tôi không thể tạo lại hiệu ứng tương tự cho các hàm không tĩnh. Tuy nhiên, tôi đã không tìm thấy bất cứ điều gì trong đặc tả ngôn ngữ, điều này ngăn cản sự tối ưu hóa này.

Vì vậy, miễn là không có phân tích hiệu suất để xác định giá trị của tối ưu hóa thủ công này, tôi thực sự khuyên bạn không nên thực hiện nó. Bộ nhớ đệm ảnh hưởng đến khả năng đọc của mã và không rõ liệu nó có bất kỳ giá trị nào hay không. Tối ưu hóa sớm là gốc rễ của mọi điều xấu.

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.