【问题标题】:Why fill array in while loop is so slow in Scala 3?为什么在 Scala 3 中的 while 循环中填充数组这么慢?
【发布时间】:2021-12-10 07:58:26
【问题描述】:

我有 2 个填充 Array[Int] 的实现,如下所述。第一个执行时间为 84 毫秒,第二个执行速度慢 100 倍:

Execute 'filling via Array.fill' in 84 ms
Execute 'filling via while' in 8334 ms

为什么第二个变体需要 100 倍以上的时间?它不是 GC,因为我可以在第二次同时删除第一次执行。我使用 Scala 3 在 Java 11 上运行它:

.jdks/adopt-openjdk-11.0.11/bin/java ... Fill

不仅如此,如果您打开 Array.fill 实现,您将通过 while... 看到实现

object Fill extends App {
  def timeMeasure[R](name: String)(block: => R): R = {
    val startTime = System.currentTimeMillis()
    val r = block
    val endTime = System.currentTimeMillis()
    println(s"Execute '$name' in ${endTime - startTime} ms")
    r
  }

  val n = 1000 * 1000 // * 10
  var ar = timeMeasure("filling via Array.fill") {
    Array.fill[Int](n)(10)
  }
  ar = timeMeasure("filling via while") {
    val array = new Array[Int](n)
    var i = 0
    while (i < n) {
      array(i) = i
      i += 1
    }
    array
  }
}

PS:我在 Scala 2.12 上重复这个测试:

Execute 'filling via Array.fill' in 118 ms
Execute 'filling via while' in 6 ms

Scala 3 中的问题...

PPS:在这种情况下,for (i &lt;- 0 until n) 以正常速度工作,时间与 Scala 2.12 相同。但在某些情况下,for 的运行速度比 while 慢 2 到 3 倍。

PPPS:对于谁认为它是随机的,它不是:100 次测试执行 516 秒(今天我的电脑更快),所以平均时间是如此之长。但无论如何,在某些程序中,一些代码块只执行一个,所以你不应该执行任何性能测试的平均时间。

更新: 我发现当val n 位于代码块之外时,执行时间大约慢了 1000 倍。但我不明白为什么。

您可以在 Scala 3 编译器的存储库中查看此问题的 cmets:https://github.com/lampepfl/dotty/issues/13819

【问题讨论】:

  • 在测量之前需要多次运行代码,以便运行时对其进行优化,否则结果毫无意义。
  • 我多次运行它。但是慢了 100 倍——这不是随机的。 8 秒对于 100 万次操作来说非常慢
  • 循环运行这 100 次。取平均值。或者,您可以使用 jmh 编写基准。
  • 使用 JMH。滚动您自己的基准代码不会给您带来有意义、可靠的结果。
  • 可能值得在 Scala 问题上添加与 cmets 相关的答案:基本上摆脱 App

标签: arrays scala optimization scala-3


【解决方案1】:

Scala 3 对这段代码做了一些非常奇怪的事情。

如果我在不改变逻辑的情况下将其重构为不同的结构,那么一切都会按预期进行(Array.fillwhile 慢一点)。

object Fill extends App {
  def timeMeasure[R](f: => R): (java.lang.Long, R) = {
    val startTime = System.nanoTime()
    val r = f
    val endTime = System.nanoTime()
    (endTime - startTime, r)
  }

  def warmup(): Unit =  {
    println("== warmup start =====================")
    for (i <- 0 to 10) {
      measureForN(1000000)
    }
    println("== warmup finish =====================")
  }

  def measureForN(n: Int): Unit = {
    val t1 = timeMeasure { Array.fill[Int](n)(10) }

    val t2 = timeMeasure({
      val array = new Array[Int](n)
      var i = 0
      while (i < n) {
        array(i) = 10
        i += 1
      }
      array
    })

    val t3 = timeMeasure({
      val array = new Array[Int](n)
      var i = 0
      while (i < n) {
        array(i) = i
        i += 1
      }
      array
    })

    // just to ensure actual array creations
    val length = List(t1._2.length, t2._2.length, t3._2.length).min

    println(s"n: ${n}, length: ${length}, fill: ${t1._1 / 1000} μs , while constant: ${t2._1 / 1000} μs, while changing: ${t3._1 / 1000} μs")
  }

  warmup()

  measureForN(10)
  measureForN(100)
  measureForN(1000)
  measureForN(10000)
  measureForN(100000)
  measureForN(1000000)
  measureForN(10000000)
  measureForN(100000000)
}

输出:

== warmup start =====================
n: 1000000, length: 1000000, fill: 23533 μs , while constant: 3804 μs, while changing: 3716 μs
n: 1000000, length: 1000000, fill: 7070 μs , while constant: 1606 μs, while changing: 1783 μs
n: 1000000, length: 1000000, fill: 3911 μs , while constant: 1497 μs, while changing: 1689 μs
n: 1000000, length: 1000000, fill: 3821 μs , while constant: 1543 μs, while changing: 1718 μs
n: 1000000, length: 1000000, fill: 3798 μs , while constant: 1510 μs, while changing: 1662 μs
n: 1000000, length: 1000000, fill: 3801 μs , while constant: 1524 μs, while changing: 1796 μs
n: 1000000, length: 1000000, fill: 3896 μs , while constant: 1541 μs, while changing: 1703 μs
n: 1000000, length: 1000000, fill: 3805 μs , while constant: 1486 μs, while changing: 1687 μs
n: 1000000, length: 1000000, fill: 3854 μs , while constant: 1606 μs, while changing: 1712 μs
n: 1000000, length: 1000000, fill: 3836 μs , while constant: 1509 μs, while changing: 1698 μs
n: 1000000, length: 1000000, fill: 3846 μs , while constant: 1553 μs, while changing: 1672 μs
== warmup finish =====================
n: 10, length: 10, fill: 3 μs , while constant: 0 μs, while changing: 0 μs
n: 100, length: 100, fill: 2 μs , while constant: 3 μs, while changing: 0 μs
n: 1000, length: 1000, fill: 6 μs , while constant: 1 μs, while changing: 2 μs
n: 10000, length: 10000, fill: 41 μs , while constant: 19 μs, while changing: 17 μs
n: 100000, length: 100000, fill: 378 μs , while constant: 156 μs, while changing: 170 μs
n: 1000000, length: 1000000, fill: 3764 μs , while constant: 1464 μs, while changing: 1676 μs
n: 10000000, length: 10000000, fill: 36976 μs , while constant: 15687 μs, while changing: 10860 μs
n: 100000000, length: 100000000, fill: 312242 μs , while constant: 190274 μs, while changing: 221980 μs

Edit :: 所需的更改就像不直接使用n 中的blocks 一样简单。

object Fill extends App {

  def timeMeasure[R](name: String)(block: => R): R = {
    val startTime = System.currentTimeMillis()
    val r = block
    val endTime = System.currentTimeMillis()
    println(s"Execute '$name' in ${endTime - startTime} ms")
    r
  }

  val n = 1000 * 1000 // * 10

  def measureFill(x: Int): Unit = {
    val ar1 = timeMeasure("filling via Array.fill") {
      Array.fill[Int](x)(10)
    }
  }

  def measureWhile(x: Int): Unit = {
    val ar2 = timeMeasure("filling via while") {
      val array = new Array[Int](x)
      var i = 0
      while (i < x) {
        array(i) = i
        i += 1
      }
      array
    }
  }

  println("== warmup ==================")
  measureFill(n)
  measureWhile(n)
  println("== warmup ==================")

  measureFill(n)
  measureWhile(n)

}

输出:

== warmup start ==================
Execute 'filling via Array.fill' in 26 ms
Execute 'filling via while' in 5 ms
== warmup finish ==================
Execute 'filling via Array.fill' in 6 ms
Execute 'filling via while' in 1 ms

Edit 2 :: 正如 Mikhail 所指出的,这是因为 n 的使用被编译为在生成的 Java 代码中使用方法 n()

object Test3 {

  val n = 1

  val k = n

}

正在编译到的,

//decompiled from Test3.class
public final class Test3 {
   public static int k() {
      return Test3$.MODULE$.k();
   }

   public static int n() {
      return Test3$.MODULE$.n();
   }
}

        //decompiled from Test3$.class
import java.io.Serializable;
import scala.runtime.ModuleSerializationProxy;

public final class Test3$ implements Serializable {
   private static final int n = 1;
   private static final int k;
   public static final Test3$ MODULE$ = new Test3$();

   private Test3$() {
   }

   static {
      k = MODULE$.n();
   }

   private Object writeReplace() {
      return new ModuleSerializationProxy(Test3$.class);
   }

   public int n() {
      return n;
   }

   public int k() {
      return k;
   }
}

但是,Scala 2.13.6 生成的 Java 代码几乎相同(只是 ScalaSignature 部分不同)。

//decompiled from Test3.class
import scala.reflect.ScalaSignature;

@ScalaSignature(
   bytes = "\u0006\u0005\r:Qa\u0002\u0005\t\u0002=1Q!\u0005\u0005\t\u0002IAQ!G\u0001\u0005\u0002iAqaG\u0001C\u0002\u0013\u0005A\u0004\u0003\u0004!\u0003\u0001\u0006I!\b\u0005\bC\u0005\u0011\r\u0011\"\u0001\u001d\u0011\u0019\u0011\u0013\u0001)A\u0005;\u0005)A+Z:ug)\u0011\u0011BC\u0001\ta\u0016\u0014X.\u00192be*\u00111\u0002D\u0001\u0005g\u0016\u0014\u0018NC\u0001\u000e\u0003\tiWm\u0001\u0001\u0011\u0005A\tQ\"\u0001\u0005\u0003\u000bQ+7\u000f^\u001a\u0014\u0005\u0005\u0019\u0002C\u0001\u000b\u0018\u001b\u0005)\"\"\u0001\f\u0002\u000bM\u001c\u0017\r\\1\n\u0005a)\"AB!osJ+g-\u0001\u0004=S:LGO\u0010\u000b\u0002\u001f\u0005\ta.F\u0001\u001e!\t!b$\u0003\u0002 +\t\u0019\u0011J\u001c;\u0002\u00059\u0004\u0013!A6\u0002\u0005-\u0004\u0003"
)
public final class Test3 {
   public static int k() {
      return Test3$.MODULE$.k();
   }

   public static int n() {
      return Test3$.MODULE$.n();
   }
}

        //decompiled from Test3$.class
public final class Test3$ {
   public static final Test3$ MODULE$ = new Test3$();
   private static final int n = 1;
   private static final int k;

   static {
      k = MODULE$.n();
   }

   public int n() {
      return n;
   }

   public int k() {
      return k;
   }

   private Test3$() {
   }
}

这意味着这是由 Scala 3 中的其他一些错误(导致 n() 产生这样的性能影响)引起的。

【讨论】:

  • 是的。尝试在measureForN 之外使用静态val n。例如,var n = 0; def measureForN(n0: Int): Unit = { n = n0。在这种情况下,您会看到非常不同的时序
  • 是的,这就是n
  • n 用于不同级别的嵌套时,这会产生更多变化。看起来像是 Scala 3 中的一个主要错误。
  • 实际上我向 Scala 3 编译器添加了问题:github.com/lampepfl/dotty/issues/13819
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-09
  • 1970-01-01
  • 2014-02-17
  • 1970-01-01
相关资源
最近更新 更多