嵌套方法的成本

时间:2010-03-20 07:38:40

标签: scala methods

在Scala中,可以在其他方法中定义方法。这将它们的使用范围限制在定义块内。我使用它们来提高使用几个高阶函数的代码的可读性。与匿名函数文字相比,这允许我在传递它们之前给它们有意义的名称。

例如:

class AggregatedPerson extends HashSet[PersonRecord] {
  def mostFrequentName: String = {
    type NameCount = (String, Int)
    def moreFirst(a: NameCount, b: NameCount) = a._2 > b._2
    def countOccurrences(nameGroup: (String, List[PersonRecord])) =
      (nameGroup._1, nameGroup._2.size) 

    iterator.toList.groupBy(_.fullName).
      map(countOccurrences).iterator.toList.
      sortWith(moreFirst).head._1
  }
}

是否有任何运行时成本,因为我应该注意嵌套方法定义?

闭包的答案有何不同?

3 个答案:

答案 0 :(得分:56)

在compilaton期间,嵌套函数moveFirstcountOccurences移出到与mostFrequentName相同的级别。它们得到编译器合成的名称:moveFirst$1countOccurences$1

此外,当您在没有参数列表的情况下引用其中一种方法时,它会被提升为一个函数。因此map(countOccurences)与撰写map((a: (String, List[PersonRecord])) => countOccurences(a))相同。这个匿名函数被编译为一个单独的类AggregatedPerson$$anonfun$mostFrequentName$2,它除了转发到countOccurences$之外什么都没有。

作为旁注,将方法提升为函数的过程称为Eta Expansion。如果在期望函数类型的上下文中省略参数列表(如在您的示例中),或者使用_代替整个参数列表,或者使用每个参数({ {1}}。

如果代码直接在闭包中,那么堆栈中的方法调用将减少一个,并且生成的合成方法会减少一个。在大多数情况下,这对性能的影响可能为零。

我发现它是构建代码的一个非常有用的工具,并认为你的例子非常惯用Scala。

另一个有用的工具是使用小块来初始化val:

val f1 = countOccurences _ ; val f2 = countOccurences(_)

您可以使用val a = { val temp1, temp2 = ... f(temp1, temp2) } 确切地了解Scala代码如何转换为为JVM准备好的表单。下面是程序的输出:

scalac -print

答案 1 :(得分:14)

运行成本很低。你可以在这里观察它(为长代码道歉):

object NestBench {
  def countRaw() = {
    var sum = 0
    var i = 0
    while (i<1000) {
      sum += i
      i += 1
      var j = 0
      while (j<1000) {
        sum += j
        j += 1
        var k = 0
        while (k<1000) {
          sum += k
          k += 1
          sum += 1
        }
      }
    }
    sum
  }
  def countClosure() = {
    var sum = 0
    var i = 0
    def sumI {
      sum += i
      i += 1
      var j = 0
      def sumJ {
        sum += j
        j += 1
        var k = 0
        def sumK {
          def sumL { sum += 1 }
          sum += k
          k += 1
          sumL
        }
        while (k<1000) sumK
      }
      while (j<1000) sumJ
    }
    while (i<1000) sumI
    sum
  }
  def countInner() = {
    var sum = 0
    def whileI = {
      def whileJ = {
        def whileK = {
          def whileL() = 1
          var ksum = 0
          var k = 0
          while (k<1000) { ksum += k; k += 1; ksum += whileL }
          ksum
        }
        var jsum = 0
        var j = 0
        while (j<1000) {
          jsum += j; j += 1
          jsum += whileK
        }
        jsum
      }
      var isum = 0
      var i = 0
      while (i<1000) {
        isum += i; i += 1
        isum += whileJ
      }
      isum
    }
    whileI
  }
  def countFunc() = {
    def summer(f: => Int)() = {
      var sum = 0
      var i = 0
      while (i<1000) {
        sum += i; i += 1
        sum += f
      }
      sum
    }
    summer( summer( summer(1) ) )()
  }
  def nsPerIteration(f:() => Int): (Int,Double) = {
    val t0 = System.nanoTime
    val result = f()
    val t1 = System.nanoTime
    (result , (t1-t0)*1e-9)
  }
  def main(args: Array[String]) {
    for (i <- 1 to 5) {
      val fns = List(countRaw _, countClosure _, countInner _, countFunc _)
      val labels = List("raw","closure","inner","func")
      val results = (fns zip labels) foreach (fl => {
        val x = nsPerIteration( fl._1 )
        printf("Method %8s produced %d; time/it = %.3f ns\n",fl._2,x._1,x._2)
      })
    }
  }
}

有四种不同的求和方法:

  • 原始while循环(“raw”)
  • while循环内部方法是和变量
  • 的闭包
  • while循环内部方法返回部分和
  • 一个求和的嵌套函数

我们在内部循环中以纳秒的形式看到我机器上的结果:

scala> NestBench.main(Array[String]())
Method      raw produced -1511174132; time/it = 0.422 ns
Method  closure produced -1511174132; time/it = 2.376 ns
Method    inner produced -1511174132; time/it = 0.402 ns
Method     func produced -1511174132; time/it = 0.836 ns
Method      raw produced -1511174132; time/it = 0.418 ns
Method  closure produced -1511174132; time/it = 2.410 ns
Method    inner produced -1511174132; time/it = 0.399 ns
Method     func produced -1511174132; time/it = 0.813 ns
Method      raw produced -1511174132; time/it = 0.411 ns
Method  closure produced -1511174132; time/it = 2.372 ns
Method    inner produced -1511174132; time/it = 0.399 ns
Method     func produced -1511174132; time/it = 0.813 ns
Method      raw produced -1511174132; time/it = 0.411 ns
Method  closure produced -1511174132; time/it = 2.370 ns
Method    inner produced -1511174132; time/it = 0.399 ns
Method     func produced -1511174132; time/it = 0.815 ns
Method      raw produced -1511174132; time/it = 0.412 ns
Method  closure produced -1511174132; time/it = 2.357 ns
Method    inner produced -1511174132; time/it = 0.400 ns
Method     func produced -1511174132; time/it = 0.817 ns

所以,底线是:在简单的情况下,嵌套函数根本不会伤害到你 - JVM会发现调用可以内联(因此rawinner给出了同一时间)。如果采用更多功能方法,则函数调用不能完全忽略,但所花费的时间非常小(每次调用大约0.4 ns)。如果你使用了很多闭包,那么关闭它们会产生每次调用1 ns的开销,至少在这种情况下写入一个可变变量。

你可以修改上面的代码来找到其他问题的答案,但最重要的是它是非常快的,介于“无任何惩罚”到“只担心在非常严格的内部循环中,否则有最小的工作要做“。

(P.S。为了比较,在我的机器上创建一个小对象需要大约4 ns。)

答案 2 :(得分:7)

截至2014年1月的当前

目前的基准测试已经有3年的历史了,Hotspot和编译器已经有了很大的发展。我也使用Google Caliper来执行基准测试。

代码

import com.google.caliper.SimpleBenchmark

class Benchmark extends SimpleBenchmark {
    def timeRaw(reps: Int) = {
        var i = 0
        var result = 0L
        while (i < reps) {
            result += 0xc37e ^ (i * 0xd5f3)
            i = i + 1
        }
        result
    }

    def normal(i: Int): Long = 0xc37e ^ (i * 0xd5f3)
    def timeNormal(reps: Int) = {
        var i = 0
        var result = 0L
        while (i < reps) {
            result += normal(i)
            i = i + 1
        }
        result
    }

    def timeInner(reps: Int) = {
        def inner(i: Int): Long = 0xc37e ^ (i * 0xd5f3)

        var i = 0
        var result = 0L
        while (i < reps) {
            result += inner(i)
            i = i + 1
        }
        result
    }

    def timeClosure(reps: Int) = {
        var i = 0
        var result = 0L
        val closure = () => result += 0xc37e ^ (i * 0xd5f3)
        while (i < reps) {
            closure()
            i = i + 1
        }
        result
    }

    def normal(i: Int, j: Int, k: Int, l: Int): Long = i ^ j ^ k ^ l 
    def timeUnboxed(reps: Int) = {
        var i = 0
        var result = 0L
        while (i < reps) {
            result += normal(i,i,i,i)
            i = i + 1
        }
        result
    }

    val closure = (i: Int, j: Int, k: Int, l: Int) => (i ^ j ^ k ^ l).toLong 
    def timeBoxed(reps: Int) = {
        var i = 0
        var result = 0L
        while (i < reps) {
            closure(i,i,i,i)
            i = i + 1
        }
        result
    }
}

结果

benchmark     ns linear runtime
   Normal  0.576 =
      Raw  0.576 =
    Inner  0.576 =
  Closure  0.532 =
  Unboxed  0.893 =
    Boxed 15.210 ==============================

令人惊讶的是,闭合测试比其他测试完成快约4ns。这似乎是Hotspot的特质而不是执行环境,多次运行都返回了相同的趋势。

使用执行装箱的闭包是一个巨大的性能打击,执行一次拆箱和重新装箱需要大约3.579ns,足以进行大量的原始数学运算。在这个特定的位置,在new optimizer上完成工作可能会有所改善。在一般情况下,拳击可能会被miniboxing缓解。

修改 新的优化器在这里没有用,它使Closure慢0.1 ns,Boxed 0.1 ns更快:

 benchmark     ns linear runtime
       Raw  0.574 =
    Normal  0.576 =
     Inner  0.575 =
   Closure  0.645 =
   Unboxed  0.889 =
     Boxed 15.107 ==============================

从magarciaEPFL / scala执行2.11.0-20131209-220002-9587726041

版本

JVM

java version "1.7.0_51"
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)

Scalac

Scala compiler version 2.10.3 -- Copyright 2002-2013, LAMP/EPFL