原文はこちら。
The original article was written by John Rose (Java Virtual Machine Architect, Oracle).
https://cr.openjdk.java.net/~jrose/oblog/value-tearing.html
この記事では、VMの値型(value type)という概念が、その値型の不変条件の完全性をどのように保護すべきかを説明し、値のstruct-like(構造体のよう)な設計とpersistent(永続的)な設計の間のメモリ効果における重要な違いを説明しています。
[2019 Note:] 値型はProject Valhallaの対象であり、inlineクラス(inline class)として知られています。
Project Valhalla
https://openjdk.java.net/projects/valhalla/
First, the running example
カプセル化された整数座標のペアで、いくつかのアクセス関数を持つ、2個の変数のデータ構造を考えます。
class XY {
private int x = 1, y = 0;
public XY() { } // make a blank one
public boolean step(int p, int q) {
int x1 = x + p, y1 = y + q;
if (x1 == 0 && y1 == 0) return false;
x = x1; /*racing*/ y = y1;
return true;
}
public double measure() { // inverse radius
return Math.pow((double)x*x + (double)y*y, -0.5);
}
public void copyFrom(XY that) {
this.x = that.x; this.y = that.y;
}
}
読み取り関数の measure
は実数を生成しますが、x、yとも値がゼロの場合には失敗します。書き込み関数の step
は、両者がゼロの場合を除き、2個の座標を(酔歩のようにインクリメンタルに)更新します。
詳細はあまり重要ではなく、重要なのは、2値がほぼ独立しているけれども、どちらか一方の値だけを変更することで違反する可能性がある不変条件によって結合されているということです。
Off to the races
何がうまくいかないのでしょうか?その理由は、以下の条件のいずれかが満たされていれば、何も起こらないからです。
- オブジェクトが1個のスレッドにのみ限定されている場合
- 複数スレッドがオブジェクトを更新しているが、同期化によって
step
とmeasure
の呼び出しが重複しないようになっている場合 - XYが変更されず、安全に公開されることが保証されている場合。オブジェクトのコピーを隠すのは基本的な考え方で、だからこそ
copyFrom
というメソッドが必要なのかもしれません。
Side note 1: 完全に閉じられたXYオブジェクトであっても、原則として、yフィールドへの割り当てをキャンセルする非同期割り込みによって破壊される可能性がありますが、そのような割り込みは現在Javaでは見られません。
Java 非推奨スレッドプリミティブ / Java Thread Primitive Deprecation
https://docs.oracle.com/javase/jp/7/technotes/guides/concurrency/threadPrimitiveDeprecation.html
https://docs.oracle.com/javase/7/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html
Side note 2: これらの共有オプションの一般的な説明は、「Java Concurrency in Practice」のChapter 3に記載があります。General descriptions of these sharing options, and more, may be found in Chapter 3 of Java Concurrency in Practice.
Java Concurrency in Practice 1st Edition
https://www.amazon.co.jp/Java-Concurrency-Practice-Brian-Goetz/dp/0321349601
https://www.amazon.com/Java-Concurrency-Practice-Brian-Goetz/dp/0321349601
すべてのケースをカバーする簡単な方法は、すべてのメソッドを同期化したものとしてマークすることです。これは、古いJavaのクラスであるjava.util.Vector
の設計で、すべてのアクセス関数を同期化しています。同期化は通常コストがかかるので、このデザインを選択するとすべてのユーザーにコストを押し付けます。このコストは、Intelがサポートしているように、水面下でトランザクションを使用することで削減できることもありますが、言語とバイトコードセットがトランザクションを直接表現しないため、JVMが実際のクリティカルセクションを作成しなければならない可能性が常にあるため、最適化は信頼性がありません。
Transactional Synchronization with Intel® Core™ 4th Generation Processor
http://software.intel.com/en-us/blogs/2012/02/07/transactional-synchronization-in-haswell
これが(ArrayList
のようなより新しい標準クラスをデザインした人を含めて)多くのプログラマーが同期を省略し、そのかわりにユーザーにロックの責任を押しつけている理由です。これにより、ユーザは競合の危険性に対してロックのコストのバランスをとることができます。
ArrayList
https://docs.oracle.com/javase/jp/7/api/java/util/ArrayList.html
https://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html
ArrayList
を利用する人は、スレッド間の適切な相互排除をせずにリストへの参照を公開しないように責任を負わなければなりませんが、多くの場合、これは十分に簡単です。リストへの参照を公開しない責任を負う必要があります。データが共有される場合、共通のデザインルールの下で一緒にプログラミングされ、単純なバグを十分に捕捉できるようテストされているスレッド間で共有されます。
Insecurity complex
リストに特権操作のパラメータなどの機密データが含まれている場合、ユーザの責任は大きくなります。その場合、プログラマーは、たとえプログラムが何らかの方法で悪用されたとしても、共有リストへの参照が管理されていないコードに漏れないようにしなければなりません。そうしないと、攻撃者は共有リストの参照を作成または取得し、それを特権的なコードに渡し、同時にそれを突然変異させる可能性があります。突然変異はデータ競合を通じて被害者のコードにあらわれます。特権コードは引数の有効性をチェックして例外を投げるので、そのような突然変異のほとんどは無害です。しかし、攻撃者が十分な回数の試行を行うと、競合が発生し、特権コードを混乱させて攻撃者以外が予測できないようなことをするような状態にリストを追いやることができます。この種の弱点は TOCTTOU(Time of check to time of use)バグとして知られており、特権チェックが変更可能なデータオブジェクトに基づいている場合には常にリスクとなります。
Time of check to time of use
https://ja.wikipedia.org/wiki/Time_of_check_to_time_of_use
https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use
変数をプライベートとしてマークしても、データの競合による一貫性のない更新からは保護されないことに注意することが重要です。上記の単純な例では、すべての状態がプライベートで、以下のようにデータ競合は許可されない状態を作成することができます。
final XY xy = new XY();
Thread racer = new Thread(() -> {
for (int i = 0; i < 1e6; i++)
{ xy.step(-1, 1); xy.step(1, -1); }
});
racer.start();
for (int i = 0; i < 1e6; i++) {
double m = xy.measure();
if (m > 1) throw new AssertionError(m+"@"+i);
// => AssertionError: Infinity@293
}
このコードは、多くの場合、数百回の試行の後に、そのアサーションをすぐに失敗させることができます。両方のフィールドがゼロの状態で被害を見ることになります。この状態は、上記のXY.step
メソッドで /* racing */
とマークされている場所で観測されます。被害の単一フィールドに対する局所的に有効な更新により、この違反が引き起こされる可能性があります。被害をその許可されていない状態に導く一連の更新に加えて、追加の驚くべき状態が存在するかどうかは、読者の演習課題としておきます。
Side note:
measure
は各フィールドを2回読み取るので、1つのフィールドの2回の読み取りで異なる値を拾う可能性があります。オプティマイザは読み取りをマージする傾向があるため、これはまれな事象ですが、除外することはできません。このように、同じ変数の一貫性のない読み取りはバグの原因になります。これをsingle-variable-double-read hazard(一変数を複数回読み取ることで招く危険)と呼ぶことがあります。
2つのフィールドが競合条件のために相互のコヒーレンスを失うと、XYの不変条件が失敗します。このコヒーレンス喪失をstruct tearing(構造体の引き裂き)と呼ぶことにしましょう。なぜなら、犠牲者は、フィールドがパーツに引き裂かれて、他の部分から再構築された構造体であるように見えるからです。
JVMは(オリジナルのドキュメントと現在のJavaメモリモデルの両方で)、整数や参照などの個々の値のword tearing(ワードの引き裂き)を禁止しています。
Word Tearing (Java Language Specification)
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.6
https://docs.oracle.com/javase/specs/jls/se15/html/jls-17.html#jls-17.6
The Java memory model
https://dl.acm.org/doi/10.1145/1047659.1040336
例外として、64ビット・プリミティブ(longとdouble)は、2つの32ビット・フィールドの構造体として扱われ、独立した更新の対象です。したがって、64ビット・プリミティブは、XYの例における2個の32ビット・フィールドのように、競合によって引き裂かれる可能性があります。
Non-atomic Treatment of double and long (Java Language Specification)
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7
https://docs.oracle.com/javase/specs/jls/se15/html/jls-17.html#jls-17.7
JVM仕様では、64ビット・プリミティブを引き裂かないように実装することを推奨しています。一般的には、64ビットアトミックを持たない32ビットCPUのみがこの動作を示しますが、これらは消滅しつつある品種です。しかし、値型は64ビットを超えて容易に成長するため、構造体の引き裂きという同じ問題を再検討する必要があります。
Persistence pays off
先ほどの簡単な例で、データを保護する別の方法はfinal変数を使って定式化し、デザインを永続化(persistent)するように変更することです。
永続データ構造 / Persistent data structure
https://ja.wikipedia.org/wiki/永続データ構造
https://en.wikipedia.org/wiki/Persistent_data_structure
Side note: 今日、古い用語「immutable」の代替および改良版として「persistent」という言葉が再利用されています。immutableという用語の問題は、「immutable object」や「a mutable object」のようなフレーズで似通って聞こえて会話が行き詰まる時点で明らかです。もぐもぐ言う可能性のある “immutable “は、”mutable “に似通っています。10倍速で言ってみてください。
class XY {
final private int x, y;
private XY(int x, int y) { this.x = x; this.y = y; }
public static final BLANK = new XY(1, 0);
public XY step(int p, int q) {
int x1 = x + p, y1 = y + q;
if (x1 == 0 && y1 == 0) return null;
return new XY(x1, y1);
}
public double measure() { // inverse radius
return Math.pow((double)x*x + (double)y*y, -0.5);
}
//XY copyFrom(XY that) { return that; } // no special copy
}
この永続バージョンのXY
とは対照的に、最初のバージョンをstruct-likeと呼ぶことができます。フィールドのカプセル化にもかかわらず、Cの構造体のようにメモリと相互作用します。
永続バージョンでは、変数x
とy
の競合に悩まされません。永続版のXY
オブジェクトの状態は、参照を移動するだけでキャプチャしたり、公開したりできます。必要なのは、1つのパブリックなブランクの非初期化値だけで、コンストラクタ自体はカプセルの中に隠されています。
この新しい安定性と引き換えに、ユーザーは更新関数step
を呼び出すたびに参照を更新しなければなりません。重要なことに、禁止された状態でのXY
オブジェクトの観察は不可能になりました。巧妙に攻撃する競合スレッドに直面しても、クラスはその内部不変条件を完全に制御できます。まだ競合する可能性はありますが、それは参照上でのことです。また、参照の更新は1つのメモリ操作なので、複数のフィールドに別れるという問題はありません。とはいえ、single-variable-double-readバグはまれとはいえどもまだ起こる可能性があります。
安定性とセキュリティのためには常にコストがかかります。この場合のコストは、それぞれの状態ごとに新しいXY
オブジェクトを割り当てることです。これは、クラスが座標空間内で一つ一つの新しい位置を表すために新しいXY
オブジェクトを作成する必要があるからです。別の言い方をすれば、ユーザは、時間の経過とともに複数の値を表現するためにXY
オブジェクトを再利用する、という最適化は使用できません。この最適化は、上述のように競合状態を引き起こす可能性があるためです。これは合理的な制限です。
Side note:
XY
コンストラクタはプライベートなので、java.lang.Integer
のように値をキャッシュすることができますが、キャッシュロジックがエスケープ解析などの最適化を妨げる可能性があるため、これには驚くべきコストがかかることがあります。いずれにしても、コンストラクタをプライベートにすることは有益な手段です。
Where flattery gets us
しかし、mutableもしくは永続的な形で、JVMがXY
のインスタンスをフラットにするための最適化を行っていたとします。フラット化されたXY
のインスタンスは、その両方のフィールドを、あるオブジェクト(または配列)に直接格納します。フィールドを保持するためにヒープ上に別のXY
オブジェクトを用意する必要はありません。もちろん、ある用途でXYへの参照が必要な場合、これらの参照は一時的に作成され、その後破棄されます。
明らかに、この種のことを値型(value type)と呼んでいます。一言で言えば、値型はコンポーネント値のグループをカプセル化し、任意の型のJava変数に効率的に格納できます。
value types in the vm
http://cr.openjdk.java.net/~jrose/oblog/value-types-in-the-vm.html
値型は、上でスケッチした現在のものよりも、XY
や類似のクラスを設計するためのかなり良いオプションを提供してくれるでしょうか?答えは限定付きでYesです。
おそらく値型の最大の利点は、メモリの使用率の向上でしょう。別個のXY
オブジェクト(ヘッダ+パディング)のためのオーバーヘッドがなくなります。また、コードは、動的でキャッシュを破壊するポインターの追跡ではなく、静的なオフセット演算を使用して、包含しているオブジェクト内で直接x
とy
フィールドにアクセスできます。これは、多くのペアが配列に格納されている場合に最も顕著です。ハードウェアは、フィールドへのシーケンシャルアクセスに関して巧みになる可能性があります。
もう一つの主な利点は、複合値を渡したり返したりするメソッドのサポートが向上することです。メソッドは、スタック上の単一の平坦化された値(通常はレジスタ内)の中で、2つ以上の値を受け取ったり返したりすることができます。もちろん、ローカル変数にも平坦化された値を含めることができます。このようにして、複素数やベクトルは、他の小さくても有用な型のホストと共に実用的になります。
値の型から得られる利点は他にもありますが、ここでは詳細を説明しません。
Memory vs. method
2つの主な利点、すなわち、メモリのより良い使用とより良いメソッド型は、互いに緊張関係にあります。もし値型がメモリレイアウトのメカニズムとしてのみ考えられているならば、その主な使用法はJavaポインタを介したものになります。(メソッド型での)レジスタでのフラット化された実現は、様々なJVM最適化を混乱させる代償として、落ち着かないでメンテナンスされるフィクションになるでしょう。
一方、値型は、(再び、メソッド引数、ローカル変数、および戻り値を保持する)レジスタのロードのための巧妙な方法としてしか考えられていない場合、メモリの効果的な使用を提供できません。これは、他の変数(フィールドや配列要素)での表現がメモリ内に存在しなければならないため、メモリレイアウトの質に影響を受けやすいからです。
当然のことながら、我々は、レジスタ・ベースの変数とメモリ・ベースの変数の両方で良い表現を持つという、両方の利点を求めています。平坦化された値を指すJava参照をシステムが実体化する範囲では、それらの参照は簡単かつ日常的に最適化されなければなりません。そして、そのような実体化された参照は稀であるはずです。したがって、値型の優れたデザインには、参照を使用する二次的な「箱型」表現があったとしても、ポインタのない一次的な表現が必要です。
Climbing into the capsule
また、コンポーネントフィールドとメソッドのプライバシーを意味するカプセル化も必要です。JVMのための信頼できる値型のデザインにより、値型がアクセスを制限し、不変条件を強制することを可能にしなければなりません。
しかし、保護された不変条件が競合条件によって覆されてしまうのであれば、カプセル化は不完全であり、危険な幻想です。競合条件は値型の割り当てを実行できる誰もが利用できるからです。
ここで、上の最初の例のXY
のstruct-likeなプログラミングスタイルの欠陥に戻ります。上で見たように、Javaのメモリモデルは、構造体の引き裂きを許可し、クラスの作成者にその契約を明示することを要求しているため、クラスが競合に対して十分に防御しない場合、ユーザは防御的な行動を取ることができます。
しかし、いつか値型を使って、96ビットのタイムスタンプやカプセル化されたネイティブ・ポインタのようなセキュリティ上重要な値を含むようにしたいと思っています。String
のpersistent-styleのデザインは、セキュリティ上重要な値を格納するためには、コンテナ外での使い勝手に不可欠です。struct-likeな値型は、カプセル化が弱いので、同じ程度のセキュリティを確保するのは難しいか、不可能だと思います。それらは安全なアクセスを管理するためにコンテナの中に入れなければならないでしょう(Collections.synchronizedList
の中のArrayList
のようなものです)、これはそもそもそれらをフラット化することの利点を打ち消してしまう傾向があります。
[2019 Note:] 値型はProject Panamaで定義されるパフォーマンスAPIにとって重要になるでしょう。これらにはカプセル化されたネイティブポインタと(様々なIntel AVXベクター型のような)ネイティブSIMD値が含まれます。
Project Panama: Interconnecting JVM and native code
https://openjdk.java.net/projects/panama/
Assignment as an act of violence
persistent-likeな設計がstruct-likeな設計よりも優れたカプセル化と安全な API を提供する具体的な理由がいくつかあります。
第一に、値型はメモリロケーションだけでなく、あらゆる種類の変数に格納できるため、あちこちへの代入が非常に一般的になります。代入は(現在のJVM設計では)JVMプリミティブであり、クラスがカスタマイズできないことに注意してください。これは既存のJava APIにとっては何の影響もありませんが、(Cのように)単に競合するコンポーネントでのコピーに一般化されてしまうと、値型に影響するすべての代入文は、structure tearingの危険性があります。
しかし、Javaプログラマーは代入によって割り当て量の内部構造を破壊することを期待していません(32ビットマシン上の64ビットプリミティブの唯一の例外は、広く無視されています)。値型の代入が基になる値の不変条件を乱すことを許すと、Javaプログラムに新しい永続的なバグを導入する可能性があります。
struct-likeな型に対するこの問題を修正するには、代入操作を明示的なメソッドとして再定義し、コピーされた値のコピー元とコピー先の両方で同期化を実行する必要があります。これを行うためのコードは複雑で、エラーやデッドロックが発生しやすくなります。実際にやってみると、値型の設計者は、(大きな目で見た信頼と希望を持って、間違いなく)ユーザーにこの問題を押し付けて、この問題を先延ばしにしてしまうでしょう。一方、ユーザは、型の安全なバージョンと安全でないバージョンの間の従来の区別、例えば FastString
と ThreadSafeString
の間の選択方法などを学ばなければならないでしょう。
Publishing via finals
第二に、より微妙ですが、安全な公開は、Javaメモリモデルの中でも最もよく理解されていない側面の一つですが、非永続型の安全な共有には欠かせません。安全な公開には、変異可能な状態を含むオブジェクトで正確に同期するか、final変数を使って状態を公開するかのいずれかが必要です。これらのオプションはどちらも、現在十分に便利な間接レベルを必要としますが、データ構造がフラット化されていくにつれ、消えていくことになるでしょう。
安全な公開パターンの値型への最も単純な(したがって最も安全な)拡張は、値がが代入されたときにコンポーネントを自動的に個別に公開するよう、その構成要素のフィールドをfinalであると宣言することです。このパターンは、persistent-styleの値から自然に出てきますが、struct-likeな値には強制する必要があります。
ある意味では、変数をデフォルトで変更可能にするというJavaの古い設計上の決定を再訪していることになります。空白の最終フィールドがJava 1.1で追加され、persistent-styleな型が可能になり、今日でもまだフリーズした(不変性、永続性のある)配列はありません。新しい複合型を、要求されない限り変異可能であるように定義することは、Javaの最古のバージョンと一致しているでしょう。しかし、より近代的な形式のスレッド間通信は、その元のモデルの外で作成されており、同期を避け、final変数に(JMMを通じて)安全な公開セマンティクスを追加しています。「Java should have been designed with immutability as the default(Javaは不変性をデフォルトとして設計されるべきだった)」と言っても誇張ではないでしょうが、私は、現代的なスレッドの安全性を得るために、Java 1.0の変異性に関する規約との整合性を合理的に引き離すことができると信じています。
Persistence costs
以上が、私がstruct-likeなパターンよりも永続的デザインパターンの方が好ましいと考える理由です。これは、次に説明する永続的デザインの難しさにもかかわらず、真実です。
第一に、final変数によってもたらされる安定性にはコストがかかります。現在のJVMでは、それらはコンストラクタでメモリフェンスを必要とすることがあります。persistent-styleの値型をサポートするJVMでは、(レジスタではなく)メモリロケーションへのすべての代入は、コンポーネント値の書き込みが安全な公開であることを保証するために、メモリシステムとの同様のハンドシェイクを必要とすることがあります。
Correctness vs. throughput?
第二に、single-referenceのアップデートによって提供される原子性(atomicity)は、オブジェクトが平坦化されると消えてしまうので、他の方法で回復しなければなりません。persistent-styleの値型への格納は、オール・オア・ナッシングでなければなりません。つまり、別のスレッドはストア全体が見えるか、何も見えないかのいずれかでなければなりません。
これには、上述のIntelのトランザクションに沿った、メモリシステムとの別の種類のハンドシェイク、またはアトミックなマルチワードストア命令が必要になるでしょう。例の XY
型 は 64 ビットに適合するため、すべての 64 ビットプロセッサで追加コストなしでサポートされることに注意してください。より大きなアトミックベクトルストア命令を提供するプロセッサは、そのベクトルに収まるあらゆる値を安価にサポートします。複数のハードウェア・ベクトルやキャッシュ・ラインからあふれる非常に大きな値型の場合、JVMのソフトウェアは、昔ながらのボクシングを含む追加のハンドシェイクを実行する必要があります。
自分が何をしているかわかっている教養のあるユーザのために、structure tearingの可能性を持つ非トランザクショナルなストア操作を提供する必要があります。tearingのために、ライブラリは適切な閉じ込めや不変性などを保証するために、追加のインターロックを提供しなければならないでしょう。
言い換えれば、struct-likeなコンポーネントでの代入演算子は、永続パターンから派生した特権演算として利用できるようにし、慎重に設計された並行安全なライブラリ内でのみ使用する必要があるでしょう。
[2019 Note:] 今日のプロトタイプでは、適切に構築された変数ハンドルを使用して、値型へのインプレース・アップデートを最も簡単に取得できます。包含するインスタンスまたは配列の内部にネストした平坦化された値の単一フィールドを更新する変数ハンドルを生成できます。このようなハンドルは一般的に安全ではありませんが、finalなフィールドを定義するクラスと協力して更新できるのと同じように、フィールドを定義しているinlineクラスの協力を得て合法的に作成できます。どちらの場合も、前の値を読んだスレッドとの競合を避けるために、さらなる注意が必要ですが、これはここでは説明できない難しいトピックです。
JEP 193: Variable Handles
https://openjdk.java.net/jeps/193
これは、安全な公開のために、struct-likeな値のための永続コンテナを提供する必要性の裏返しです。重要な問題は、どちらをデフォルトにすべきか(mutableかpersistentか)、そしてどのような状況で非デフォルトモードを提供すべきか、ということだと思います。
[2019 Note:] 今日のコンセンサスは、妥当なパフォーマンスプロファイルを維持するために、inlineクラスは(デフォルトでは)以前のlongやdoubleのように引き裂くことができるようすべきだということです。Doug Leaに謝罪しますが、その妥協点は「あまりひどいものではない」ということです。一方、オプションで(
volatile
やalwaysatomic
のような)修飾子を付けてinlineクラスを宣言することができ、パフォーマンスを犠牲にしてでも、struct tearingを排除するために必要なすべての措置を取るようにJVMに指示します。これらのコストには、オンヒープの割り当てやソフトウェアのトランザクション処理が含まれている可能性が高いです。レジスタやスタック上の値は競合の対象とならないため、これらのコストは実際にヒープに格納されている値にのみ適用されます。このオプションは、ネイティブポインタなどのセキュリティ上重要な情報を格納する複数ワードのインラインに必要です。
Notation, notation, notation
値の永続スタイルの3つ目の欠点は、表記法です。「この数値の虚数成分をゼロに変更してください」と言いたくなることがあるでしょう。値型がその構成要素を公開し、構成要素全体の更新を受け入れてくれるのであれば、ユーザーにゼロから新しい値を作成することを要求するのは厳しいように思えます。
Complex c = ...;
c = new Complex(c.re, 0.0); // rebuild from scratch
c = c.changeIm(0.0); // maybe use a helper method
c.im = 0.0; // but what I meant was this
これは主にシンタックスシュガーの問題と見ることができますが、ここにはハードウェアとの深い関係もあります。マルチコンポーネントの値型は、メモリワードのブロック内のビットとして、あるいはライブレジスタのコレクション内のビットとして実現されます。ライブラリ設計者が操作を許可していると仮定すると、ユーザーがこれらの低レベルコンポーネントのうちの1つだけを分離して変更するように求めることは、基本的に合理的です。
JVMレベルでは、(必要に応じてメモリハンドシェイクを追加して)レジスタまたはメモリ書き込みで混乱せずに、コンポーネントの更新のバイトコードをかなり直接的にレンダリングすることができるはずです。あるコンポーネントが変わったという理由だけで、新しい値をゼロから再構築すると、ノイズの多い中間表現の束を作成する可能性があり、JITコンパイラがより重要な最適化作業に注力できなくなる可能性があります。
[2019 Note:] 今日のコンセンサスでは、値型へのインプレース更新は、特別なバイトコードではなく、変数ハンドルで表現するのが良いということです。
Our race is now run
結論として、persistent-styleの値の実装の課題は管理可能であり、struct-likeな値に対応する対立するトレードオフの方が制御がはるかに難しいと考えています。最終的には、安全でクリーンなユーザーモデルと、メモリ命令への直接コンパイルのどちらが重要かということになります。そして、安全性がスピードに反しているように見える場合、セキュアでないことよりも遅いことを修正するほうが容易いという期待を込めて、JVMはデフォルトで安全な設計に傾く必要があることに同意できると思います。
そして最終的には安全性とスピードの両方を手に入れることになると思います。