Rompere le ottimizzazioni JIT con la riflessione


9

Quando ho armeggiato con i test unitari per una classe singleton altamente concorrente mi sono imbattuto nel seguente strano comportamento (testato su 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);
}

Le ultime 2 righe del metodo main () non sono d'accordo sul valore di INSTANCE - la mia ipotesi è che JIT abbia eliminato completamente il metodo poiché il campo è statico finale. La rimozione della parola chiave finale rende i valori corretti dell'output del codice.

Lasciando da parte la tua simpatia (o la sua mancanza) per i singoli e dimenticando per un minuto che usare la riflessione in questo modo richiede problemi - la mia ipotesi è corretta in quanto le ottimizzazioni della JIT sono da biasimare? In tal caso, sono limitati ai soli campi finali statici?


1
Un singleton è una classe per la quale può esistere solo un'istanza. Pertanto, non hai un singleton, hai solo una classe con un static finalcampo. Oltre a ciò, non importa se questo hack di riflessione si interrompe a causa di JIT o concorrenza.
Holger

@Holger questo hack è stato fatto in unit test solo come un tentativo di deridere il singleton per più casi di test di una classe che lo utilizza. Non vedo come la concorrenza potrebbe averlo causato (non ce n'è nel codice sopra) e mi piacerebbe davvero sapere cosa è successo.
Kelm

1
Bene, hai detto "classe singleton altamente concorrente" nella tua domanda e io dico " non importa " che cosa lo rompa. Quindi, se il tuo particolare codice di esempio si interrompe a causa di JIT e trovi una soluzione per quello e poi, il codice reale cambia da interruzione a causa di JIT a interruzione a causa di concorrenza, cosa hai guadagnato?
Holger

@Holger okay, il testo era un po 'troppo forte lì, mi dispiace per quello. Quello che intendevo dire era questo: se non capiamo perché qualcosa va così orribilmente storto siamo inclini a essere morsi dalla stessa cosa in futuro, quindi preferirei conoscere la ragione piuttosto che supporre "succede semplicemente". Comunque, grazie per il tempo dedicato a rispondere!
Kelm

Risposte:


7

Prendendo letteralmente la tua domanda, “ … è corretto il mio presupposto in quanto le ottimizzazioni della JIT sono da biasimare? ", La risposta è sì, è molto probabile che le ottimizzazioni JIT siano responsabili di questo comportamento in questo esempio specifico.

Ma poiché cambiare i static finalcampi è completamente fuori specifica, ci sono altre cose che possono romperlo in modo simile. Ad esempio, il JMM non ha una definizione per la visibilità della memoria di tali cambiamenti, quindi non è completamente specificato se o quando altri thread notano tali cambiamenti. Non sono nemmeno tenuti a notarlo in modo coerente, ovvero possono usare il nuovo valore, seguito dal nuovo valore, anche in presenza di primitive di sincronizzazione.

Tuttavia, JMM e l'ottimizzatore sono difficili da separare comunque qui.

La tua domanda " ... sono limitati ai soli campi finali statici? "È molto più difficile rispondere, poiché le ottimizzazioni, ovviamente, non si limitano ai static finalcampi, ma il comportamento di, ad esempio finalcampi non statici , non è lo stesso e presenta differenze anche tra teoria e pratica.

Per i finalcampi non statici , in determinate circostanze sono consentite modifiche tramite Reflection. Ciò è indicato dal fatto che setAccessible(true)è sufficiente per rendere possibile tale modifica, senza hackerare l' Fieldistanza per cambiare il modifierscampo interno .

Le specifiche dicono:

17.5.3. Modifica successiva dei finalcampi

In alcuni casi, come la deserializzazione, il sistema dovrà modificare i finalcampi di un oggetto dopo la costruzione. finali campi possono essere modificati tramite la riflessione e altri mezzi dipendenti dall'implementazione. L'unico modello in cui ciò ha una semantica ragionevole è quello in cui viene costruito un oggetto e quindi i finalcampi dell'oggetto vengono aggiornati. L'oggetto non deve essere reso visibile ad altri thread, né i finalcampi devono essere letti, fino al completamento di tutti gli aggiornamenti ai finalcampi dell'oggetto. I blocchi di un finalcampo si verificano sia alla fine del costruttore in cui finalè impostato il campo, sia immediatamente dopo ogni modifica di un finalcampo tramite riflessione o altro meccanismo speciale.

...

Un altro problema è che le specifiche consentono un'ottimizzazione aggressiva dei finalcampi. All'interno di un thread, è consentito riordinare le letture di un finalcampo con quelle modifiche di un finalcampo che non avvengono nel costruttore.

Esempio 17.5.3-1. Ottimizzazione aggressiva dei finalcampi
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 
    } 
}

Nel dmetodo, il compilatore può riordinare liberamente le letture xe la chiamata g. Così, new A().f()potrebbe tornare -1, 0o 1.

In pratica, determinare i posti giusti in cui sono possibili ottimizzazioni aggressive senza interrompere gli scenari legali sopra descritti, è un problema aperto , quindi se non -XX:+TrustFinalNonStaticFieldsè stato specificato, HotSpot JVM non ottimizzerà i finalcampi non statici allo stesso modo dei static finalcampi.

Naturalmente, quando non si dichiara il campo come final, il JIT non può presumere che non cambierà mai, anche se, in assenza di primitive di sincronizzazione dei thread, può considerare le modifiche effettive che si verificano nel percorso del codice che ottimizza (incluso il quelli riflettenti). Quindi può ancora ottimizzare in modo aggressivo l'accesso, ma solo se le letture e le scritture avvengono ancora nell'ordine del programma all'interno del thread di esecuzione. Quindi noteresti le ottimizzazioni solo quando lo guardi da un thread diverso senza costrutti di sincronizzazione appropriati.


sembra che molte persone provino a sfruttare questo final, ma, sebbene alcuni abbiano dimostrato di essere più performanti, alcuni risparmi nsnon valgono la pena spaccare un sacco di altro codice. Motivo per cui Shenandoah sta sostenendo fuori su alcune delle sue bandiere per esempio
Eugene
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.