Le coût des méthodes nestedes

Dans Scala, on pourrait définir des méthodes à l’intérieur d’autres méthodes. Cela limite leur champ d’utilisation à l’intérieur du bloc de définition. Je les utilise pour améliorer la lisibilité du code qui utilise plusieurs fonctions d’ordre supérieur. Contrairement aux littéraux de fonctions anonymes, cela me permet de leur donner des noms significatifs avant de les transmettre.

Par exemple:

class AggregatedPerson extends HashSet[PersonRecord] { def mostFrequentName: Ssortingng = { type NameCount = (Ssortingng, Int) def moreFirst(a: NameCount, b: NameCount) = a._2 > b._2 def countOccurrences(nameGroup: (Ssortingng, List[PersonRecord])) = (nameGroup._1, nameGroup._2.size) iterator.toList.groupBy(_.fullName). map(countOccurrences).iterator.toList. sortWith(moreFirst).head._1 } } 

Y a-t-il un coût d’exécution à cause de la définition de la méthode nestede dont je devrais être conscient?

La réponse est-elle différente pour les fermetures?

Au cours de la compilation, les fonctions nestedes sont moveFirst et countOccurences sont déplacées au même niveau que mostFrequentName . Ils obtiennent des noms synthétisés par le compilateur: moveFirst$1 et countOccurences$1 .

De plus, lorsque vous vous référez à l’une de ces méthodes sans liste d’arguments, elle devient une fonction. Donc, map(countOccurences) est identique à écrire une map((a: (Ssortingng, List[PersonRecord])) => countOccurences(a)) . Cette fonction anonyme est compilée dans une classe distincte AggregatedPerson$$anonfun$mostFrequentName$2 , qui ne fait que transmettre à countOccurences$ .

En parallèle, le processus de levée de la méthode à une fonction s’appelle Eta Expansion. Il est déclenché si vous omettez la liste d’arguments dans un contexte où un type de fonction est attendu (comme dans votre exemple) ou si vous utilisez _ à la place de la liste complète des arguments ou à la place de chaque argument ( val f1 = countOccurences _ ; val f2 = countOccurences(_) .

Si le code était directement dans la fermeture, vous auriez un appel de méthode en moins dans votre stack et une méthode de synthèse moins générée. L’impact sur la performance de ce système sera probablement nul dans la plupart des cas.

Je trouve que c’est un outil extrêmement utile pour structurer le code et considérer votre exemple comme Scala très idiomatique.

Un autre outil utile consiste à utiliser de petits blocs pour initialiser un val:

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

Vous pouvez utiliser scalac -print pour voir exactement comment le code Scala est traduit dans un formulaire prêt pour la JVM. Voici le résultat de votre programme:

 [[syntax trees at end of cleanup]]// Scala source: nested-method.scala package  { class AggregatedPerson extends scala.collection.mutable.HashSet with ScalaObject { def mostFrequentName(): java.lang.Ssortingng = 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.Ssortingng](); 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  class AggregatedPerson$$anonfun$mostFrequentName$1 extends scala.runtime.AbstractFunction1 { final def apply(x$1: PersonRecord): java.lang.Ssortingng = x$1.fullName(); final  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  class AggregatedPerson$$anonfun$mostFrequentName$2 extends scala.runtime.AbstractFunction1 { final def apply(nameGroup: Tuple2): Tuple2 = AggregatedPerson$$anonfun$mostFrequentName$2.this.$outer.countOccurrences$1(nameGroup);   private[this] val $outer: AggregatedPerson = _; final  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  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);   private[this] val $outer: AggregatedPerson = _; final  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(); () } } } 

Il y a un petit coût d’exécution. Vous pouvez l’observer ici (excuses pour le code long):

 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[Ssortingng]) { 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) }) } } } 

Il existe quatre méthodes différentes pour la sum des nombres entiers:

  • Une boucle while crue (“raw”)
  • Alors que les méthodes internes de boucle qui sont des fermetures sur la variable sum
  • Bien que les méthodes internes de boucle qui renvoient la sum partielle
  • Une fonction nestede tout en sum

Et nous voyons les résultats sur ma machine en termes de nanosecondes sockets dans la boucle interne:

 scala> NestBench.main(Array[Ssortingng]()) 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 

Donc, la ligne de fond est la suivante: les fonctions d’imbrication ne vous font vraiment pas de mal dans des cas simples – la JVM comprendra que l’appel peut être intégré (donc raw et inner donnent les mêmes heures). Si vous optez pour une approche plus fonctionnelle, l’appel de la fonction ne peut pas être complètement négligé, mais le temps nécessaire est extrêmement réduit (environ 0,4 ns supplémentaire par appel). Si vous utilisez beaucoup de fermetures, les fermer donne une surcharge de 1 ns par appel au moins dans le cas où une seule variable mutable est écrite.

Vous pouvez modifier le code ci-dessus pour trouver des réponses à d’autres questions, mais l’essentiel est que tout est très rapide, allant de “pas de peine que ce soit” à ne s’inquiéter que dans les boucles internes les plus serrées. faire”.

(PS A titre de comparaison, la création d’un petit object unique prend environ 4 ns sur ma machine.)

En date de janvier 2014

Le benchmark actuel a environ 3 ans et Hotspot et le compilateur ont considérablement évolué. J’utilise également Google Caliper pour effectuer les tests de performances.

Code

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

Résultats

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

Ce qui est très surprenant, c’est que le test de fermeture a été effectué sur 4ns plus rapidement que les autres. Cela semble être une idiosyncrasie de Hotspot au lieu de l'environnement d'exécution, plusieurs exécutions ont renvoyé la même tendance.

Utiliser une fermeture qui exécute la boxe est un énorme problème de performance, il faut environ 3,579 ns pour effectuer un déballage et un reboxing, assez pour faire beaucoup d'opérations mathématiques primitives. Dans cette position spécifique, les choses pourraient s'améliorer avec le travail effectué sur un nouvel optimiseur . Dans le cas général, la boxe pourrait être atténuée par le miniboxing .

Edit: Le nouvel optimiseur n'aide pas vraiment ici, il rend la Closure 0.1 ns plus lente et Boxed 0.1 ns plus rapide:

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

Interprété avec 2.11.0-20131209-220002-9587726041 de magarciaEPFL / scala

Des versions

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 comstackr version 2.10.3 -- Copyright 2002-2013, LAMP/EPFL