Description
Array.fill
accepts a by-name parameter for the element. For the (common) case that a constant expression is used, this will incur a performance penalty (calling the virtual Function0.apply
method within the loop that fills the array.
There is an extra cost of calling ScalaRuntime.Array_update
inside the loop, too.
For best performance, the user needs to revert to the Java API:
class Fill {
def slow(n: Int, x: Int) = Array.fill[Int](n)(x)
def fast(n: Int, x: Int) = {
val a = new Array[Int](n)
java.util.Arrays.fill(a, x)
a
}
}
(TODO: Benchmark the difference here. The benchmark should use a variety of array types to show the worst case for the megamorphic call inside the Array.fill
while loop.)
Can we make the compiler and/or library do this automatically?
The most direct fix would be to teach the optimizer or some other part of the compiler about this specific case, and add a rewrite. Perhaps some of it falls out of well targetted inlining. But once we solve fill
, we would need to turn attention to tabulate
, and maybe map
, etc.
What if we let the library author introspect the provided lambda to check if it is constant, and dispatch to a more efficient branch.
Sketching out the library support:
/** Marker trait added by the compiler to anonymous functions that return a constant expression */
trait ConstantFunction
/**
* Reveals the lambda that implements the by-name parameter
*
* ```
* scala> val f = new scala.runtime.AbstractFunction0[Int] with ConstantFunction { def apply = 42 }
* f: scala.runtime.AbstractFunction0[Int] with ConstantFunction = ...
*
* scala> def foo[A](a: => A) = lambdaOf(a)
* foo: [A](a: => A)() => A
*
* scala> foo(f()) eq f
* res3: Boolean = true
* ```
* */
final def lambdaOf[A](a: => A): () => A = () => a
/** fill generic array */
final def array_fill(xs: AnyRef, value: Any): Unit = {
xs match {
case x: Array[AnyRef] => java.util.Arrays.fill(x, value.asInstanceOf[AnyRef])
case x: Array[Int] => java.util.Arrays.fill(x, value.asInstanceOf[Int])
case x: Array[Double] => java.util.Arrays.fill(x, value.asInstanceOf[Double])
case x: Array[Long] => java.util.Arrays.fill(x, value.asInstanceOf[Long])
case x: Array[Float] => java.util.Arrays.fill(x, value.asInstanceOf[Float])
case x: Array[Char] => java.util.Arrays.fill(x, value.asInstanceOf[Char])
case x: Array[Byte] => java.util.Arrays.fill(x, value.asInstanceOf[Byte])
case x: Array[Short] => java.util.Arrays.fill(x, value.asInstanceOf[Short])
case x: Array[Boolean] => java.util.Arrays.fill(x, value.asInstanceOf[Boolean])
case x: Array[Unit] => java.util.Arrays.fill(x.asInstanceOf[Array[Object]], ())
case null => throw new NullPointerException
}
}
Which we could exploit as:
def fill[T: ClassTag](n: Int)(elem: => T): Array[T] = {
lambdaOf(elem) match {
case _: ConstantFunction =>
val e = elem
val a = empty[T]
array_fill(a, e)
a
case _ =>
val b = newBuilder[T]
b.sizeHint(n)
var i = 0
while (i < n) {
b += elem
i += 1
}
b.result()
}
}
Related:
- I once prototyped use of reused classes for constant functions to avoid anonymous class bloat.
- I think Paul tried out similar ideas