【问题标题】:Time taken by a while loop and recursionwhile 循环和递归所花费的时间
【发布时间】:2018-10-05 12:54:01
【问题描述】:

我不是在问我应该使用递归还是迭代,或者它们之间哪个更快。我试图了解迭代和递归所花费的时间,我想出了一个有趣的模式,即两者所花费的时间,即文件顶部的内容比另一个花费更多的时间。

例如:如果我在一开始就编写 for 循环,则它比递归花费更多时间,反之亦然。这两个过程所用时间之间的差异非常大,大约是 30 到 40 倍。

我的问题是:-

  1. 循环的顺序和递归是否重要?
  2. 有什么和印刷有关的吗?
  3. 这种行为的可能原因是什么?

以下是我在同一个文件中的代码,我使用的语言是 scala?

  def count(x: Int): Unit = {
    if (x <= 1000) {
      print(s"$x ")
      count(x + 1)
    }
  }

  val t3 = System.currentTimeMillis()
  count(1)
  val t4 = System.currentTimeMillis()
  println(s"\ntime taken by the recursion look = ${t4 - t3} mili second")

  var c = 1

  val t1 = System.currentTimeMillis()
  while(c <= 1000)
    {
      print(s"$c ")
      c+=1
    }
  val t2 = System.currentTimeMillis()

  println(s"\ntime taken by the while loop = ${t2 - t1} mili second")

在这种情况下,递归和while循环花费的时间分别是986ms和20ms。

当我切换循环和递归的位置时,即先循环后递归,递归和 while 循环的时间分别为 1.69 秒和 28 毫秒。

编辑 1: 如果递归代码在顶部,我可以看到与 bufferWriter 相同的行为。但当递归低于循环时,情况并非如此。当递归低于循环时,它所花费的时间几乎相同,相差 2 到 3 毫秒。

【问题讨论】:

  • 这个问题没有一般的答案,因为编译器也很神奇,有时递归函数会展开到循环中(参见尾递归)。
  • @AndreasNeumann 那么它取决于哪件事?你能告诉我吗?
  • 使用BufferedWriter时也存在时间差异。因此,我们不能为此责怪 print 或 println。

标签: scala performance recursion tail-recursion


【解决方案1】:

如果您想在不依赖任何分析工具的情况下说服自己 tailrec 优化有效,您可以尝试以下方法:

  • 使用更多的迭代方式
  • 放弃最初的几次迭代,让 JIT 有时间唤醒并进行热点优化
  • 抛弃所有不可预知的副作用,例如打印到标准输出
  • 抛弃所有在这两种方法中相同的昂贵操作(格式化数字等)
  • 多轮测量
  • 随机化每轮的重复次数
  • 在每一轮中随机化变体的顺序,以避免与垃圾收集器的循环发生任何“灾难性共振”
  • 最好不要在计算机上运行其他任何东西

类似的东西:

def compare(
  xs: Array[(String, () => Unit)],
  maxRepsPerBlock: Int = 10000,
  totalRounds: Int = 100000,
  warmupRounds: Int = 1000
): Unit = {
  val n = xs.size
  val times: Array[Long] = Array.ofDim[Long](n)
  val rng = new util.Random
  val indices = (0 until n).toList
  var totalReps: Long = 0

  for (round <- 1 to totalRounds) {
    val order = rng.shuffle(indices)
    val reps = rng.nextInt(maxRepsPerBlock / 2) + maxRepsPerBlock / 2
    for (i <- order) {
      var r = 0
      while (r < reps) {
        r += 1
        val start = System.currentTimeMillis
        (xs(i)._2)()
        val end = System.currentTimeMillis
        if (round > warmupRounds) {
          times(i) += (end - start)
        }
      }
    }
    if (round > warmupRounds) {
      totalReps += reps
    }
  }

  for (i <- 0 until n) {
    println(f"${xs(i)._1}%20s : ${times(i) / totalReps.toDouble}")
  }
}

def gaussSumWhile(n: Int): Long = {
  var acc: Long = 0
  var i = 0
  while (i <= n) {
    acc += i
    i += 1
  }
  acc
}

@annotation.tailrec
def gaussSumRec(n: Int, acc: Long = 0, i: Int = 0): Long = {
  if (i <= n) gaussSumRec(n, acc + i, i + 1)
  else acc 
}

compare(Array(
  ("while", { () => gaussSumWhile(1000) }),
  ("@tailrec", { () => gaussSumRec(1000) })
))

这是打印的内容:

           while : 6.737733046257334E-5
        @tailrec : 6.70325653896487E-5

即使是上面的简单提示也足以创建一个基准,表明 while 循环和尾递归函数大致在同一时间。

【讨论】:

    【解决方案2】:

    Scala 不会编译成机器码,而是编译成“Java 虚拟机”(JVM) 的字节码,然后在本机处理器上解释该代码。 JVM 使用多种机制来优化频繁运行的代码,最终将频繁调用的函数(“热点”)转化为纯机器码。

    这意味着测试函数的第一次运行并不能很好地衡量最终的性能。在尝试测量所用时间之前,您需要通过多次运行测试代码来“预热”JIT 编译器。

    此外,正如 cmets 中所述,执行任何类型的 I/O 都会使时序变得非常不可靠,因为存在 I/O 阻塞的危险。如果可能,编写一个不做任何阻塞的测试用例。

    【讨论】:

      猜你喜欢
      • 2019-07-06
      • 1970-01-01
      • 2013-08-12
      • 2021-06-05
      • 1970-01-01
      • 2020-06-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多