4

My understanding was that all non-capturing lambdas shouldn't require object creation at use site, because one can be created as a static field and reused. In principle, the same could be true for lambdas constituting of a class method call - only the field would be non static. I never actually tried to dig any deeper into it; now I am looking at the bytecode, don't see one in the enclosing class and don't have a good idea where to look? I see though that the lambda factory is different than in Java, so this should have a clear answer - at least for a given Scala version.

My motivation is simple: profiling is very time consuming. Introducing method values (or in general, lambdas capturing only the state of the enclosing object) as private class fields is less clean and more work than writing them inline and, in general, not good code. But when writing areas known (with high likelihood) to be a hot spot, it's a very simple optimisation that can be performed straight away without any real impact on the programmer's time. It doesn't make sense though if no new object is created anyway.

Take for example:

def alias(x :X) = aliases.getOrElse(x, x)

def alias2(x :X) = aliases.getOrElse(x, null) match {
    case null => x
    case a => a
}

The first lambda (a Function0) must be a new object because it captures method parameter x, while the second one returns a constant (null) and thus doesn't really have to. It is also less messy (IMO) than a private class field, which pollutes the namespace, but I would like to be able to know for sure - or have a way of easily confirming my expectations.

Turin
  • 2,208
  • 15
  • 23
  • 2
    Does [this question](https://stackoverflow.com/q/27524445/11882002) help? – user Dec 21 '20 at 14:20
  • Not really; following the links did improve a bit my knowledge of the Java case, but I was particularly interested in Scala. By the look at the bytecode, Scala has it's own 'lambda factory' meaning the behaviour is 1) likely not JVM-dependent and 2) potentially different. And while some implemetations in Scala are more sophisticated than in Java, the team behind it is much smaller, the language much more complex, and sometimes there are corners cut. I was interested in a repeatable way to check the lambda implementation. – Turin Dec 21 '20 at 17:54
  • 1
    I expanded my question with motivation and my current knowledge. – Turin Dec 21 '20 at 18:16

2 Answers2

3

The following proves that at least some of the time, the answer is "no":

scala 2.13.4> def foo = () => 1
def foo: () => Int

scala 2.13.4> foo eq foo
val res5: Boolean = true
Seth Tisue
  • 29,985
  • 11
  • 82
  • 149
1

Looking at the bytecode produced by this code:

import scala.collection.immutable.ListMap

object ByName {
  def aliases = ListMap("Ein" -> "One", "Zwei" -> "Two", "Drei" -> "Three")

  val default = "NaN"

  def alias(x: String) = aliases.getOrElse(x, x)

  def alias2(x: String) = aliases.getOrElse(x, null) match {
      case null => x
      case a => a
  }
  def alias3(x: String) = aliases.getOrElse(x, default)
}

The compiler generates static methods for the by-name parameters. They look like this:

  public static final java.lang.String $anonfun$alias$1(java.lang.String);
    Code:
       0: aload_0
       1: areturn

  public static final scala.runtime.Null$ $anonfun$alias2$1();
    Code:
       0: aconst_null
       1: areturn

  public static final java.lang.String $anonfun$alias3$1();
    Code:
       0: getstatic     #26                 // Field MODULE$:LByName$;
       3: invokevirtual #138                // Method default:()Ljava/lang/String;
       6: areturn

The naive approach would have been for the compiler to generate anonymous classes that implement the Function0 interface. However, this would cause bytecode-bloat. Instead the compiler defers creating these anonymous inner classes until runtime via invokedynamic instructions.

Exactly how Scala uses these invokedynamic instructions is beyond my knowledge. It's possible that they cache the generated Function0 object somehow, but my guess is that the invokedynamic call is sufficiently optimized that it's faster to just generate a new one every time. Allocating short lived objects is cheap, and the cost is most often overestimated. Reusing an existing object might even be slower than creating a new one if it means cache misses.

I also want to point out that this is a implementation detail, and likely to change at any time. The Scala compiler devs and JVM devs know what they are doing, so you are probably better off trusting that their implementation balances performance well.

mbloms
  • 21
  • 3