11

I am considering using Scala on a pretty computationally intensive program. Profiling the C++ version of our code reveals that we could benefit significantly from Lazy evaluation. I have tried it out in Scala 2.9.1 and really like it. However, when I ran the class through a decompiler the implemenation didn't look quite right. I'm assuming that it's an artifact of the decompiler, but I wanted to get a more conclusive answer...

consider the following trivial example:

class TrivialAngle(radians : Double) 
{
    lazy val sin = math.sin(radians)
}

when I decompile it, I get this:

import scala.ScalaObject;
import scala.math.package.;
import scala.reflect.ScalaSignature;

@ScalaSignature(bytes="omitted")
public class TrivialAngle
  implements ScalaObject
{
  private final double radians;
  private double sin;
  public volatile int bitmap$0;

  public double sin()
  {
    if ((this.bitmap$0 & 0x1) == 0);
    synchronized (this)
    {
      if (
        (this.bitmap$0 & 0x1) == 0)
      {
        this.sin = package..MODULE$.sin(this.radians);
        this.bitmap$0 |= 1; 
      } 
      return this.sin;
    }
  }

  public TrivialAngle(double radians)
  {
  }
}

To me, the return block is in the wrong spot, and you will always acquire the lock. This can't be what the real code is doing, but I am unable to confirm this. Can anyone confirm or deny that I have a bogus decompilation, and that the lazy implementation is somewhat reasonable (ie, only locks when it is computing the value, and doesn't acquire the lock for subsequent calls?)

Thanks!

For reference, this is the decompiler I used: http://java.decompiler.free.fr/?q=jdgui

fbl
  • 2,840
  • 3
  • 33
  • 41
  • Computationally intensive and you want to be doing locks? – Mike Dunlavey Oct 22 '11 at 21:05
  • no, I have lots of items that I only want to compute if/when I need them, and I would like those results cached after computation. Depending on the implementation, lazy does exactly what I would like. If I could specify no locking, that would be even better, but that's not the point of this question. – fbl Oct 22 '11 at 21:11
  • 1
    Well I've done plenty of tuning of computationally intensive C/C++/Fortran code (pharma simulation). The method I use [is this](http://stackoverflow.com/questions/375913/what-can-i-use-to-profile-c-code-in-linux/378024#378024). (You can't always believe profilers, even when they speak clearly.) – Mike Dunlavey Oct 22 '11 at 21:35
  • 6
    Scala code cannot be decompiled to Java. It makes use of features that are valid bytecode but to which there's no Java equivalent. I don't mean that's what's happening here (though it could be), but that one can't rely on decompiling Scala to Java. If you must, read the bytecode instead. – Daniel C. Sobral Oct 22 '11 at 22:10
  • @MikeDunlavey - I have used the VS2010 C++ Profiler in 'sampling' mode quite effectively. Are you aware of a good profiler that does stack trace sampling for java? I have used your technique in the past fairly successfully, but I tend to prefer profilers when they are useful. – fbl Oct 23 '11 at 14:24
  • @flevine100: It's not for Java, but I think [Zoom](http://www.rotateright.com) does the right thing, which is a) *stack* sampling, b) on *wall* time, c) reporting percent *by line/instruction* occupancy on stack, and d) manual throttling of sampling, so it's sampling during the time you care about (i.e. not user-wait). Also, lots of samples don't help much, and they actually hurt if they make you give up (c). Even Zoom can be improved on, but why most profilers don't at least do that much I don't know. Personally, I can't be in the profiler-writing business. – Mike Dunlavey Oct 23 '11 at 15:31

2 Answers2

9

What I get with javap -c does not correspond to your decompile. In particular, there is no monitor enter when the field is found to be initialized. Version 2.9.1 too. There is still the memory barrier implied by the volatile access of course, so it does not come completely free. Comments starting with /// are mine

public double sin();
  Code:
   0:   aload_0
   1:   getfield        #14; //Field bitmap$0:I
   4:   iconst_1
   5:   iand
   6:   iconst_0
   7:   if_icmpne       54 /// if getField & 1 == O goto 54, skip lock
   10:  aload_0
   11:  dup
   12:  astore_1
   13:  monitorenter
            /// 14 to 52 reasonably equivalent to synchronized block 
            /// in your decompiled code, without the return
   53:  monitorexit
   54:  aload_0
   55:  getfield        #27; //Field sin:D
   58:  dreturn        /// return outside lock
   59:  aload_1        /// (this would be the finally implied by the lock)
   60:  monitorexit
   61:  athrow
  Exception table:
   from   to  target type
    14    54    59   any
Didier Dupont
  • 29,398
  • 7
  • 71
  • 90
  • Thanks! If I could mark 2 responses as 'correct', I would. Both this answer and retronym's reveal the true nature of Lazy. – fbl Oct 22 '11 at 21:51
  • No problem, I prefer retronym's answer too, all the more that I did not new of this -Xprint:jvm option. javap might still be the final judge if you really do not trust scala, but better if it does not come to that. – Didier Dupont Oct 23 '11 at 14:35
9

scala -Xprint:jvm reveals the true story:

[[syntax trees at end of jvm]]// Scala source: lazy.scala
package <empty> {
  class TrivialAngle extends java.lang.Object with ScalaObject {
    @volatile protected var bitmap$0: Int = 0;
    <paramaccessor> private[this] val radians: Double = _;
    lazy private[this] var sin: Double = _;
    <stable> <accessor> lazy def sin(): Double = {
      if (TrivialAngle.this.bitmap$0.&(1).==(0))
        {
          TrivialAngle.this.synchronized({
            if (TrivialAngle.this.bitmap$0.&(1).==(0))
              {
                TrivialAngle.this.sin = scala.math.`package`.sin(TrivialAngle.this.radians);
                TrivialAngle.this.bitmap$0 = TrivialAngle.this.bitmap$0.|(1);
                ()
              };
            scala.runtime.BoxedUnit.UNIT
          });
          ()
        };
      TrivialAngle.this.sin
    };
    def this(radians: Double): TrivialAngle = {
      TrivialAngle.this.radians = radians;
      TrivialAngle.super.this();
      ()
    }
  }
}

It's a (since JVM 1.5) safe, and very fast, double checked lock.

More details:

What's the (hidden) cost of Scala's lazy val?

Be aware that if you have multiple lazy val members in a class, only one of them can be initialized at once, as they are guarded by synchronized(this) { ... }.

Community
  • 1
  • 1
retronym
  • 54,768
  • 12
  • 155
  • 168
  • Thanks! If I could mark 2 responses as 'correct', I would. I chose this one because it's slightly more readable ;) I am fully aware of the fact that lazy fields can only be initialized one at a time. This may, or may not influence how we architect our solution, but it certainly informs the decision. – fbl Oct 22 '11 at 21:52