Come profilare i metodi in Scala?


117

Qual è un modo standard di profilare le chiamate ai metodi Scala?

Quello di cui ho bisogno sono i ganci attorno a un metodo, che posso usare per avviare e arrestare i timer.

In Java utilizzo la programmazione degli aspetti, aspectJ, per definire i metodi da profilare e iniettare bytecode per ottenere lo stesso.

Esiste un modo più naturale in Scala, in cui posso definire un gruppo di funzioni da chiamare prima e dopo una funzione senza perdere la digitazione statica nel processo?


Se AspectJ funziona bene con Scala, usa AspectJ. Perché reinventare la ruota? Le risposte sopra che utilizzano il controllo del flusso personalizzato non riescono a soddisfare i requisiti di base di AOP poiché per utilizzarle è necessario modificare il codice. Questi potrebbero anche essere di interesse: java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-concerns-in-scala
Ant Kutschera,


Cosa ti interessa? Vuoi sapere quanto tempo impiega un determinato metodo nell'ambiente di produzione. Quindi dovresti guardare le librerie di metriche e non eseguire il rollio delle misurazioni come nella risposta accettata. Se vuoi indagare quale variante di codice è più veloce "in generale", cioè nel tuo ambiente di sviluppo, usa sbt-jmh come presentato di seguito.
jmg

Risposte:


214

Vuoi farlo senza cambiare il codice per il quale vuoi misurare i tempi? Se non ti dispiace cambiare il codice, potresti fare qualcosa del genere:

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 }

Questo è pulito, posso fare la stessa cosa senza modificare il codice?
sheki

Non automaticamente con questa soluzione; come farebbe Scala a sapere cosa vorresti cronometrare?
Jesper

1
Questo non è strettamente vero: puoi avvolgere automaticamente le cose nella REPL
oxbow_lakes

1
Quasi perfetto, ma devi reagire anche a possibili eccezioni. Calcola t1all'interno di una finallyclausola
juanmirocks

2
Puoi aggiungere un'etichetta alle tue stampe con un po 'di curry: def time[R](label: String)(block: => R): R = {quindi aggiungi l'etichetta aprintln
Glenn' devalias '

34

Oltre alla risposta di Jesper, puoi racchiudere automaticamente le invocazioni dei metodi nella 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

Ora, avvolgiamo qualcosa in questo

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

OK, dobbiamo essere in modalità di alimentazione

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

Avvolgere

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

Non ho idea del perché quella roba sia stata stampata 5 volte

Aggiornamento dalla 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
Per risparmiare a chiunque la fatica di chiederselo ora, la :wrapfunzione è stata rimossa dalla REPL: - \
ches

25

Ci sono tre librerie di benchmarking per Scala di cui puoi usufruire.

Poiché è probabile che gli URL sul sito collegato cambino, incollo il contenuto pertinente di seguito.

  1. SPerformance - Framework di test delle prestazioni volto a confrontare automaticamente i test delle prestazioni e lavorare all'interno di Simple Build Tool.

  2. scala-benchmarking-template - Progetto modello SBT per la creazione di benchmark Scala (micro-) basati su Caliper.

  3. Metriche : acquisizione di metriche a livello di applicazione e JVM. Quindi sai cosa sta succedendo


21

Questo quello che uso:

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 potrebbe essere 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

5
Tieni presente che testing.Benchmark è @deprecated ("Questa classe verrà rimossa.", "2.10.0").
Tvaroh

5

Ho preso la soluzione da Jesper e ho aggiunto un po 'di aggregazione su più esecuzioni dello stesso codice

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

Supponiamo di voler cronometrare due funzioni counter_newe counter_old, di seguito, l'utilizzo:

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

Si spera che questo sia utile


4

Uso una tecnica che è facile da spostare nei blocchi di codice. Il punto cruciale è che la stessa riga esatta inizia e finisce il timer, quindi è davvero un semplice copia e incolla. L'altra cosa bella è che puoi definire cosa significa il tempismo per te come una stringa, tutto sulla stessa linea.

Utilizzo di esempio:

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

Il codice:

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

Professionisti:

  • non c'è bisogno di racchiudere il codice come un blocco o manipolare all'interno di righe
  • può facilmente spostare l'inizio e la fine del timer tra le righe di codice quando è esplorativo

Contro:

  • meno brillante per codice assolutamente funzionale
  • ovviamente questo oggetto perde le voci della mappa se non si "chiudono" i timer, ad esempio se il codice non arriva alla seconda invocazione per un dato avvio del timer.

Questo è fantastico, ma l'uso non dovrebbe essere Timelog.timer("timer name/description"):?
schoon

4

ScalaMeter è una bella libreria per eseguire il benchmarking in Scala

Di seguito è riportato un semplice esempio

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

Se esegui lo snippet di codice sopra in Scala Worksheet, ottieni il tempo di esecuzione in millisecondi

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

3

Mi piace la semplicità della risposta di @ wrick, ma volevo anche:

  • il profiler gestisce il loop (per coerenza e comodità)

  • tempistica più precisa (utilizzando nanoTime)

  • tempo per iterazione (non tempo totale di tutte le iterazioni)

  • basta restituire ns / iteration - non una tupla

Ciò si ottiene qui:

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

Per una precisione ancora maggiore, una semplice modifica consente un ciclo di riscaldamento Hotspot JVM (non temporizzato) per la temporizzazione di piccoli frammenti:

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
}

Questa non è una risposta, sarebbe meglio scriverla come commento
nedim

1
@nedim La soluzione è data alla domanda: un wrapper per tutto ciò che vuoi cronometrare. Qualsiasi funzione che l'OP vorrebbe chiamare può essere inserita nel wrapper, o nel blocco che chiama le sue funzioni in modo che "possa definire un gruppo di funzioni da chiamare prima e dopo una funzione senza perdere alcuna digitazione statica"
Brent Faust

1
Hai ragione. Scusa, devo aver ignorato il codice. Quando la mia modifica viene rivista, posso annullare il voto negativo.
nedim

3

L'approccio consigliato per il benchmarking del codice Scala è tramite sbt-jmh

"Non fidarti di nessuno, metti tutto in panchina." - plugin sbt per JMH (Java Microbenchmark Harness)

Questo approccio è adottato da molti dei principali progetti Scala, ad esempio,

  • Scala stesso linguaggio di programmazione
  • punteggiato (Scala 3)
  • gattilibreria per la programmazione funzionale
  • Metals language server per IDE

Il semplice timer wrapper basato su nonSystem.nanoTime è un metodo affidabile di benchmarking:

System.nanoTimeè brutto come String.internadesso: puoi usarlo, ma usalo con saggezza. Gli effetti di latenza, granularità e scalabilità introdotti dai timer possono influenzare e influenzeranno le misurazioni se eseguite senza il giusto rigore. Questo è uno dei tanti motivi per cui System.nanoTimedovrebbe essere astratto dagli utenti tramite benchmarking framework

Inoltre, considerazioni come il riscaldamento JIT , la raccolta dei rifiuti, gli eventi a livello di sistema, ecc. Potrebbero introdurre imprevedibilità nelle misurazioni:

Tantissimi effetti devono essere mitigati, inclusi il riscaldamento, l'eliminazione del codice morto, il fork, ecc. Fortunatamente, JMH si occupa già di molte cose e ha collegamenti sia per Java che per Scala.

Sulla base della risposta di Travis Brown, ecco un esempio di come impostare il benchmark JMH per Scala

  1. Aggiungi jmh a project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. Abilita il plugin jmh in build.sbt
    enablePlugins(JmhPlugin)
  3. Aggiungere a 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. Esegui il benchmark con
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

I risultati sono

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

che sembra indicare che anteporre a a Liste poi invertirlo alla fine è l'ordine di grandezza più veloce di continuare ad aggiungerlo ad a Vector.


1

Mentre in piedi sulle spalle dei giganti ...

Una solida libreria di terze parti sarebbe più ideale, ma se hai bisogno di qualcosa di veloce e basato su libreria std, la seguente variante fornisce:

  • ripetizioni
  • L'ultimo risultato vince per più ripetizioni
  • Tempo totale e tempo medio per ripetizioni multiple
  • Elimina la necessità di un provider di tempo / istantaneo come parametro

.

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

Vale anche la pena notare che puoi utilizzare il Duration.toCoarsestmetodo per convertire nella più grande unità di tempo possibile, anche se non sono sicuro di quanto sia amichevole con una differenza di tempo minore tra le corse, ad es.

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

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

Uso:

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

nanoTime te lo mostrerà ns, quindi sarà difficile da vedere. Quindi suggerisco che puoi usare currentTimeMillis al posto di esso.


Il fatto che i nanosecondi siano difficili da vedere non è una buona ragione per scegliere tra i due. Ci sono alcune importanti differenze oltre alla risoluzione. Per uno, currentTimeMillis può cambiare e persino tornare indietro durante le regolazioni dell'orologio che il sistema operativo esegue periodicamente. Un altro è che nanoTime potrebbe non essere thread-safe: stackoverflow.com/questions/351565/…
Chris
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.