Skip to content

Array.fill, etc, could be optimized for constant functions #468

Open
@retronym

Description

@retronym

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions