リフレクションでJIT最適化を打ち破る


9

並行性の高いシングルトンクラスのユニットテストをいじるとき、次の奇妙な動作に遭遇しました(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);
}

main()メソッドの最後の2行はINSTANCEの値に同意しません。フィールドが静的なfinalであるため、JITがメソッドを完全に削除したと思います。最後のキーワードを削除すると、コードは正しい値を出力します。

シングルトンの同情(またはその欠如)を脇に置き、このようなリフレクションを使用すると問題が発生することを1分間忘れます-JITの最適化のせいであるという私の仮定は正しいですか?もしそうなら-それらは静的な最終フィールドのみに限定されますか?


1
シングルトンは、1つのインスタンスしか存在できないクラスです。したがって、シングルトンはなく、static finalフィールドを持つクラスがあります。その上、このリフレクションハックがJITまたは同時実行性のために壊れるかどうかは問題ではありません。
ホルガー

@Holgerこのハックは、単体テストでシングルトンを使用するクラスの複数のテストケースのシングルトンを模擬する試みとしてのみ行われました。同時実行によってどのように引き起こされたかはわかりません(上記のコードにはありません)。何が起こったのかを知りたいのですが。
ケルム

1
さて、あなたはあなたの質問で「非常に同時のシングルトンクラス」と言った、そして私はそれが壊れる原因は「問題ではない言った。したがって、JITが原因で特定のサンプルコードが機能しなくなり、その回避策が見つかった場合、実際のコードはJITによる機能停止から同時実行性による機能停止に変わりますが、何が得られましたか?
Holger

@ホルガー大丈夫、言葉遣いが少し強すぎたので、申し訳ありません。私が言ったのはこれでした-何かがひどくうまくいかない理由がわからない場合、将来同じことで噛まれる傾向があるので、私は「ただ起こっている」と仮定するよりも理由を知っています。とにかく、時間を割いて答えてくれてありがとう!
ケルム

回答:


7

あなたの質問を文字通り受けて、「JITの最適化のせいだという私の仮定は正しいですか?」、答えはイエスです。この特定の例では、JIT最適化がこの動作の原因である可能性が非常に高いです。

しかし、static finalフィールドの変更は完全に仕様から外れているため、同様にそれを壊す可能性のあるものが他にもあります。たとえば、JMMにはそのような変更のメモリの可視性に関する定義がないため、他のスレッドがそのような変更に気づくかどうか、またはいつ通知されるかは完全に指定されていません。つまり、同期プリミティブが存在する場合でも、新しい値を使用した後、古い値を再び使用する可能性があります。

ただし、JMMとオプティマイザをここで分離するのは困難です。

あなたの質問は「…それらは静的な最終フィールドのみに限定されていますか?もちろん、最適化はstatic finalフィールドに限定されないため、回答するのははるかに困難ですが、非静的finalフィールドなどの動作は同じではなく、理論と実践の間にも違いがあります。

非静的finalフィールドの場合、特定の状況でReflectionによる変更が許可されます。これは、内部フィールドを変更setAccessible(true)するためにFieldインスタンスにハッキングすることなく、そのような変更を可能にするのに十分であるという事実によって示されmodifiersます。

仕様は言う:

17.5.3。finalフィールドのその後の変更

逆シリアル化などの一部のケースでは、システムはfinal構築後にオブジェクトのフィールドを変更する必要があります。finalフィールドは、リフレクションおよびその他の実装に依存する手段によって変更できます。これが妥当なセマンティクスを持つ唯一のパターンは、オブジェクトが構築finalされ、オブジェクトのフィールドが更新されるパターンです。オブジェクトfinalfinalフィールドに対するすべての更新が完了するまで、オブジェクトを他のスレッドから見えるようにしたり、フィールドを読み取ったりしてはなりません。finalフィールドのフリーズは、フィールドfinalが設定されているコンストラクタの最後と、finalリフレクションまたはその他の特別なメカニズムによるフィールドの変更の直後の両方で発生します。

別の問題は、仕様がfinalフィールドの積極的な最適化を許可することです。スレッド内では、コンストラクターで発生しないフィールドのfinal変更を使用して、finalフィールドの読み取りを並べ替えることができます。

例17.5.3-1。finalフィールドの積極的な最適化
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 
    } 
}

このdメソッドでは、コンパイラは読み取りxと呼び出しをg自由に並べ替えることができます。このように、new A().f()返すことができ-10または1

実際には、上記の法的シナリオを壊すことなく積極的な最適化が可能な適切な場所を決定することは未解決の問題です。そのため-XX:+TrustFinalNonStaticFields、指定されていない限り、HotSpot JVMは非静的finalフィールドをフィールドと同じ方法で最適化しませんstatic final

もちろん、フィールドをとして宣言しない場合final、JIT はフィールドが決して変更されないとは想定できませんが、スレッド同期プリミティブがない場合、最適化されたコードパスで発生する実際の変更(反射するもの)。したがって、アクセスを積極的に最適化する可能性がありますが、実行中のスレッド内でプログラムの順序で読み取りと書き込みが行われる場合のみです。したがって、適切な同期構造を持たない別のスレッドからそれを見ると、最適化に気付くだけです。


多くの人がこのfinals を悪用しようとするようですが、パフォーマンスの向上が証明されている人nsもいますが、他の多くのコードを壊す価値のない節約もあります。たとえば
Eugene
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.