Performance 什么';s Scala的(隐藏)成本';什么是懒惰的瓦尔?
Scala的一个方便的特性是Performance 什么';s Scala的(隐藏)成本';什么是懒惰的瓦尔?,performance,scala,lazy-evaluation,Performance,Scala,Lazy Evaluation,Scala的一个方便的特性是lazy val,其中val的计算延迟到必要时(第一次访问时) 当然,lazy val必须有一些开销-Scala必须跟踪该值是否已被计算,并且计算必须同步,因为多个线程可能会尝试第一次同时访问该值 lazy val的成本到底是多少-是否有一个与lazy val关联的隐藏布尔标志来跟踪它是否已被评估,同步的成本到底是多少,是否还有其他成本 此外,假设我这样做: class Something { lazy val (x, y) = { ... } } 这与拥有
lazy val
,其中val
的计算延迟到必要时(第一次访问时)
当然,lazy val
必须有一些开销-Scala必须跟踪该值是否已被计算,并且计算必须同步,因为多个线程可能会尝试第一次同时访问该值
lazy val
的成本到底是多少-是否有一个与lazy val
关联的隐藏布尔标志来跟踪它是否已被评估,同步的成本到底是多少,是否还有其他成本
此外,假设我这样做:
class Something {
lazy val (x, y) = { ... }
}
这与拥有两个独立的
lazy val
sx
和y
相同吗?还是对于这对(x,y)
,我只得到一次开销?编译器似乎安排了一个类级位图int字段,以将多个惰性字段标记为已初始化(或未初始化)如果位图的相关xor指示需要,则初始化同步块中的目标字段
使用:
class Something {
lazy val foo = getFoo
def getFoo = "foo!"
}
生成示例字节码:
0 aload_0 [this]
1 getfield blevins.example.Something.bitmap$0 : int [15]
4 iconst_1
5 iand
6 iconst_0
7 if_icmpne 48
10 aload_0 [this]
11 dup
12 astore_1
13 monitorenter
14 aload_0 [this]
15 getfield blevins.example.Something.bitmap$0 : int [15]
18 iconst_1
19 iand
20 iconst_0
21 if_icmpne 42
24 aload_0 [this]
25 aload_0 [this]
26 invokevirtual blevins.example.Something.getFoo() : java.lang.String [18]
29 putfield blevins.example.Something.foo : java.lang.String [20]
32 aload_0 [this]
33 aload_0 [this]
34 getfield blevins.example.Something.bitmap$0 : int [15]
37 iconst_1
38 ior
39 putfield blevins.example.Something.bitmap$0 : int [15]
42 getstatic scala.runtime.BoxedUnit.UNIT : scala.runtime.BoxedUnit [26]
45 pop
46 aload_1
47 monitorexit
48 aload_0 [this]
49 getfield blevins.example.Something.foo : java.lang.String [20]
52 areturn
53 aload_1
54 monitorexit
55 athrow
class LazyTest {
lazy val msg = "Lazy"
}
在元组中初始化的值,如lazy val(x,y)={…}
通过相同的机制进行嵌套缓存。元组结果将被延迟计算和缓存,对x或y的访问将触发元组计算。从元组中提取单个值是独立、惰性地(并缓存)完成的。因此,上面的双实例化代码生成x
,y
,以及x$1
类型为Tuple2
的x$1
字段。这是取自的,并以Java代码(而不是字节码)的形式给出了lazy
的实现细节:
编译为与以下Java代码等效的内容:
class LazyTest {
public int bitmap$0;
private String msg;
public String msg() {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
msg = "Lazy";
}
}
bitmap$0 = bitmap$0 | 1;
}
}
return msg;
}
}
public class Example {
private String x;
private volatile boolean bitmap$0;
public String x() {
if(this.bitmap$0 == true) {
return this.x;
} else {
return x$lzycompute();
}
}
private String x$lzycompute() {
synchronized(this) {
if(this.bitmap$0 != true) {
this.x = "Value";
this.bitmap$0 = true;
}
return this.x;
}
}
}
考虑到scala为lazy生成的字节码,它可能会遇到双重检查锁定中提到的线程安全问题提出了一种新的lazy val实现,该实现更正确,但比“当前”版本慢约25% 下面的示例如下所示: Java文件中的类LazyCellBase{//我们需要一个公共位图\u 0 公共静态原子IntegerFieldDupDater arfu_0= AtomicIntegerFieldDupdater.newUpdater(LazyCellBase.class,“位图_0”); 公共易失性int位图_0=0; } 最后一个类LazyCell扩展了LazyCellBase{ 进口LazyCellBase_ 变量值_0:Int=_ @tailrec final def value():Int=(arfu_0.get(this):@开关)匹配{ 案例0=> if(arfu_0.比较数据集(this,0,1)){ val结果=0 值0=结果 @tailrec def complete():Unit=(arfu_0.get(this):@开关)匹配{ 案例1=> 如果(!arfu_0.compareAndSet(this,1,3))完成() 案例2=> if(arfu_0.比较数据集(this,2,3)){ 已同步{notifyAll()} }else complete() } 完成() 结果 }else值() 案例1=> arfu_0.比较数据集(这个,1,2) 同步的{ while(arfu_0.get(this)!=3)等待 } 值0 案例2=> 同步的{ while(arfu_0.get(this)!=3)等待 } 值0 案例3=>值0 } } 截至2013年6月,本SIP尚未获得批准。根据邮件列表的讨论,我希望它可能会被批准并包含在Scala的未来版本中。因此,我认为你最好注意: Lazy val不是免费的(甚至不便宜)。只有在你绝对同意的情况下才使用它 懒惰是为了正确,而不是为了优化
对于Scala 2.10,一个延迟值如下:
class Example {
lazy val x = "Value";
}
编译为类似以下Java代码的字节码:
class LazyTest {
public int bitmap$0;
private String msg;
public String msg() {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
msg = "Lazy";
}
}
bitmap$0 = bitmap$0 | 1;
}
}
return msg;
}
}
public class Example {
private String x;
private volatile boolean bitmap$0;
public String x() {
if(this.bitmap$0 == true) {
return this.x;
} else {
return x$lzycompute();
}
}
private String x$lzycompute() {
synchronized(this) {
if(this.bitmap$0 != true) {
this.x = "Value";
this.bitmap$0 = true;
}
return this.x;
}
}
}
请注意,位图由一个布尔值表示。如果添加另一个字段,编译器将增加该字段的大小,使其能够表示至少2个值,即作为字节
。这只适用于大型课程
但你可能想知道为什么这会起作用?进入同步块时,必须清除线程本地缓存,以便将非易失性x
值刷新到内存中。这篇博客文章给出了。我已经就这个问题写了一篇文章
简而言之,惩罚是如此之小,以至于在实践中你可以忽略它。我认为自从2007年发布这个Java版本以来,实现肯定已经改变了。只有一个同步块,bitmap$0
字段在当前实现(2.8)中不稳定。是的-我应该更加注意我发布的内容@米奇——我希望实现已经改变了!双重检查初始化反模式是一个典型的微妙缺陷。看,它是反模式到Java1.4的。由于Java1.5volatile关键字的含义更为严格,现在这种双重检查是可以的。那么,对于Scala2.10,当前的实现是什么?另外,请有人给出一个提示,这在实践中意味着多少开销,以及什么时候使用,什么时候避免的一些经验法则?这一说法也是由mitch对公认答案的评论提出的,并被@iirekm驳斥:从java1.5开始,这种模式就很好。感谢这个基准测试。您还可以针对SIP-20建议的实现进行基准测试吗?