Wie profiliere ich Methoden in Scala?


117

Was ist eine Standardmethode zum Profilieren von Scala-Methodenaufrufen?

Was ich brauche, sind Hooks um eine Methode, mit der ich Timer starten und stoppen kann.

In Java verwende ich die Aspektprogrammierung, AspektJ, um die zu profilierenden Methoden zu definieren und Bytecode einzufügen, um dasselbe zu erreichen.

Gibt es in Scala einen natürlicheren Weg, auf dem ich eine Reihe von Funktionen definieren kann, die vor und nach einer Funktion aufgerufen werden sollen, ohne dabei statische Eingaben zu verlieren?


Wenn AspectJ gut mit Scala zusammenarbeitet, verwenden Sie AspectJ. Warum das Rad neu erfinden? Die obigen Antworten, die eine benutzerdefinierte Flusssteuerung verwenden, erfüllen nicht die grundlegenden Anforderungen von AOP, da Sie Ihren Code ändern müssen, um sie zu verwenden. Diese könnten auch von Interesse sein: java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-concerns-in-scala
Ant Kutschera


Was interessiert dich? Möchten Sie wissen, wie lange eine bestimmte Methode in der Produktionsumgebung dauert? Dann sollten Sie sich die Metrikbibliotheken ansehen und die Messung nicht wie in der akzeptierten Antwort selbst durchführen. Wenn Sie untersuchen möchten, welche Codevariante "im Allgemeinen" schneller ist, dh in Ihrer Entwicklungsumgebung, verwenden Sie sbt-jmh wie unten dargestellt.
16.

Antworten:


214

Möchten Sie dies tun, ohne den Code zu ändern, für den Sie das Timing messen möchten? Wenn es Ihnen nichts ausmacht, den Code zu ändern, können Sie Folgendes tun:

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 }

Das ist ordentlich. Kann ich das Gleiche ohne Codeänderung tun?
Sheki

Nicht automatisch mit dieser Lösung; Wie würde Scala wissen, was Sie Zeit haben möchten?
Jesper

1
Dies ist nicht unbedingt der Fall - Sie können Dinge automatisch in die REPL
oxbow_lakes

1
Fast perfekt, aber Sie müssen auch auf mögliche Ausnahmen reagieren. Berechnen Sie t1innerhalb einer finallyKlausel
Juanmirocks

2
Sie können Ihren Drucken mit etwas Currying ein Etikett hinzufügen: def time[R](label: String)(block: => R): R = {println
Fügen

34

Zusätzlich zu Jespers Antwort können Sie Methodenaufrufe automatisch in die REPL einschließen:

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

Nun - lassen Sie uns etwas darin einwickeln

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

OK - wir müssen im Power-Modus sein

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

Weg wickeln

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

Ich habe keine Ahnung, warum das 5 Mal gedruckt wurde

Update ab 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

8
Um Ersatz jemand die Mühe nun fragen, das :wrapFeature wurde entfernt aus dem REPL: - \
Ches

25

Es gibt drei Benchmarking-Bibliotheken für Scala , die Sie nutzen können.

Da sich die URLs auf der verlinkten Website wahrscheinlich ändern, füge ich den entsprechenden Inhalt unten ein.

  1. SPerformance - Framework für Leistungstests, das darauf abzielt, Leistungstests automatisch zu vergleichen und in Simple Build Tool zu arbeiten.

  2. Scala-Benchmarking-Vorlage - SBT-Vorlagenprojekt zum Erstellen von Scala (Mikro-) Benchmarks basierend auf Caliper.

  3. Metriken - Erfassen von Metriken auf JVM- und Anwendungsebene. Sie wissen also, was los ist


21

Das was ich benutze:

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)

6

testing.Benchmark könnte nützlich sein.

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

5
Beachten Sie, dass testing.Benchmark @deprecated ist ("Diese Klasse wird entfernt.", "2.10.0").
Tvaroh

5

Ich nahm die Lösung von Jesper und fügte ihr bei mehreren Durchläufen desselben Codes eine Aggregation hinzu

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

Angenommen, Sie möchten zwei Funktionen zeitlich festlegen, counter_newund counter_oldFolgendes wird verwendet:

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

Hoffentlich ist das hilfreich


4

Ich verwende eine Technik, die sich leicht in Codeblöcken bewegen lässt. Der springende Punkt ist, dass genau dieselbe Zeile den Timer startet und beendet - es ist also wirklich ein einfaches Kopieren und Einfügen. Die andere schöne Sache ist, dass Sie definieren können, was das Timing für Sie als Zeichenfolge bedeutet, alle in derselben Zeile.

Anwendungsbeispiel:

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

Der 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()
  }

Vorteile:

  • Sie müssen den Code nicht als Block umbrechen oder innerhalb von Zeilen bearbeiten
  • kann den Anfang und das Ende des Timers leicht zwischen Codezeilen verschieben, wenn er explorativ ist

Nachteile:

  • weniger glänzend für absolut funktionalen Code
  • Offensichtlich verliert dieses Objekt Karteneinträge, wenn Sie Timer nicht "schließen", z. B. wenn Ihr Code für einen bestimmten Timer-Start nicht zum zweiten Aufruf gelangt.

Das ist großartig, aber sollte die Verwendung nicht sein : Timelog.timer("timer name/description")?
Schoon

4

ScalaMeter ist eine nette Bibliothek, um Benchmarking in Scala durchzuführen

Unten ist ein einfaches Beispiel

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

Wenn Sie das obige Code-Snippet im Scala-Arbeitsblatt ausführen, erhalten Sie die Laufzeit in Millisekunden

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

3

Ich mag die Einfachheit von @ wricks Antwort, wollte aber auch:

  • Der Profiler verarbeitet Schleifen (aus Gründen der Konsistenz und Bequemlichkeit).

  • genaueres Timing (mit nanoTime)

  • Zeit pro Iteration (nicht die Gesamtzeit aller Iterationen)

  • Geben Sie einfach ns / iteration zurück - kein Tupel

Dies wird hier erreicht:

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

Für noch mehr Genauigkeit ermöglicht eine einfache Änderung eine JVM-Hotspot-Aufwärmschleife (nicht zeitgesteuert) zum Timing kleiner Snippets:

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
}

Dies ist keine Antwort, es wäre am besten, es als Kommentar zu schreiben
nedim

1
@nedim Die Lösung wird für die Frage gegeben - ein Wrapper für alles, was Sie Zeit haben möchten. Alle Funktionen, die das OP aufrufen möchte, können im Wrapper oder in dem Block platziert werden, der seine Funktionen aufruft, damit er "eine Reihe von Funktionen definieren kann, die vor und nach einer Funktion aufgerufen werden sollen, ohne die statische Typisierung zu verlieren"
Brent Faust

1
Du hast recht. Entschuldigung, ich muss den Code übersehen haben. Wenn meine Bearbeitung überprüft wird, kann ich die Abstimmung rückgängig machen.
Nedim

3

Der empfohlene Ansatz für das Benchmarking von Scala-Code ist sbt-jmh

"Vertraue niemandem, setze alles auf die Bank." - sbt Plugin für JMH (Java Microbenchmark Harness)

Dieser Ansatz wird von vielen großen Scala-Projekten verfolgt, zum Beispiel von

Ein einfacher Wrapper-Timer basierend auf System.nanoTimeist keine zuverlässige Methode zum Benchmarking:

System.nanoTimeist so schlimm wie String.internjetzt: Sie können es verwenden, aber verwenden Sie es mit Bedacht. Die durch Timer verursachten Latenz-, Granularitäts- und Skalierbarkeitseffekte können und werden sich auf Ihre Messungen auswirken, wenn sie ohne angemessene Genauigkeit durchgeführt werden. Dies ist einer der vielen Gründe, warum System.nanoTimedurch Benchmarking-Frameworks von den Benutzern abstrahiert werden sollte

Darüber hinaus können Überlegungen wie JIT-Aufwärmphase , Speicherbereinigung , systemweite Ereignisse usw. zu Unvorhersehbarkeit bei Messungen führen:

Unzählige Effekte müssen gemindert werden, einschließlich Aufwärmen, Eliminieren von totem Code, Verzweigen usw. Glücklicherweise kümmert sich JMH bereits um viele Dinge und verfügt über Bindungen für Java und Scala.

Basierend auf Travis Browns Antwort finden Sie hier ein Beispiel für die Einrichtung eines JMH-Benchmarks für Scala

  1. Fügen Sie jmh hinzu project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. Aktivieren Sie das JMH-Plugin build.sbt
    enablePlugins(JmhPlugin)
  3. Hinzufügen 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. Benchmark ausführen mit
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

Die Ergebnisse sind

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

Dies scheint darauf hinzudeuten, dass das Voranstellen an a Listund das anschließende Umkehren am Ende um eine Größenordnung schneller ist als das Anhängen an a Vector.


1

Während auf den Schultern von Riesen stehen ...

Eine solide Bibliothek von Drittanbietern wäre idealer, aber wenn Sie eine schnelle und auf Standardbibliotheken basierende Bibliothek benötigen, bietet die folgende Variante:

  • Wiederholungen
  • Das letzte Ergebnis gewinnt für mehrere Wiederholungen
  • Gesamtzeit und durchschnittliche Zeit für mehrere Wiederholungen
  • Entfernt die Notwendigkeit eines Zeit- / Sofortanbieters als Parameter

.

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

Erwähnenswert ist auch, dass Sie die Duration.toCoarsestMethode verwenden können, um in die größtmögliche Zeiteinheit zu konvertieren, obwohl ich nicht sicher bin, wie freundlich dies mit einem geringen Zeitunterschied zwischen den Läufen ist, z

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

Sie können verwenden 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
}

Verwendung:

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

nanoTime wird es Ihnen zeigen ns, daher ist es schwer zu sehen. Daher schlage ich vor, dass Sie stattdessen currentTimeMillis verwenden können.


Schwer zu erkennende Nanosekunden sind ein schlechter Grund, zwischen beiden zu wählen. Neben der Auflösung gibt es einige wichtige Unterschiede. Zum einen kann sich currentTimeMillis während der vom Betriebssystem regelmäßig durchgeführten Taktanpassungen ändern und sogar rückwärts gehen. Ein weiterer Grund ist, dass nanoTime möglicherweise nicht threadsicher ist: stackoverflow.com/questions/351565/…
Chris
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.