¿Cómo perfilar métodos en Scala?


117

¿Cuál es una forma estándar de perfilar las llamadas al método Scala?

Lo que necesito son ganchos en torno a un método, que puedo usar para iniciar y detener temporizadores.

En Java uso la programación de aspectos, aspectJ, para definir los métodos a perfilar e inyectar bytecode para lograr lo mismo.

¿Existe una forma más natural en Scala, donde puedo definir un montón de funciones para ser llamadas antes y después de una función sin perder ninguna escritura estática en el proceso?


Si AspectJ funciona bien con Scala, use AspectJ. ¿Por qué reinventar la rueda? Las respuestas anteriores que usan control de flujo personalizado no logran los requisitos básicos de AOP, ya que para usarlas necesita modificar su código. Estos también podrían ser de interés: java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-concerns-in-scala
Ant Kutschera


¿En que estas interesado? ¿Quiere saber cuánto tiempo tarda un determinado método en el entorno de producción? Entonces debe buscar bibliotecas de métricas y no medir usted mismo como en la respuesta aceptada. Si desea investigar qué variante de código es más rápida "en general", es decir, en su entorno de desarrollo, use sbt-jmh como se presenta a continuación.
jmg

Respuestas:


214

¿Quiere hacer esto sin cambiar el código para el que quiere medir tiempos? Si no le importa cambiar el código, puede hacer algo como esto:

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 }

Esto es genial, ¿puedo hacer lo mismo sin ningún cambio de código?
sheki

No automáticamente con esta solución; ¿Cómo sabría Scala el tiempo que le gustaría?
Jesper

1
Esto no es del todo cierto - se puede envolver automáticamente cosas en el REPL
oxbow_lakes

1
Casi perfecto, pero también debes reaccionar ante posibles excepciones. Calcular t1dentro de una finallycláusula
juanmirocks

2
Puede agregar una etiqueta a sus impresiones con un poco de curry: def time[R](label: String)(block: => R): R = {luego agregue la etiqueta alprintln
Glenn 'devalias'

34

Además de la respuesta de Jesper, puede ajustar automáticamente las invocaciones de métodos en el 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

Ahora - envuelvamos cualquier cosa en esto

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

OK, tenemos que estar en modo de energía

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

Envolver

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

No tengo idea de por qué eso imprimió 5 veces

Actualización a 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

8
Para evitarle a cualquiera la molestia de preguntarse ahora, la :wrapfunción se eliminó del REPL: - \
ches

25

Hay tres bibliotecas de evaluación comparativa para Scala de las que puede hacer uso.

Dado que es probable que cambien las URL del sitio vinculado, a continuación pegaré el contenido relevante.

  1. SPerformance : marco de pruebas de rendimiento destinado a comparar automáticamente las pruebas de rendimiento y trabajar dentro de Simple Build Tool.

  2. scala-benchmarking-template : proyecto de plantilla SBT para crear (micro) puntos de referencia Scala basados ​​en Caliper.

  3. Métricas : captura de métricas de JVM y de nivel de aplicación. Entonces sabes lo que esta pasando


21

Esto es lo que 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 podría ser útil.

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
Tenga en cuenta que testing.Benchmark está @deprecated ("Esta clase será eliminada", "2.10.0").
Tvaroh

5

Tomé la solución de Jesper y le agregué algo de agregación en múltiples ejecuciones del mismo código

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

Suponga que desea cronometrar dos funciones counter_newy counter_oldel siguiente es el uso:

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

Espero que esto sea útil


4

Utilizo una técnica que es fácil de mover en bloques de código. El quid es que la misma línea exacta inicia y finaliza el temporizador, por lo que es realmente una simple copia y pegado. La otra cosa buena es que puedes definir lo que significa la sincronización para ti como una cadena, todo en la misma línea.

Uso de ejemplo:

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

El código:

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

Pros:

  • no es necesario envolver el código como un bloque o manipular dentro de las líneas
  • puede mover fácilmente el inicio y el final del temporizador entre líneas de código cuando se está explorando

Contras:

  • menos brillante para un código completamente funcional
  • obviamente, este objeto filtra las entradas del mapa si no "cierra" los temporizadores, por ejemplo, si su código no llega a la segunda invocación para un inicio de temporizador determinado.

Esto es genial, pero ¿no debería ser el uso Timelog.timer("timer name/description"):?
Schoon

4

ScalaMeter es una buena biblioteca para realizar evaluaciones comparativas en Scala

A continuación se muestra un ejemplo sencillo

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 ejecuta el fragmento de código anterior en la hoja de trabajo de Scala, obtiene el tiempo de ejecución en milisegundos

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

3

Me gusta la simplicidad de la respuesta de @ wrick, pero también quería:

  • el perfilador maneja el bucle (para mayor consistencia y conveniencia)

  • sincronización más precisa (usando nanoTime)

  • tiempo por iteración (no tiempo total de todas las iteraciones)

  • solo devuelve ns / iteración, no una tupla

Esto se logra aquí:

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

Para una mayor precisión, una simple modificación permite un ciclo de calentamiento de JVM Hotspot (no cronometrado) para cronometrar pequeños fragmentos:

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
}

Esta no es una respuesta, sería mejor escribirla como un comentario
nedim

1
@nedim La solución se le da a la pregunta: un contenedor para cualquier cosa que desee cronometrar. Cualquier función que el OP quiera llamar se puede colocar en el contenedor, o en el bloque que llama a sus funciones para que "pueda definir un montón de funciones para ser llamadas antes y después de una función sin perder ningún tipo estático"
Brent Faust

1
Tienes razón. Lo siento, debo haber pasado por alto el código. Cuando mi edición sea revisada, puedo deshacer el voto negativo.
nedim

3

El enfoque recomendado para la evaluación comparativa del código Scala es a través de sbt-jmh

"No confíes en nadie, banca todo". - complemento sbt para JMH (Java Microbenchmark Harness)

Este enfoque es adoptado por muchos de los principales proyectos de Scala, por ejemplo,

  • Scala propio lenguaje de programación
  • Punteado (Scala 3)
  • gatosbiblioteca de para programación funcional
  • Servidor de idiomas de metales para IDE

El temporizador de contenedor simple basado en noSystem.nanoTime es un método confiable de evaluación comparativa:

System.nanoTimees tan malo como String.internahora: puedes usarlo, pero úsalo sabiamente. Los efectos de latencia, granularidad y escalabilidad introducidos por los temporizadores pueden afectar y afectarán sus mediciones si se realizan sin el rigor adecuado. Esta es una de las muchas razones por las System.nanoTimeque los marcos de evaluación comparativa deben abstraerse de los usuarios.

Además, consideraciones como el calentamiento de JIT , la recolección de basura, los eventos de todo el sistema, etc. pueden introducir imprevisibilidad en las mediciones:

Se deben mitigar toneladas de efectos, incluido el calentamiento, la eliminación de código muerto, la bifurcación, etc. Afortunadamente, JMH ya se encarga de muchas cosas y tiene enlaces para Java y Scala.

Basado en la respuesta de Travis Brown, aquí hay un ejemplo de cómo configurar el punto de referencia JMH para Scala

  1. Agregar jmh a project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. Habilitar el complemento jmh en build.sbt
    enablePlugins(JmhPlugin)
  3. añadir 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. Ejecute benchmark con
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

Los resultados son

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

lo que parece indicar que anteponer a a Listy luego invertirlo al final es un orden de magnitud más rápido que seguir añadiendo a a Vector.


1

Mientras está de pie sobre los hombros de gigantes ...

Una biblioteca sólida de terceros sería más ideal, pero si necesita algo rápido y basado en una biblioteca estándar, la siguiente variante proporciona:

  • Repeticiones
  • El último resultado gana por múltiples repeticiones
  • Tiempo total y tiempo medio para varias repeticiones
  • Elimina la necesidad de un proveedor de tiempo / instantáneo como parámetro

.

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

También vale la pena señalar que puede usar el Duration.toCoarsestmétodo para convertir a la unidad de tiempo más grande posible, aunque no estoy seguro de cuán amigable es esto con una pequeña diferencia de tiempo entre ejecuciones, por ejemplo

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

Puede utilizar 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 mostrará ns, por lo que será difícil de ver. Entonces sugiero que pueda usar currentTimeMillis en lugar de él.


Que los nanosegundos sean difíciles de ver es una mala razón para elegir entre los dos. Hay algunas diferencias importantes además de la resolución. Por un lado, currentTimeMillis puede cambiar e incluso retroceder durante los ajustes del reloj que el sistema operativo realiza periódicamente. Otro es que nanoTime puede no ser seguro para subprocesos: stackoverflow.com/questions/351565/…
Chris
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.