Rüd*_*ehn 20
首先:如果懒惰的初始化出现问题(比如访问不存在的外部资源),你只会在第一次访问val时注意到它,而使用普通的val你会很快注意到正在构建对象.您还可以在延迟val中具有循环依赖性,这将导致该类根本不起作用(一个可怕的NullPointerExceptions),但是您只能在第一次访问其中一个连接的惰性值时找到它.
所以懒惰的vals使程序不那么确定,这总是一件坏事.
第二:懒惰的val涉及运行时开销.懒惰的val目前由一个使用延迟val的类中的私有位掩码(int)实现(每个懒惰的val有一位,所以如果你有超过32个懒的val,那么将有两个位掩码等)
为了确保延迟val初始值设定项仅运行一次,在初始化字段时会对位掩码进行同步写入,并且每次访问该字段时都会进行易失性读取.现在,在x86架构中,易失性读取相当便宜,但是易失性写入可能非常昂贵.
据我所知,在未来版本的scala中正在努力优化这一点,但是与直接访问相比,检查字段是否已初始化总会有开销.例如,lazy val访问的额外代码可能会阻止方法被内联.
当然对于非常小的类,位掩码的内存开销也可能是相关的.
但即使你没有任何性能问题,最好弄清楚val相互依赖的顺序,然后按顺序对它们进行排序并使用正常的val.
编辑:这是一个代码示例,说明了使用延迟val时可能获得的非确定性:
class Test {
lazy val x:Int = y
lazy val y:Int = x
}
Run Code Online (Sandbox Code Playgroud)
您可以毫无问题地创建此类的实例,但只要您访问x或y,就会获得StackOverflow.这当然是一个人为的例子.在现实世界中,你有更长的和非明显的依赖周期.
这是一个使用scala控制台会话:javap,它说明了lazy val的运行时开销.首先是正常的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
}
Run Code Online (Sandbox Code Playgroud)
而现在懒惰的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
}
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,普通的val访问器非常短并且肯定会内联,而惰性val访问器非常复杂(并且最重要的是并发)涉及同步块(monitorenter/monitorexit指令).您还可以看到编译器生成的额外字段.
首先,我们应该讨论lazy val
s(Scala的"常量"),而不是懒惰变量(我认为不存在).
两个原因是可维护性和效率,特别是在类字段的上下文中:
效率:非懒惰初始化的好处是你可以控制它发生的位置.想象一下fork-join类型框架,在这个框架中,您可以在工作线程中生成许多对象,然后将它们交给中央处理.通过eager eval,初始化在工作线程上完成.使用延迟eval,这是在主线程上完成的,可能会产生瓶颈.
可维护性:如果您的所有值都被懒惰地初始化,并且您的程序爆炸,您将获得一个堆栈跟踪,该跟踪本地化在与实例初始化完全不同的上下文中,可能在另一个线程中.
几乎可以肯定的是,与语言实现相关的成本(我看到@Beryllium已经发布了一个例子),但我觉得没有足够的能力来讨论它们.
归档时间: |
|
查看次数: |
1718 次 |
最近记录: |