Przełamywanie optymalizacji JIT dzięki refleksji


9

Podczas majstrowania przy testach jednostkowych dla wysoce współbieżnej klasy singleton natknąłem się na następujące dziwne zachowanie (testowane na 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);
}

Ostatnie 2 wiersze metody main () nie zgadzają się co do wartości INSTANCE - domyślam się, że JIT całkowicie pozbył się tej metody, ponieważ pole jest statyczne. Usunięcie końcowego słowa kluczowego powoduje, że kod wyjściowy ma prawidłowe wartości.

Odkładając na bok swoją sympatię (lub jej brak) na singletony i na chwilę zapominając, że użycie tego rodzaju refleksji prosi o kłopoty - czy moje założenie jest słuszne, jeśli chodzi o to, że optymalizacja JIT jest winna? Jeśli tak - czy są one ograniczone tylko do statycznych pól końcowych?


1
Singleton to klasa, dla której może istnieć tylko jedna instancja. Dlatego nie masz singletona, po prostu masz zajęcia z static finalpolem. Poza tym nie ma znaczenia, czy hack odbicia zrywa się z powodu JIT czy współbieżności.
Holger

@Holger ten hack został zrobiony w testach jednostkowych tylko jako próba wyśmiewania singletonu dla wielu przypadków testowych klasy, która go używa. Nie rozumiem, jak mogła to spowodować współbieżność (nie ma żadnego w powyższym kodzie) i naprawdę chciałbym wiedzieć, co się stało.
Kelm

1
W swoim pytaniu powiedziałeś „wysoce równoległa klasa singletonów” i mówię „ to nie ma znaczenia ”, co powoduje, że się psuje. Więc jeśli Twój przykładowy kod zepsuje się z powodu JIT i znajdziesz rozwiązanie tego problemu, a następnie, prawdziwy kod zmieni się z zepsucia z powodu JIT na zerwanie z powodu współbieżności, co zyskałeś?
Holger

@Holger w porządku, sformułowanie było tam zbyt mocne, przepraszam za to. Miałem na myśli to - jeśli nie rozumiemy, dlaczego coś dzieje się tak okropnie źle, jesteśmy skłonni do gryzienia się tym samym w przyszłości, więc wolę znać przyczynę niż zakładać, że „tak się po prostu dzieje”. W każdym razie dzięki za poświęcenie czasu na odpowiedź!
Kelm

Odpowiedzi:


7

Biorąc twoje pytanie dosłownie: „ … czy moje założenie jest słuszne, że winą są optymalizacje JIT? ”, Odpowiedź brzmi tak, jest bardzo prawdopodobne, że optymalizacje JIT są odpowiedzialne za to zachowanie w tym konkretnym przykładzie.

Ale ponieważ zmiana static finalpól jest całkowicie niezgodna ze specyfikacją, istnieją inne rzeczy, które mogą ją złamać podobnie. Np. JMM nie ma definicji widoczności pamięci takich zmian, dlatego nie jest określone, czy lub kiedy inne wątki zauważą takie zmiany. Nie są nawet zobowiązani do ciągłego zauważania tego, tzn. Mogą użyć nowej wartości, a następnie ponownie użyć starej wartości, nawet w obecności operacji podstawowych synchronizacji.

Chociaż JMM i optymalizator i tak są tutaj trudne do oddzielenia.

Twoje pytanie „ … czy są one ograniczone tylko do statycznych pól końcowych? ”Jest o wiele trudniej odpowiedzieć, ponieważ optymalizacje nie są oczywiście ograniczone do static finalpól, ale zachowanie, np. finalPól niestatycznych , nie jest takie samo i ma również różnice między teorią a praktyką.

W przypadku finalpól niestatycznych modyfikacje przez odbicie są dozwolone w pewnych okolicznościach. Wskazuje na to fakt, że setAccessible(true)wystarczy, aby taka modyfikacja była możliwa, bez włamywania się do Fieldinstancji w celu zmiany modifierspola wewnętrznego .

Specyfikacja mówi:

17.5.3. Późniejsza modyfikacja finalpól

W niektórych przypadkach, takich jak deserializacja, system będzie musiał zmienić finalpola obiektu po zakończeniu budowy. finalpola można zmieniać za pomocą refleksji i innych środków zależnych od implementacji. Jedyny wzorzec, w którym ma to rozsądną semantykę, to taki, w którym obiekt jest konstruowany, a następnie finalpola obiektu są aktualizowane. Obiekt nie powinien być widoczny dla innych wątków, a finalpola nie powinny być odczytywane, dopóki wszystkie aktualizacje finalpól obiektu nie zostaną zakończone. Zamrożenie finalpola występuje zarówno na końcu konstruktora, w którym finalpole jest ustawione, jak i natychmiast po każdej modyfikacji finalpola poprzez odbicie lub inny specjalny mechanizm.

Innym problemem jest to, że specyfikacja pozwala na agresywną optymalizację finalpól. W obrębie wątku dopuszcza się zmianę kolejności odczytów finalpola z tymi modyfikacjami finalpola, które nie mają miejsca w konstruktorze.

Przykład 17.5.3-1. Agresywna optymalizacja finalpól
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 
    } 
}

W dmetodzie kompilator może dowolnie zmieniać kolejność odczytów xi wywoływania g. W ten sposób new A().f()może powrócić -1, 0albo 1.

W praktyce określenie właściwych miejsc, w których możliwe są agresywne optymalizacje bez naruszenia opisanych powyżej scenariuszy prawnych, jest kwestią otwartą , więc jeśli -XX:+TrustFinalNonStaticFieldsnie zostanie to określone, HotSpot JVM nie będzie optymalizował finalpól niestatycznych w taki sam sposób, jak static finalpól.

Oczywiście, gdy nie zadeklarujesz pola jako final, JIT nie może zakładać, że nigdy się nie zmieni, jednak przy braku operacji podstawowych synchronizacji wątków może rozważyć faktyczne modyfikacje zachodzące w zoptymalizowanej ścieżce kodu (w tym odblaskowe). Może więc nadal agresywnie optymalizować dostęp, ale tylko tak, jakby odczyty i zapisy wciąż zachodziły w kolejności programów w wykonywanym wątku. Optymalizacje można zauważyć tylko wtedy, gdy patrzy się na to z innego wątku bez odpowiednich konstrukcji synchronizacyjnych.


wygląda na to, że wiele osób próbuje to wykorzystać final, ale chociaż niektóre okazały się bardziej wydajne, oszczędności nsnie są warte zepsucia wielu innych kodów. Powód, dla którego Shenandoah wycofuje się na przykład na niektórych swoich flagach
Eugene
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.