Стоимость вложенных методов

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

Там какое-либо время выполнения стоится из-за вложенного определения метода, о котором я должен знать?

Ответ отличается для закрытий?

48
задан Palimondo 20 March 2010 в 07:38
поделиться

2 ответа

Во время компиляции вложенные функции moveFirst и countOccurences перемещаются на тот же уровень, что и ] mostFrequentName . Они получают имена, синтезированные компилятором: moveFirst $ 1 и countOccurences $ 1 .

Кроме того, когда вы ссылаетесь на один из этих методов без списка аргументов, он превращается в функцию. Итак, map (countOccurences) - это то же самое, что и запись map ((a: (String, List [PersonRecord])) => countOccurences (a)) . Эта анонимная функция скомпилирована в отдельный класс AggregatedPerson $$ anonfun $ mostFrequentName $ 2 , который не выполняет ничего, кроме пересылки в countOccurences $ .

Кстати, процесс преобразования метода в функцию называется Eta Expansion. Он запускается, если вы опускаете список аргументов в контексте, где ожидается тип функции (как в вашем примере), или если вы используете _ вместо всего списка аргументов или вместо каждого аргумента ( 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();
      ()
    }
  }
}
57
ответ дан 26 November 2019 в 18:53
поделиться

Это небольшая стоимость времени выполнения. Вы можете наблюдать это здесь (извинения за длинный код):

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, которые являются замыканиями по сумме переменная
  • Внутренние методы цикла 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 определит, что вызов может быть встроен (таким образом, raw и inner дают то же самое раз). Если вы выберете более функциональный подход, вызовом функции нельзя полностью пренебречь, но затрачиваемое время исчезающе мало (примерно 0,4 нс на каждый вызов). Если вы используете много замыканий, то их закрытие приводит к накладным расходам примерно в 1 нс на вызов, по крайней мере, в этом случае, когда записывается одна изменяемая переменная.

Вы можете изменить приведенный выше код, чтобы найти ответы на другие вопросы, но суть в том, что все происходит очень быстро, от «без штрафа вообще» до «беспокоиться только о очень узких внутренних циклах, которые в противном случае минимальная работа ".

(PS. Для сравнения, создание одного небольшого объекта на моей машине занимает ~ 4 нс.)

14
ответ дан 26 November 2019 в 18:53
поделиться
Другие вопросы по тегам:

Похожие вопросы: