Comment profiler les méthodes dans Scala?

117

Qu'est-ce qu'une méthode standard de profilage des appels de méthode Scala?

Ce dont j'ai besoin, ce sont des crochets autour d'une méthode que je peux utiliser pour démarrer et arrêter les minuteries.

En Java, j'utilise la programmation d'aspect, aspectJ, pour définir les méthodes à profiler et injecter du bytecode pour y parvenir.

Existe-t-il un moyen plus naturel dans Scala, où je peux définir un tas de fonctions à appeler avant et après une fonction sans perdre aucun typage statique dans le processus?

sheki
la source
Si AspectJ joue bien avec Scala, utilisez AspectJ. Pourquoi réinventer la roue? Les réponses ci-dessus qui utilisent le contrôle de flux personnalisé ne répondent pas aux exigences de base d'AOP car pour les utiliser, vous devez modifier votre code. Ceux-ci pourraient également être intéressants: java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-concerns-in-scala
Ant Kutschera
Qu'est-ce qui vous intéresse? Voulez-vous savoir combien de temps une méthode donnée prend dans l'environnement de production. Ensuite, vous devriez regarder les bibliothèques de métriques et ne pas rouler vous-même la mesure comme dans la réponse acceptée. Si vous voulez rechercher quelle variante de code est plus rapide "en général", c'est-à-dire dans votre environnement de développement, utilisez sbt-jmh comme présenté ci-dessous.
jmg

Réponses:

214

Voulez-vous faire cela sans changer le code pour lequel vous souhaitez mesurer les temps? Si cela ne vous dérange pas de changer le code, vous pouvez faire quelque chose comme ceci:

def time[R](block: => R): R = {
    val t0 = System.nanoTime()
    val result = block    // call-by-name
    val t1 = System.nanoTime()
    println("Elapsed time: " + (t1 - t0) + "ns")
    result
}

// Now wrap your method calls, for example change this...
val result = 1 to 1000 sum

// ... into this
val result = time { 1 to 1000 sum }
Jesper
la source
C'est super, puis-je faire la même chose sans aucun changement de code?
sheki
Pas automatiquement avec cette solution; Comment Scala pourrait-elle savoir ce que vous voudriez chronométrer?
Jesper
1
Ce n'est pas strictement vrai - vous pouvez automatiquement envelopper les choses dans le REPL
oxbow_lakes
1
Presque parfait, mais vous devez également réagir à d'éventuelles exceptions. Calculer t1dans une finallyclause
juanmirocks
2
Vous pouvez ajouter une étiquette à vos impressions avec un peu de curry: def time[R](label: String)(block: => R): R = {puis ajoutez l'étiquette auprintln
Glenn 'devalias'
34

En plus de la réponse de Jesper, vous pouvez automatiquement encapsuler les invocations de méthode dans le REPL:

scala> def time[R](block: => R): R = {
   | val t0 = System.nanoTime()
   | val result = block
   | println("Elapsed time: " + (System.nanoTime - t0) + "ns")
   | result
   | }
time: [R](block: => R)R

Maintenant - enveloppons quoi que ce soit dans ce

scala> :wrap time
wrap: no such command.  Type :help for help.

OK - nous devons être en mode puissance

scala> :power
** Power User mode enabled - BEEP BOOP SPIZ **
** :phase has been set to 'typer'.          **
** scala.tools.nsc._ has been imported      **
** global._ and definitions._ also imported **
** Try  :help,  vals.<tab>,  power.<tab>    **

Envelopper

scala> :wrap time
Set wrapper to 'time'

scala> BigDecimal("1.456")
Elapsed time: 950874ns
Elapsed time: 870589ns
Elapsed time: 902654ns
Elapsed time: 898372ns
Elapsed time: 1690250ns
res0: scala.math.BigDecimal = 1.456

Je ne sais pas pourquoi ce truc imprimé 5 fois

Mise à jour à partir de la 2.12.2:

scala> :pa
// Entering paste mode (ctrl-D to finish)

package wrappers { object wrap { def apply[A](a: => A): A = { println("running...") ; a } }}

// Exiting paste mode, now interpreting.


scala> $intp.setExecutionWrapper("wrappers.wrap")

scala> 42
running...
res2: Int = 42
oxbow_lakes
la source
8
Pour éviter à quiconque de se demander maintenant, la :wrapfonctionnalité a été supprimée de la REPL: - \
ches
25

Il existe trois bibliothèques d'analyse comparative pour Scala dont vous pouvez bénéficier.

Étant donné que les URL du site lié sont susceptibles de changer, je colle le contenu pertinent ci-dessous.

  1. SPerformance - Cadre de test de performance visant à comparer automatiquement les tests de performance et à travailler dans Simple Build Tool.

  2. scala-benchmarking-template - Projet de modèle SBT pour créer des (micro-) benchmarks Scala basés sur Caliper.

  3. Métriques - Capture des métriques au niveau de la JVM et de l'application. Alors tu sais ce qui se passe

manquant
la source
21

C'est ce que j'utilise:

import System.nanoTime
def profile[R](code: => R, t: Long = nanoTime) = (code, nanoTime - t)

// usage:
val (result, time) = profile { 
  /* block of code to be profiled*/ 
}

val (result2, time2) = profile methodToBeProfiled(foo)
pathikrit
la source
6

testing.Benchmark pourrait être utile.

scala> def testMethod {Thread.sleep(100)}
testMethod: Unit

scala> object Test extends testing.Benchmark {
     |   def run = testMethod
     | }
defined module Test

scala> Test.main(Array("5"))
$line16.$read$$iw$$iw$Test$     100     100     100     100     100
Luigi Plinge
la source
5
Sachez que testing.Benchmark est @deprecated ("Cette classe sera supprimée.", "2.10.0").
Tvaroh
5

J'ai pris la solution de Jesper et y ai ajouté une agrégation sur plusieurs exécutions du même code

def time[R](block: => R) = {
    def print_result(s: String, ns: Long) = {
      val formatter = java.text.NumberFormat.getIntegerInstance
      println("%-16s".format(s) + formatter.format(ns) + " ns")
    }

    var t0 = System.nanoTime()
    var result = block    // call-by-name
    var t1 = System.nanoTime()

    print_result("First Run", (t1 - t0))

    var lst = for (i <- 1 to 10) yield {
      t0 = System.nanoTime()
      result = block    // call-by-name
      t1 = System.nanoTime()
      print_result("Run #" + i, (t1 - t0))
      (t1 - t0).toLong
    }

    print_result("Max", lst.max)
    print_result("Min", lst.min)
    print_result("Avg", (lst.sum / lst.length))
}

Supposons que vous vouliez chronométrer deux fonctions counter_newet counter_old, ce qui suit est l'utilisation:

scala> time {counter_new(lst)}
First Run       2,963,261,456 ns
Run #1          1,486,928,576 ns
Run #2          1,321,499,030 ns
Run #3          1,461,277,950 ns
Run #4          1,299,298,316 ns
Run #5          1,459,163,587 ns
Run #6          1,318,305,378 ns
Run #7          1,473,063,405 ns
Run #8          1,482,330,042 ns
Run #9          1,318,320,459 ns
Run #10         1,453,722,468 ns
Max             1,486,928,576 ns
Min             1,299,298,316 ns
Avg             1,407,390,921 ns

scala> time {counter_old(lst)}
First Run       444,795,051 ns
Run #1          1,455,528,106 ns
Run #2          586,305,699 ns
Run #3          2,085,802,554 ns
Run #4          579,028,408 ns
Run #5          582,701,806 ns
Run #6          403,933,518 ns
Run #7          562,429,973 ns
Run #8          572,927,876 ns
Run #9          570,280,691 ns
Run #10         580,869,246 ns
Max             2,085,802,554 ns
Min             403,933,518 ns
Avg             797,980,787 ns

Espérons que cela soit utile

John Zhu
la source
4

J'utilise une technique facile à déplacer dans les blocs de code. Le point crucial est que la même ligne exacte commence et termine la minuterie - il s'agit donc vraiment d'un simple copier-coller. L'autre chose intéressante est que vous pouvez définir ce que le timing signifie pour vous comme une chaîne, le tout dans la même ligne.

Exemple d'utilisation:

Timelog("timer name/description")
//code to time
Timelog("timer name/description")

Le code:

object Timelog {

  val timers = scala.collection.mutable.Map.empty[String, Long]

  //
  // Usage: call once to start the timer, and once to stop it, using the same timer name parameter
  //
  def timer(timerName:String) = {
    if (timers contains timerName) {
      val output = s"$timerName took ${(System.nanoTime() - timers(timerName)) / 1000 / 1000} milliseconds"
      println(output) // or log, or send off to some performance db for analytics
    }
    else timers(timerName) = System.nanoTime()
  }

Avantages:

  • pas besoin d'envelopper le code sous forme de bloc ou de manipuler dans les lignes
  • peut facilement déplacer le début et la fin de la minuterie entre les lignes de code lors de l'exploration

Les inconvénients:

  • moins brillant pour un code totalement fonctionnel
  • de toute évidence, cet objet fuit des entrées de carte si vous ne "fermez" pas les temporisateurs, par exemple si votre code n'atteint pas la deuxième invocation pour un démarrage de temporisateur donné.
matanster
la source
C'est génial, mais l'utilisation ne devrait-elle pas être Timelog.timer("timer name/description"):?
schoon
4

ScalaMeter est une belle bibliothèque pour effectuer des analyses comparatives dans Scala

Voici un exemple simple

import org.scalameter._

def sumSegment(i: Long, j: Long): Long = (i to j) sum

val (a, b) = (1, 1000000000)

val execution_time = measure { sumSegment(a, b) }

Si vous exécutez l'extrait de code ci-dessus dans Scala Worksheet, vous obtenez le temps d'exécution en millisecondes

execution_time: org.scalameter.Quantity[Double] = 0.260325 ms
Dharmesh
la source
3

J'aime la simplicité de la réponse de @ wrick, mais je voulais aussi:

  • le profileur gère le bouclage (par souci de cohérence et de commodité)

  • synchronisation plus précise (en utilisant nanoTime)

  • temps par itération (pas le temps total de toutes les itérations)

  • retourne simplement ns / itération - pas un tuple

Ceci est réalisé ici:

def profile[R] (repeat :Int)(code: => R, t: Long = System.nanoTime) = { 
  (1 to repeat).foreach(i => code)
  (System.nanoTime - t)/repeat
}

Pour encore plus de précision, une simple modification permet une boucle de préchauffage JVM Hotspot (non chronométrée) pour chronométrer de petits extraits:

def profile[R] (repeat :Int)(code: => R) = {  
  (1 to 10000).foreach(i => code)   // warmup
  val start = System.nanoTime
  (1 to repeat).foreach(i => code)
  (System.nanoTime - start)/repeat
}
Brent Faust
la source
Ce n'est pas une réponse, il serait préférable de l'écrire en commentaire
nedim
1
@nedim La solution est donnée à la question - un wrapper pour tout ce que vous voulez chronométrer. Toutes les fonctions que l'OP voudrait appeler peuvent être placées dans le wrapper, ou dans le bloc qui appelle ses fonctions afin qu'il "puisse définir un tas de fonctions à appeler avant et après une fonction sans perdre aucun typage statique"
Brent Faust
1
Vous avez raison. Désolé, j'ai dû ignorer le code. Lorsque ma modification est examinée, je peux annuler le vote défavorable.
nedim
3

L'approche recommandée pour l'analyse comparative du code Scala est via sbt-jmh

"Ne faites confiance à personne, mettez tout au banc." - plugin sbt pour JMH (Java Microbenchmark Harness)

Cette approche est adoptée par de nombreux grands projets Scala, par exemple,

  • Le langage de programmation Scala lui-même
  • Dotty (Scala 3)
  • bibliothèque de chats pour la programmation fonctionnelle
  • Serveur de langage Metals pour les IDE

Le minuteur de wrapper simple basé sur System.nanoTimen'est pas une méthode fiable d'analyse comparative:

System.nanoTimeest aussi mauvais que String.internmaintenant: vous pouvez l'utiliser, mais utilisez-le à bon escient. Les effets de latence, de granularité et d'évolutivité introduits par les minuteries peuvent affecter et affecteront vos mesures si elles sont effectuées sans rigueur appropriée. C'est l'une des nombreuses raisons pour lesquelles System.nanoTimedevrait être abstraite des utilisateurs par des cadres d'analyse comparative

En outre, des considérations telles que le préchauffage JIT , le garbage collection, les événements à l'échelle du système, etc. peuvent introduire de l'imprévisibilité dans les mesures:

Des tonnes d'effets doivent être atténués, y compris le préchauffage, l'élimination du code mort, le forking, etc. Heureusement, JMH s'occupe déjà de beaucoup de choses et a des liaisons pour Java et Scala.

Basé sur la réponse de Travis Brown, voici un exemple de configuration du benchmark JMH pour Scala

  1. Ajouter jmh à project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. Activer le plugin jmh dans build.sbt
    enablePlugins(JmhPlugin)
  3. Ajouter à src/main/scala/bench/VectorAppendVsListPreppendAndReverse.scala

    package bench
    
    import org.openjdk.jmh.annotations._
    
    @State(Scope.Benchmark)
    @BenchmarkMode(Array(Mode.AverageTime))
    class VectorAppendVsListPreppendAndReverse {
      val size = 1_000_000
      val input = 1 to size
    
      @Benchmark def vectorAppend: Vector[Int] = 
        input.foldLeft(Vector.empty[Int])({ case (acc, next) => acc.appended(next)})
    
      @Benchmark def listPrependAndReverse: List[Int] = 
        input.foldLeft(List.empty[Int])({ case (acc, next) => acc.prepended(next)}).reverse
    }
  4. Exécuter un benchmark avec
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

Les résultats sont

Benchmark                                                   Mode  Cnt  Score   Error  Units
VectorAppendVsListPreppendAndReverse.listPrependAndReverse  avgt   20  0.024 ± 0.001   s/op
VectorAppendVsListPreppendAndReverse.vectorAppend           avgt   20  0.130 ± 0.003   s/op

ce qui semble indiquer le préfixe à a List, puis l'inverser à la fin est d'un ordre de grandeur plus rapide que de continuer à ajouter à a Vector.

Mario Galic
la source
1

Tout en se tenant sur les épaules de géants ...

Une bibliothèque tierce solide serait plus idéale, mais si vous avez besoin de quelque chose de rapide et basé sur une bibliothèque std, la variante suivante fournit:

  • Répétitions
  • Le dernier résultat l'emporte sur plusieurs répétitions
  • Temps total et temps moyen pour plusieurs répétitions
  • Supprime le besoin de fournisseur de temps / instant en tant que paramètre

.

import scala.concurrent.duration._
import scala.language.{postfixOps, implicitConversions}

package object profile {

  def profile[R](code: => R): R = profileR(1)(code)

  def profileR[R](repeat: Int)(code: => R): R = {
    require(repeat > 0, "Profile: at least 1 repetition required")

    val start = Deadline.now

    val result = (1 until repeat).foldLeft(code) { (_: R, _: Int) => code }

    val end = Deadline.now

    val elapsed = ((end - start) / repeat)

    if (repeat > 1) {
      println(s"Elapsed time: $elapsed averaged over $repeat repetitions; Total elapsed time")

      val totalElapsed = (end - start)

      println(s"Total elapsed time: $totalElapsed")
    }
    else println(s"Elapsed time: $elapsed")

    result
  }
}

Il convient également de noter que vous pouvez utiliser la Duration.toCoarsestméthode pour convertir la plus grande unité de temps possible, bien que je ne sois pas sûr à quel point cela est convivial avec un décalage horaire mineur entre les exécutions, par exemple

Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_60).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import scala.concurrent.duration._
import scala.concurrent.duration._

scala> import scala.language.{postfixOps, implicitConversions}
import scala.language.{postfixOps, implicitConversions}

scala> 1000.millis
res0: scala.concurrent.duration.FiniteDuration = 1000 milliseconds

scala> 1000.millis.toCoarsest
res1: scala.concurrent.duration.Duration = 1 second

scala> 1001.millis.toCoarsest
res2: scala.concurrent.duration.Duration = 1001 milliseconds

scala> 
Darren Bishop
la source
1

Vous pouvez utiliser System.currentTimeMillis:

def time[R](block: => R): R = {
    val t0 = System.currentTimeMillis()
    val result = block    // call-by-name
    val t1 = System.currentTimeMillis()
    println("Elapsed time: " + (t1 - t0) + "ms")
    result
}

Usage:

time{
    //execute somethings here, like methods, or some codes.
}  

nanoTime vous montrera ns, donc il sera difficile de voir. Je suggère donc que vous puissiez utiliser currentTimeMillis à la place.

haiyang
la source
Le fait que les nanosecondes soient difficiles à voir est une mauvaise raison de choisir entre les deux. Il existe des différences importantes en plus de la résolution. D'une part, currentTimeMillis peut changer et même revenir en arrière lors des ajustements d'horloge que le système d'exploitation effectue périodiquement. Une autre est que nanoTime n'est peut-être pas thread-safe: stackoverflow.com/questions/351565/…
Chris