7

I encountered the following code in JAXMag's Scala special issue:

package com.weiglewilczek.gameoflife

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

Does the use of lazy val in the above code provide considerably more performance than the following code?

package com.weiglewilczek.gameoflife

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

Or is it just a case of unnecessary optimization?

  • lazy should push the computation of the value off until it is called. However, good question. Does it create an object to hold the function for evalution? – wheaties Oct 07 '10 at 15:32
  • 2
    Just so you know, you can get that same string format by calling `(x, y).toString` –  Oct 07 '10 at 17:00

5 Answers5

19

One thing to note about lazy vals is that, while they are only calculated once, every access to them is protected by a double-checked locking wrapper. This is necessary to prevent two different threads from attempting to initialize the value at the same time with hilarious results. Now double-checked locking is pretty efficient (now that it actually works in the JVM), and won't require lock acquisition in most cases, but there is more overhead than a simple value access.

Additionally (and somewhat obviously), by caching the string representation of your object, you are explicitly trading off CPU cycles for possibly large increases in memory usage. The strings in the "def" version can be garbage-collected, while those in the "lazy val" version will not be.

Finally, as is always the case with performance questions, theory-based hypotheses mean nearly nothing without fact-based benchmarking. You'll never know for sure without profiling, so might as well try it and see.

Dave Griffith
  • 20,435
  • 3
  • 55
  • 76
13

toString can be directly overriden with a 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))

Note that a def may not override a val -- you can only make members more stable in the sub class.

retronym
  • 54,768
  • 12
  • 155
  • 168
1

In the first snippet position will be calculated just once, on demand, [when|if] toString method is called. In the second snippet, toString body will be re-evaluated every time the method is called. Given that x and y cannot be changed, it's senseless, and toString value should be stored.

Vasil Remeniuk
  • 20,519
  • 6
  • 71
  • 81
0

Case classes are, by definition, immutable. Any value returned by toString will itself be immutable, too. Thus it makes sense to essentially "cache" this value by utilizing a lazy val. On the other hand, the provided toString implementation does little more than the default toString provided by all case classes. I would not be surprised if a vanilla case class toString used a lazy val underneath.

Max A.
  • 4,842
  • 6
  • 29
  • 27
  • 3
    Immutable by definition? scala> case class Foo(var x: Int) defined class Foo – Viktor Klang Oct 08 '10 at 09:04
  • Didn't know the `var` modifier could be used on case class data members. I should have said, "case class given in the question is immutable by definition" although I still think what you did there is pure evil. – Max A. Oct 09 '10 at 17:47
0

Looks like a micro-optimization to me. JVM is able enough to take care of such cases.

  • 6
    There is absolutely nothing in the JVM which will memoize method results like this outside of local scope (i.e, if you call .toString twice in the same method, the JVM has a chance of caching simple method returns.) Persistently caching such results is way outside of it's scope, which is good since (in this case), automatically memoizing would result in significant memory footprint increases – Dave Griffith Oct 07 '10 at 16:02