Phá vỡ tối ưu hóa JIT với sự phản ánh


9

Khi loay hoay với các bài kiểm tra đơn vị cho một lớp singleton có tính đồng thời cao, tôi đã vấp phải hành vi kỳ lạ sau đây (được thử nghiệm trên JDK 1.8.0_162):

private static class SingletonClass {
    static final SingletonClass INSTANCE = new SingletonClass(0);
    final int value;

    static SingletonClass getInstance() {
        return INSTANCE;
    }

    SingletonClass(int value) {
        this.value = value;
    }
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

    System.out.println(SingletonClass.getInstance().value); // 0

    // Change the instance to a new one with value 1
    setSingletonInstance(new SingletonClass(1));
    System.out.println(SingletonClass.getInstance().value); // 1

    // Call getInstance() enough times to trigger JIT optimizations
    for(int i=0;i<100_000;++i){
        SingletonClass.getInstance();
    }

    System.out.println(SingletonClass.getInstance().value); // 1

    setSingletonInstance(new SingletonClass(2));
    System.out.println(SingletonClass.INSTANCE.value); // 2
    System.out.println(SingletonClass.getInstance().value); // 1 (2 expected)
}

private static void setSingletonInstance(SingletonClass newInstance) throws NoSuchFieldException, IllegalAccessException {
    // Get the INSTANCE field and make it accessible
    Field field = SingletonClass.class.getDeclaredField("INSTANCE");
    field.setAccessible(true);

    // Remove the final modifier
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    // Set new value
    field.set(null, newInstance);
}

2 dòng cuối cùng của phương thức main () không đồng ý với giá trị của INSTANCE - tôi đoán là JIT đã loại bỏ hoàn toàn phương thức này vì trường là tĩnh cuối cùng. Loại bỏ từ khóa cuối cùng làm cho đầu ra mã đúng giá trị.

Bỏ qua sự thông cảm của bạn (hoặc thiếu nó) cho những người độc thân và quên mất một phút rằng sử dụng sự phản chiếu như thế này sẽ gây rắc rối - liệu giả định của tôi có đúng trong sự tối ưu hóa JIT không? Nếu vậy - những cái đó chỉ giới hạn trong các trường cuối cùng tĩnh?


1
Một singleton là một lớp mà chỉ có một thể hiện có thể tồn tại. Do đó, bạn không có một người độc thân, bạn chỉ có một lớp học với một static finallĩnh vực. Bên cạnh đó, việc hack phản xạ này không thành vấn đề do JIT hoặc đồng thời.
Holger

@Holger bản hack này đã được thực hiện trong các bài kiểm tra đơn vị chỉ như một nỗ lực để chế nhạo singleton cho nhiều trường hợp kiểm tra của một lớp sử dụng nó. Tôi không thấy làm thế nào đồng thời có thể gây ra nó (không có mã nào ở trên) và tôi thực sự muốn biết chuyện gì đã xảy ra.
Kelm

1
Chà, bạn đã nói rằng, lớp singleton đồng thời rất cao trong câu hỏi của bạn và tôi nói rằng nó không quan trọng, điều gì làm cho nó bị phá vỡ. Vì vậy, nếu mã ví dụ cụ thể của bạn bị hỏng do JIT và bạn tìm ra cách khắc phục cho điều đó và sau đó, mã thực sự thay đổi từ phá vỡ do JIT sang phá vỡ do đồng thời, bạn đã đạt được gì?
Holger

@Holger được rồi, từ ngữ hơi quá mạnh ở đó, xin lỗi về điều đó. Ý tôi là thế này - nếu chúng ta không hiểu tại sao có điều gì đó quá tệ, chúng ta dễ bị cắn bởi điều tương tự trong tương lai, vì vậy tôi muốn biết lý do hơn là cho rằng "nó chỉ xảy ra". Dù sao, cảm ơn đã dành thời gian của bạn để trả lời!
Kelm

Câu trả lời:


7

Theo câu hỏi của bạn theo nghĩa đen, thì tối ưu là giả định của tôi trong việc tối ưu hóa JIT có đáng trách không? Câu trả lời là có, rất có thể các tối ưu hóa JIT chịu trách nhiệm cho hành vi này trong ví dụ cụ thể này.

Nhưng vì việc thay đổi static finalcác trường hoàn toàn không có đặc điểm kỹ thuật, có những thứ khác có thể phá vỡ nó tương tự. Ví dụ, JMM không có định nghĩa về khả năng hiển thị bộ nhớ của những thay đổi đó, do đó, nó hoàn toàn không được xác định cho dù các luồng khác có nhận thấy những thay đổi đó hay không. Họ thậm chí không bắt buộc phải chú ý đến nó một cách nhất quán, tức là họ có thể sử dụng giá trị mới, tiếp theo là sử dụng lại giá trị cũ, ngay cả khi có sự hiện diện của các nguyên thủy đồng bộ hóa.

Mặc dù vậy, JMM và trình tối ưu hóa khó có thể tách rời dù sao ở đây.

Câu hỏi của bạn có thể chỉ giới hạn trong các trường cuối cùng tĩnh? Tất nhiên, rất khó để trả lời, vì tối ưu hóa, tất nhiên, không giới hạn ở static finalcác trường, nhưng hành vi của, ví dụ như các trường không tĩnh final, không giống nhau và cũng có sự khác biệt giữa lý thuyết và thực tiễn.

Đối với các trường không tĩnh final, sửa đổi thông qua Reflection được cho phép trong một số trường hợp nhất định. Điều này được chỉ ra bởi thực tế setAccessible(true)là đủ để thực hiện sửa đổi như vậy, mà không cần hack vào Fieldtrường hợp để thay đổi trường nội bộ modifiers.

Các đặc điểm kỹ thuật nói:

17.5.3. Sửa đổi các finallĩnh vực sau đó

Trong một số trường hợp, chẳng hạn như khử lưu huỳnh, hệ thống sẽ cần thay đổi các finaltrường của một đối tượng sau khi xây dựng. finalcác trường có thể được thay đổi thông qua sự phản ánh và các phương tiện phụ thuộc vào việc thực hiện khác. Mẫu duy nhất trong đó có ngữ nghĩa hợp lý là một mẫu trong đó một đối tượng được xây dựng và sau đó các finaltrường của đối tượng được cập nhật. Không nên hiển thị đối tượng cho các luồng khác, cũng như các finaltrường không được đọc, cho đến khi tất cả các cập nhật cho các finaltrường của đối tượng hoàn tất. Sự đóng băng của một finaltrường xảy ra cả ở cuối của hàm tạo trong đó finaltrường được đặt và ngay sau mỗi lần sửa đổi một finaltrường thông qua sự phản chiếu hoặc cơ chế đặc biệt khác.

Giáo dục

Một vấn đề khác là đặc tả cho phép tối ưu hóa mạnh mẽ các finaltrường. Trong một luồng, cho phép sắp xếp lại các lần đọc của một finaltrường với những sửa đổi của một finaltrường không diễn ra trong hàm tạo.

Ví dụ 17.5.3-1. Tối ưu hóa tích cực của các finallĩnh vực
class A {
    final int x;
    A() { 
        x = 1; 
    } 

    int f() { 
        return d(this,this); 
    } 

    int d(A a1, A a2) { 
        int i = a1.x; 
        g(a1); 
        int j = a2.x; 
        return j - i; 
    }

    static void g(A a) { 
        // uses reflection to change a.x to 2 
    } 
}

Trong dphương thức, trình biên dịch được phép sắp xếp lại các lần đọc xvà cuộc gọi để gtự do. Như vậy, new A().f()có thể trở lại -1, 0hoặc 1.

Trong thực tế, việc xác định đúng nơi có thể tối ưu hóa tích cực mà không vi phạm các kịch bản pháp lý được mô tả ở trên, là một vấn đề mở , vì vậy trừ khi -XX:+TrustFinalNonStaticFieldsđược chỉ định, JVM của HotSpot sẽ không tối ưu hóa các trường không tĩnh finalgiống như static finalcác trường.

Tất nhiên, khi bạn không khai báo trường là final, JIT không thể cho rằng nó sẽ không bao giờ thay đổi, mặc dù, nếu không có các nguyên hàm đồng bộ hóa luồng, nó có thể xem xét các sửa đổi thực tế xảy ra trong đường dẫn mã mà nó tối ưu hóa (bao gồm cả những người phản ánh). Vì vậy, nó vẫn có thể tối ưu hóa tối đa quyền truy cập, nhưng chỉ khi - việc đọc và ghi vẫn xảy ra theo thứ tự chương trình trong luồng thực thi. Vì vậy, bạn chỉ nhận thấy sự tối ưu hóa khi nhìn vào nó từ một luồng khác mà không có cấu trúc đồng bộ hóa phù hợp.


có vẻ như nhiều người cố gắng khai thác điều này final, nhưng, mặc dù một số người đã được chứng minh là hoạt động tốt hơn, một số tiết kiệm nskhông đáng để phá vỡ nhiều mã khác. Lý do tại sao Shenandoah lại ủng hộ một số lá cờ của nó chẳng hạn
Eugene
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.