追加された冗長性を除いて、すべてのインスタンス変数を遅延初期化する必要があると述べるべきではない他の強力な理由はありますか?
3 に答える
まず第一に: 遅延 val の初期化で何か問題が発生した場合 (存在しない外部リソースへのアクセスなど)、最初に val にアクセスしたときにのみ気付くでしょうが、通常の val ではすぐに気付くでしょう。オブジェクトが構築されているので。クラスがまったく機能しなくなる (恐ろしい NullPointerExceptions の 1 つ) 原因となる lazy val に循環依存関係を持たせることもできますが、接続された lazy val の 1 つに初めてアクセスしたときにのみわかります。
そのため、怠惰な val はプログラムの決定性を低下させます。これは常に悪いことです。
2 つ目: 遅延 val に関連するランタイム オーバーヘッドがあります。Lazy val は現在、lazy val を使用するクラスのプライベート ビットマスク (int) によって実装されています (lazy val ごとに 1 ビットなので、32 以上の lazy val がある場合は 2 つのビットマスクになります)。
lazy val 初期化子が正確に 1 回だけ実行されるようにするために、フィールドが初期化されるときにビットマスクへの同期書き込みが行われ、フィールドがアクセスされるたびに揮発性読み取りが行われます。現在、揮発性読み取りは x86 アーキテクチャではかなり安価ですが、揮発性書き込みは非常に高価になる可能性があります。
私が知る限り、将来のバージョンの scala でこれを最適化するための取り組みが進行中ですが、単純な val アクセスと比較して、フィールドが初期化されているかどうかを確認するためのオーバーヘッドが常に発生します。たとえば、遅延 val アクセス用の追加コードにより、メソッドがインライン化されない場合があります。
もちろん、非常に小さなクラスの場合、ビットマスクのメモリ オーバーヘッドも関連している可能性があります。
しかし、パフォーマンス上の問題がない場合でも、val が相互に依存する順序を把握し、その順序で並べ替えて、通常の val を使用することをお勧めします。
編集:遅延値を使用した場合に得られる可能性のある非決定性を示すコード例を次に示します。
class Test {
lazy val x:Int = y
lazy val y:Int = x
}
このクラスのインスタンスは問題なく作成できますが、x または y にアクセスするとすぐに StackOverflow が発生します。もちろん、これは人為的な例です。現実の世界では、はるかに長く、自明ではない依存サイクルがあります。
以下は、:javap を使用した scala コンソール セッションであり、lazy val の実行時のオーバーヘッドを示しています。最初に通常の値:
scala> class Test { val x = 0 }
defined class Test
scala> :javap -c Test
Compiled from "<console>"
public class Test extends java.lang.Object implements scala.ScalaObject{
public int x();
Code:
0: aload_0
1: getfield #11; //Field x:I
4: ireturn
public Test();
Code:
0: aload_0
1: invokespecial #17; //Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #11; //Field x:I
9: return
}
そして今、怠惰なval:
scala> :javap -c Test
Compiled from "<console>"
public class Test extends java.lang.Object implements scala.ScalaObject{
public volatile int bitmap$0;
public int x();
Code:
0: aload_0
1: getfield #12; //Field bitmap$0:I
4: iconst_1
5: iand
6: iconst_0
7: if_icmpne 45
10: aload_0
11: dup
12: astore_1
13: monitorenter
14: aload_0
15: getfield #12; //Field bitmap$0:I
18: iconst_1
19: iand
20: iconst_0
21: if_icmpne 39
24: aload_0
25: iconst_0
26: putfield #14; //Field x:I
29: aload_0
30: aload_0
31: getfield #12; //Field bitmap$0:I
34: iconst_1
35: ior
36: putfield #12; //Field bitmap$0:I
39: getstatic #20; //Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
42: pop
43: aload_1
44: monitorexit
45: aload_0
46: getfield #14; //Field x:I
49: ireturn
50: aload_1
51: monitorexit
52: athrow
Exception table:
from to target type
14 45 50 any
public Test();
Code:
0: aload_0
1: invokespecial #26; //Method java/lang/Object."<init>":()V
4: return
}
ご覧のとおり、通常の val アクセサーは非常に短く、間違いなくインライン化されますが、遅延 val アクセサーは非常に複雑であり、(最も重要なのは同時実行性のために) 同期ブロック (monitorenter/monitorexit 命令) を伴います。コンパイラによって生成された追加フィールドも確認できます。
lazy val
まず、遅延変数 (存在しないと思います) ではなく、s (Scala の「定数」)について話すべきです。
2 つの理由は、特にクラス フィールドのコンテキストでは、保守性と効率性です。
効率: 非遅延 init の利点は、それがどこで発生するかを制御できることです。ワーカー スレッドで多数のオブジェクトを生成し、それらを中央処理に渡す fork-join タイプのフレームワークを想像してください。熱心な評価では、ワーカー スレッドで初期化が行われます。遅延評価では、これがマスター スレッドで行われるため、ボトルネックが発生する可能性があります。
保守性 : すべての値が遅延初期化され、プログラムが異常終了した場合、インスタンスの初期化とはまったく異なるコンテキストで、場合によっては別のスレッドでローカライズされたスタック トレースが取得されます。
また、ほぼ確実に、言語の実装に関連するコストもありますが(@Berylium が 1 つの例を投稿しているのを参照)、それらについて議論するのに十分な能力があるとは感じていません。
私があなたのコードを読んであなたがレイジーを使用した場合、パフォーマンスのペナルティに加えて、おそらくレイジーの最も高価なコストであるレイジー初期化を使用した理由を尋ねるのに時間を費やすでしょう。
ここで、遅延初期化 (および同様に、ここに含めるストリーム) について考えるべき場所は次のとおりです。
循環依存: 1 つの変数が初期化されている別の変数に依存する場合、および/またはその逆。無限集合: ストリームを使用すると、最初の 1000 個の素数を見つけることができます。通過する実数がいくつあるかを知る必要はありません。
他にもいくつかあると思います-これらは私が見ることができる大きなものです。
遅延 val は正確に 1 回評価される def のようなものであり、実際に必要な場合にのみ使用する必要があることを覚えておいてください。