揮発性読み取りを取り除くためにJavaでメモリバリアをエミュレートする


8

同時にアクセスされるフィールドがあり、何度も読み取られ、めったに書き込まれないと仮定します。

public Object myRef = new Object();

スレッドT1がmyRefを1分に1回別の値に設定し、他のN個のスレッドがmyRefを数十億回連続して同時に読み取るとします。myRefが最終的にすべてのスレッドに表示されることだけが必要です。

簡単な解決策は、AtomicReferenceまたは単にvolatileを次のように使用することです。

public volatile Object myRef = new Object();

ただし、afaikの揮発性読み取りではパフォーマンスコストが発生します。非常に小さいことはわかっていますが、これは実際に必要なものというよりは、私が不思議に思っているようなものです。したがって、パフォーマンスに関係なく、これは純粋に理論的な質問であると想定しましょう。

つまり、質問は次のようになります。書き込みサイトで何かを行うことによって、書き込みがほとんどない参照の揮発性読み取りを安全にバイパスする方法はありますか?

いくつか読んだ後、メモリの壁が私が必要とするものであるように見える。したがって、このような構成が存在する場合、私の問題は解決されます。

  • 書く
  • バリアを呼び出す(同期)
  • すべてが同期され、すべてのスレッドに新しい値が表示されます。(読み取りサイトでの永続的なコストがないと、キャッシュが同期されるため、古くなったり、一時的なコストが発生する可能性がありますが、その後は通常のフィールドに戻り、次の書き込みまで続きます)。

そのような構造はJava、または一般にありますか?この時点で私は仕方がありませんが、このようなものが存在する場合、それを維持するはるかに賢い人々によってすでにアトミックパッケージに組み込まれていると思います。(不定期に頻繁に読み取りと書き込みを行う必要はなかったのではないでしょうか?)それで、私の考えに何か問題があり、そのような構成はまったく不可能ですか?

いくつかのコードサンプルが同様の目的で「揮発性」を使用し、それが契約前に発生することを利用するのを見てきました。別の同期フィールドがあります。例:

public Object myRef = new Object();
public volatile int sync = 0;

そしてスレッド/サイトを書く時:

myRef = new Object();
sync += 1 //volatile write to emulate barrier

これが機能するかどうかはわかりませんが、x86アーキテクチャでのみ機能すると主張する人もいます。JMSの関連セクションを読んだ後、その揮発性書き込みがmyRefの新しい値を確認する必要があるスレッドからの揮発性読み取りと結合されている場合にのみ、動作することが保証されていると思います。(したがって、揮発性の読み取りを取り除きません)。

元の質問に戻ります。これはまったく可能ですか?Javaで可能ですか?Java 9 VarHandlesの新しいAPIの1つで可能ですか?


1
私には、ワークロードをシミュレートする実際のベンチマークを作成して実行する必要がある領域に十分に慣れているように思えます。
NPE、

1
JMMは、書き込みスレッドがsync += 1;読み取り、読み取りスレッドがsync値を読み取った場合、myRef更新も表示すると述べています。最終的にはリーダーが更新を確認するだけでよいので、これを利用して、リーダースレッドの1000回の反復ごとにのみ同期を読み取るなどの利点があります。しかし、同様のトリックをvolatileで行うこともできます。myRefフィールドをリーダーで1000回反復してキャッシュし、揮発性を使用して再度読み取ります...
PetrJanečekOct

@PetrJanečekしかし、スレッド間で共有されるカウンター変数へのアクセスを同期する必要はありませんか?それはボトルネックになりませんか?私の意見では、それはさらに高価になるでしょう。
Ravindra Ranwala

@RavindraRanwala 1000回程度の繰り返しまでカウントする場合は、すべてのリーダーに独自のカウンターがあります。sync読者がフィールドを意味する場合、いいえ、sync毎回の反復でフィールドに触れることはありません。更新があるかどうかを確認したい場合は、日和見的に行います。それは簡単な解決策をキャッシュするようになり、言ったmyRef...それを再読み込み、その後、1000回のラウンドのために
ペトルJaneček

@PetrJanečekありがとう、私はそれを可能な解決策と考えました。しかし、私はこれが一般的で強固な実装を使用して可能かどうか疑問に思っています。
sydnal

回答:


2

したがって、基本的にvolatileはランタイムコストのないのセマンティクスが必要です。

それは可能ではないと思います。

問題は、の実行時のコストがvolatile、ライターおよびリーダーコードにメモリバリアを実装する命令によるものであることです。メモリバリアを取り除いてリーダーを「最適化」すると、実際に書き込まれるときに「ほとんど書き込まれない」新しい値がリーダーに表示されることが保証されなくなります。

FWIW、sun.misc.Unsafeクラスの一部のバージョンは明示的なloadFencestoreFenceおよびfullFenceメソッドを提供しますが、それらを使用しても、を使用するよりもパフォーマンスが向上するとは思いませんvolatile


仮説的に ...

マルチプロセッサシステムの1つのプロセッサが他のすべてのプロセッサを認識できるようにする必要があります。

「ねえ!何をしていても、アドレスXYZのメモリキャッシュを無効にして、今すぐ実行してください。」

残念ながら、最近のISAはこれをサポートしていません。

実際には、各プロセッサが独自のキャッシュを制御します。


確かに、あなたの答えの中に仮説的に含まれているのは、私が求めていたものです。ありがとう。
sydnal

0

これが正しいかどうかはわかりませんが、キューを使用して解決する可能性があります。

ArrayBlockingQueue属性をラップするクラスを作成します。クラスには、更新メソッドと読み取りメソッドがあります。updateメソッドは新しい値をキューにポストし、最後の値を除くすべての値を削除します。readメソッドは、キューのピーク操作の結果を返します。つまり、読み取りは行いますが、削除はしません。キューの先頭で要素をピークしているスレッドは、妨げられることはありません。キューを更新するスレッドは、きれいに実行します。


0
  • ReentrantReadWriteLock少数の書き込み、多数の読み取りシナリオ用に設計されたものを使用できます。
  • StampedLock少数の書き込みと多数の読み取りの同じケース用に設計されたを使用できますが、読み取りを楽観的に試行することもできます。例:

    private StampedLock lock = new StampedLock();
    
    public void modify() {            // write method
        long stamp = lock.writeLock();
        try {
          modifyStateHere();
        } finally {
          lock.unlockWrite(stamp);
        }
    } 
    
    public Object read() {            // read method
      long stamp = lock.tryOptimisticRead();
      Object result = doRead();       //try without lock, method should be fast
      if (!lock.validate(stamp)) {    //optimistic read failed
        stamp = lock.readLock();      //acquire read lock and repeat read
        try {
          result = doRead();
        } finally {
          lock.unlockRead(stamp);
        }
      }
      return result;
    }
  • 既存のオブジェクトを複製し、コンストラクターを介して必要なプロパティのみを変更することによってのみ、状態を不変にし、制御された変更を許可します。新しい状態が構築されたら、多くの読み取りスレッドによって読み取られている参照にそれを割り当てます。この方法でスレッドを読み取ると、コストは発生しません



不変にすることは私のシナリオでは不可能です。そして、スタンプされたロックのケースが単純な揮発性の読み取りよりもコストがかからなければ、私はかなり驚かれるでしょう。しかし、私はそれを試してみます、ありがとう。
sydnal

0

X86はTSOを提供します。[LoadLoad] [LoadStore] [StoreStore]フェンスを無料で入手できます。

揮発性の読み取りには、リリースのセマンティクスが必要です。

r1=Y
[LoadLoad]
[LoadStore]
...

ご覧のとおり、これはすでにX86によって無料で提供されています。

あなたの場合、ほとんどの呼び出しは読み取りであり、キャッシュラインはすでにローカルキャッシュにあります。

コンパイラレベルの最適化には費用がかかりますが、ハードウェアレベルでは、揮発性読み取りは通常の読み取りと同じくらい高価です。

一方、揮発性書き込みは順次整合性を保証するために[StoreLoad]を必要とするため、より高価です(JVMでは、これは lock addl %(rsp),0またはMFENCEます)。あなたの状況では書き込みはめったにないので、これは問題ではありません。

このレベルでの最適化には注意が必要です。コードを実際に必要なものよりも複雑にするのは非常に簡単だからです。いくつかのベンチマーク(JMHの使用など)によって開発努力を導き、できれば実際のハードウェアでテストするのが最善です。また、偽りの共有のように隠された他の厄介な生き物がいる可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.