【问题标题】:The cost of nested methods嵌套方法的成本
【发布时间】:2011-01-29 18:04:30
【问题描述】:

在 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
  }
}

由于我应该注意嵌套方法定义,是否存在任何运行时成本?

闭包的答案是否不同?

【问题讨论】:

  • 如果您对 Q 投了反对票,请善待并留下评论,解释为什么...

标签: scala methods


【解决方案1】:

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

此外,当您在没有参数列表的情况下引用这些方法之一时,它会被提升为函数。所以map(countOccurences) 和写map((a: (String, List[PersonRecord])) => countOccurences(a)) 是一样的。这个匿名函数被编译成一个单独的类AggregatedPerson$$anonfun$mostFrequentName$2,它只是转发到countOccurences$

附带说明,将方法提升为函数的过程称为 Eta 扩展。如果您在预期函数类型的上下文中省略参数列表(如您的示例),或者如果您使用 _ 代替整个参数列表或代替每个参数(val f1 = countOccurences _ ; val f2 = countOccurences(_) .

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

我发现它是一个非常有用的构建代码的工具,并且认为您的示例非常符合 Scala 的习惯。

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

val a = {
   val temp1, temp2 = ...
   f(temp1, temp2)
}

您可以使用scalac -print 准确了解如何将 Scala 代码转换为可用于 JVM 的形式。这是您程序的输出:

[[syntax trees at end of cleanup]]// Scala source: nested-method.scala
package <empty> {

  class AggregatedPerson extends scala.collection.mutable.HashSet with ScalaObject {
    def mostFrequentName(): java.lang.String = AggregatedPerson.this.iterator().toList().groupBy({
      (new AggregatedPerson$$anonfun$mostFrequentName$1(AggregatedPerson.this): Function1)
    }).map({
      {
        (new AggregatedPerson$$anonfun$mostFrequentName$2(AggregatedPerson.this): Function1)
      }
    }, collection.this.Map.canBuildFrom()).$asInstanceOf[scala.collection.MapLike]().iterator().toList().sortWith({
      {
        (new AggregatedPerson$$anonfun$mostFrequentName$3(AggregatedPerson.this): Function2)
      }
    }).$asInstanceOf[scala.collection.IterableLike]().head().$asInstanceOf[Tuple2]()._1().$asInstanceOf[java.lang.String]();
    final def moreFirst$1(a: Tuple2, b: Tuple2): Boolean = scala.Int.unbox(a._2()).>(scala.Int.unbox(b._2()));
    final def countOccurrences$1(nameGroup: Tuple2): Tuple2 = new Tuple2(nameGroup._1(), scala.Int.box(nameGroup._2().$asInstanceOf[scala.collection.SeqLike]().size()));
    def this(): AggregatedPerson = {
      AggregatedPerson.super.this();
      ()
    }
  };

  @SerialVersionUID(0) @serializable final <synthetic> class AggregatedPerson$$anonfun$mostFrequentName$1 extends scala.runtime.AbstractFunction1 {
    final def apply(x$1: PersonRecord): java.lang.String = x$1.fullName();
    final <bridge> def apply(v1: java.lang.Object): java.lang.Object = AggregatedPerson$$anonfun$mostFrequentName$1.this.apply(v1.$asInstanceOf[PersonRecord]());
    def this($outer: AggregatedPerson): AggregatedPerson$$anonfun$mostFrequentName$1 = {
      AggregatedPerson$$anonfun$mostFrequentName$1.super.this();
      ()
    }
  };

  @SerialVersionUID(0) @serializable final <synthetic> class AggregatedPerson$$anonfun$mostFrequentName$2 extends scala.runtime.AbstractFunction1 {
    final def apply(nameGroup: Tuple2): Tuple2 = AggregatedPerson$$anonfun$mostFrequentName$2.this.$outer.countOccurrences$1(nameGroup);
    <synthetic> <paramaccessor> private[this] val $outer: AggregatedPerson = _;
    final <bridge> def apply(v1: java.lang.Object): java.lang.Object = AggregatedPerson$$anonfun$mostFrequentName$2.this.apply(v1.$asInstanceOf[Tuple2]());
    def this($outer: AggregatedPerson): AggregatedPerson$$anonfun$mostFrequentName$2 = {
      if ($outer.eq(null))
        throw new java.lang.NullPointerException()
      else
        AggregatedPerson$$anonfun$mostFrequentName$2.this.$outer = $outer;
      AggregatedPerson$$anonfun$mostFrequentName$2.super.this();
      ()
    }
  };
  @SerialVersionUID(0) @serializable final <synthetic> class AggregatedPerson$$anonfun$mostFrequentName$3 extends scala.runtime.AbstractFunction2 {
    final def apply(a: Tuple2, b: Tuple2): Boolean = AggregatedPerson$$anonfun$mostFrequentName$3.this.$outer.moreFirst$1(a, b);
    <synthetic> <paramaccessor> private[this] val $outer: AggregatedPerson = _;
    final <bridge> def apply(v1: java.lang.Object, v2: java.lang.Object): java.lang.Object = scala.Boolean.box(AggregatedPerson$$anonfun$mostFrequentName$3.this.apply(v1.$asInstanceOf[Tuple2](), v2.$asInstanceOf[Tuple2]()));
    def this($outer: AggregatedPerson): AggregatedPerson$$anonfun$mostFrequentName$3 = {
      if ($outer.eq(null))
        throw new java.lang.NullPointerException()
      else
        AggregatedPerson$$anonfun$mostFrequentName$3.this.$outer = $outer;
      AggregatedPerson$$anonfun$mostFrequentName$3.super.this();
      ()
    }
  }
}

【讨论】:

    【解决方案2】:

    运行时成本很小。你可以在这里观察它(对长代码表示歉意):

    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 循环(“原始”)
    • while 循环内部方法是 sum 变量的闭包
    • while 循环返回部分和的内部方法
    • while-and-sum 嵌套函数

    我们在我的机器上看到了内部循环中以纳秒为单位的结果:

    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。)

    【讨论】:

      【解决方案3】:

      截至 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)
      

      标量

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-03-28
        • 1970-01-01
        相关资源
        最近更新 更多