web-dev-qa-db-fra.com

Comment profiler les méthodes dans Scala?

Qu'est-ce qu'une méthode standard de profilage 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 des minuteries.

Dans Java j'utilise la programmation d'aspects, aspectJ, pour définir les méthodes à profiler et injecter du bytecode pour obtenir le même résultat.

Existe-t-il une méthode plus naturelle dans Scala, dans laquelle je peux définir un ensemble de fonctions à appeler avant et après une fonction sans perdre de typage statique dans le processus?

114
sheki

Voulez-vous faire cela sans changer le code pour lequel vous voulez mesurer les timings? 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 }
205
Jesper

En plus de la réponse de Jesper, vous pouvez encapsuler automatiquement les invocations de méthodes 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, passons à autre chose

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 ces trucs imprimés sont sortis 5 fois

Mise à jour à partir de 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
33
oxbow_lakes

Il existe trois bibliothèques d'analyse comparative pour Scala dont vous pouvez vous prévaloir.

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

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

  2. scala-benchmarking-template - Projet de modèle SBT permettant de créer Scala (micro-)) critères de référence basés sur Caliper.

  3. Metrics - Capture des métriques JVM et au niveau de l'application. Donc vous savez ce qui se passe

23
missingfaktor

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)
21
pathikrit

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
6

J'ai pris la solution de Jesper et y ai ajouté un peu d'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_new et counter_old, voici 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

J'espère que c'est utile

5
John Zhu

J'utilise une technique facile à déplacer dans des blocs de code. Le point crucial est que la même ligne exacte commence et finit le chronomètre - il s’agit donc d’un simple copier-coller. L’autre atout, c’est que vous définissez ce que le timing signifie pour vous en tant que 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 en bloc ou de le manipuler dans des 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
  • il est évident que cet objet laisse échapper des entrées de la carte si vous ne "fermez" pas les minuteries, par exemple. si votre code n'atteint pas la deuxième invocation pour un démarrage donné du minuteur.
4
matanster

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

  • le profileur gère les boucles (pour la cohérence et la commodité)

  • chronométrage plus précis (avec nanoTime)

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

  • retourne juste 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 d'échauffement JVM Hotspot (non temporisée) de 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
}
3
Brent Faust

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

Ci-dessous 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 Feuille de calcul, vous obtenez le temps d'exécution en millisecondes

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

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, il sera donc difficile de voir. Je suggère donc que vous puissiez utiliser currentTimeMillis à la place.

1
haiyang

En se tenant sur les épaules de géants ...

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

  • Répétitions
  • Le dernier résultat gagne pour plusieurs répétitions
  • Temps total et temps moyen pour plusieurs répétitions
  • Supprime le besoin d'un fournisseur de temps/instant comme 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
  }
}

À noter également que vous pouvez utiliser le Duration.toCoarsest méthode pour convertir en unité de temps la plus grande possible, bien que je ne sois pas sûr que ce soit convivial, avec une différence de temps minime 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> 
1
Darren Bishop