Performance 什么';s 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) = { ... } } 这与拥有

Scala的一个方便的特性是
lazy val
,其中
val
的计算延迟到必要时(第一次访问时)

当然,
lazy val
必须有一些开销-Scala必须跟踪该值是否已被计算,并且计算必须同步,因为多个线程可能会尝试第一次同时访问该值

lazy val
的成本到底是多少-是否有一个与
lazy val
关联的隐藏布尔标志来跟踪它是否已被评估,同步的成本到底是多少,是否还有其他成本

此外,假设我这样做:

class Something {
    lazy val (x, y) = { ... }
}

这与拥有两个独立的
lazy val
s
x
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建议的实现进行基准测试吗?