使用lazy val缓存字符串表示

时间:2010-10-07 15:21:46

标签: scala lazy-evaluation micro-optimization

我在JAXMag的Scala特刊中遇到了以下代码:

package com.weiglewilczek.gameoflife

case class Cell(x: Int, y: Int) {
  override def toString = position
  private lazy val position = "(%s, %s)".format(x, y)
}

在上面的代码中使用lazy val是否比以下代码提供了更多的性能?

package com.weiglewilczek.gameoflife

case class Cell(x: Int, y: Int) {
  override def toString = "(%s, %s)".format(x, y)
}

或者只是一个不必要的优化案例?

5 个答案:

答案 0 :(得分:19)

关于延迟val的一件事是,虽然它们只计算一次,但每次访问它们都受到双重检查锁定包装的保护。这对于防止两个不同的线程同时尝试使用热闹的结果来初始化值是必要的。现在,双重检查锁定是非常有效的(现在它实际上在JVM中工作),并且在大多数情况下不需要锁定获取,但是比简单的值访问有更多的开销。

此外(有些明显),通过缓存对象的字符串表示形式,您可以明确地权衡CPU周期,因为内存使用量可能会大幅增加。 “def”版本中的字符串可以被垃圾收集,而“lazy val”版本中的字符串则不会。

最后,正如性能问题一样,基于理论的假设在没有基于事实的基准测试的情况下几乎没有任何意义。如果没有剖析,你永远不会知道,所以不妨试试看看。

答案 1 :(得分:13)

toString可以使用lazy val直接覆盖。

scala> case class Cell(x: Int, y: Int) {
     |   override lazy val toString = {println("here"); "(%s, %s)".format(x, y)}
     | }
defined class Cell

scala> {val c = Cell(1, 2); (c.toString, c.toString)}
here
res0: (String, String) = ((1, 2),(1, 2))

请注意,def可能无法覆盖val - 您只能在子类中使成员更稳定。

答案 2 :(得分:1)

在第一个代码段position中,只需按需计算一次[when | if] toString方法。在第二个代码段中,每次调用方法时都会重新评估toString正文。鉴于xy无法更改,这是无意义的,应该存储toString值。

答案 3 :(得分:0)

根据定义,案例类是不可变的。 toString返回的任何值本身也是不可变的。因此,通过利用惰性val来实质上“缓存”该值是有意义的。另一方面,提供的toString实现只比所有case类提供的默认toString更多。如果香草案例类toString在下面使用了一个懒惰的val,我不会感到惊讶。

答案 4 :(得分:0)

对我来说看起来像微优化。 JVM足以处理此类情况。