Como criar o perfil de métodos no Scala?


117

Qual é a maneira padrão de criar perfil de chamadas de método Scala?

O que eu preciso são ganchos em torno de um método, usando o qual posso usar para iniciar e parar os cronômetros.

Em Java, eu uso a programação de aspecto, aspectJ, para definir os métodos a serem traçados e injetar bytecode para conseguir o mesmo.

Existe uma maneira mais natural em Scala, onde posso definir um monte de funções a serem chamadas antes e depois de uma função sem perder qualquer tipo de estática no processo?


Se AspectJ funciona bem com Scala, use AspectJ. Por que reinventar a roda? As respostas acima, que usam controle de fluxo personalizado, não atendem aos requisitos básicos do AOP, pois para usá-los é necessário modificar seu código. Eles também podem ser de interesse: java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-concerns-in-scala
Ant Kutschera


Em quê você está interessado? Você quer saber quanto tempo leva um determinado método no ambiente de produção. Então você deve olhar as bibliotecas de métricas e não rolar a medição como na resposta aceita. Se você deseja investigar qual variante de código é mais rápida "em geral", ou seja, em seu ambiente de desenvolvimento, use sbt-jmh conforme apresentado abaixo.
jmg

Respostas:


214

Deseja fazer isso sem alterar o código para o qual deseja medir os tempos? Se você não se importa em alterar o código, pode fazer algo assim:

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 }

Isso é legal, posso fazer a mesma coisa sem qualquer alteração no código?
sheki de

Não automaticamente com esta solução; como o Scala saberia o que você quer cronometrar?
Jesper

1
Isso não é estritamente verdadeiro - você pode
agrupar

1
Quase perfeito, mas você também deve reagir a possíveis exceções. Calcule t1dentro de uma finallycláusula
juanmirocks

2
Você pode adicionar um rótulo às suas impressões com um pouco de curry: em def time[R](label: String)(block: => R): R = {seguida, adicione o rótulo aoprintln
Glenn 'devalias'

34

Além da resposta de Jesper, você pode envolver automaticamente invocações de método no 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

Agora - vamos embrulhar qualquer coisa neste

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

OK - precisamos estar no modo de energia

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

Embrulhar

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

Não tenho ideia do por que isso imprimiu 5 vezes

Atualização a 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

8
Para poupar qualquer um do trabalho de se perguntar agora, o :wraprecurso foi removido do REPL: - \
ches

25

Existem três bibliotecas de benchmarking para Scala que você pode aproveitar.

Como os URLs do site vinculado provavelmente serão alterados, estou colando o conteúdo relevante abaixo.

  1. SPerformance - framework de teste de desempenho que tem como objetivo comparar os testes de desempenho de forma automática e mágica e trabalhar dentro da ferramenta Simple Build.

  2. scala-benchmarking-template - Projeto de template SBT para a criação de benchmarks Scala (micro) baseados no Caliper.

  3. Métricas - captura de métricas de nível de aplicativo e JVM. Então você sabe o que está acontecendo


21

Isso é o que eu 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 pode 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
Esteja ciente de que testing.Benchmark é @deprecated ("Esta classe será removida.", "2.10.0").
Tvaroh

5

Peguei a solução de Jesper e adicionei alguma agregação a ela em várias execuções do mesmo 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))
}

Suponha que você queira cronometrar duas funções counter_newe counter_old, o seguinte é o 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 isso seja útil


4

Eu uso uma técnica que é fácil de mover em blocos de código. O ponto crucial é que a mesma linha exata inicia e termina o cronômetro - portanto, é realmente um simples copiar e colar. A outra coisa boa é que você consegue definir o que o tempo significa para você como uma string, tudo na mesma linha.

Exemplo de uso:

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

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

Prós:

  • não há necessidade de quebrar o código como um bloco ou manipular dentro das linhas
  • pode mover facilmente o início e o fim do cronômetro entre as linhas de código ao ser exploratório

Contras:

  • menos brilhante para código totalmente funcional
  • obviamente, este objeto vaza entradas de mapa se você não "fechar" os cronômetros, por exemplo, se seu código não chegar à segunda chamada para um determinado início de cronômetro.

Isso é ótimo, mas o uso não deveria ser Timelog.timer("timer name/description"):?
escuna de

4

ScalaMeter é uma boa biblioteca para realizar benchmarking em Scala

Abaixo está um exemplo simples

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 você executar o trecho de código acima na planilha Scala, você obterá o tempo de execução em milissegundos

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

3

Gosto da simplicidade da resposta de @wrick, mas também queria:

  • o criador de perfil lida com o loop (para consistência e conveniência)

  • tempo mais preciso (usando nanoTime)

  • tempo por iteração (não o tempo total de todas as iterações)

  • apenas retorna ns / iteração - não uma tupla

Isso é feito aqui:

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

Para ainda mais precisão, uma modificação simples permite um loop de aquecimento JVM Hotspot (não cronometrado) para cronometrar pequenos 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
}

Esta não é uma resposta, seria melhor escrevê-la como um comentário
nedim

1
@nedim A solução é dada para a questão - um invólucro para qualquer coisa que você queira cronometrar. Quaisquer funções que o OP queira chamar podem ser colocadas no wrapper ou no bloco que chama suas funções para que ele "possa definir um monte de funções a serem chamadas antes e depois de uma função sem perder qualquer tipo estático"
Brent Faust

1
Você está certo. Desculpe, devo ter esquecido o código. Quando minha edição for revisada, posso desfazer a votação negativa.
nedim

3

A abordagem recomendada para comparar o código do Scala é via sbt-jmh

"Não confie em ninguém, confie em tudo." - Plug-in sbt para JMH (Java Microbenchmark Harness)

Essa abordagem é adotada por muitos dos principais projetos Scala, por exemplo,

  • A própria linguagem de programação Scala
  • Dotty (Scala 3)
  • biblioteca gatos para programação funcional
  • Servidor de linguagem de metais para IDEs

O temporizador de invólucro simples baseado em nãoSystem.nanoTime é um método confiável de comparação:

System.nanoTimeestá tão ruim quanto String.internagora: você pode usá-lo, mas com sabedoria. Os efeitos de latência, granularidade e escalabilidade introduzidos pelos temporizadores podem e irão afetar suas medições se feitas sem o rigor adequado. Esta é uma das muitas razões pelas quais System.nanoTimedeve ser abstraído dos usuários por estruturas de benchmarking

Além disso, considerações como aquecimento JIT , coleta de lixo, eventos de todo o sistema, etc. podem introduzir imprevisibilidade nas medições:

Toneladas de efeitos precisam ser mitigadas, incluindo aquecimento, eliminação de código morto, bifurcação, etc. Felizmente, JMH já cuida de muitas coisas e tem ligações para Java e Scala.

Com base na resposta de Travis Brown, aqui está um exemplo de como configurar o benchmark JMH para Scala

  1. Adicionar jmh a project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. Habilitar plugin jmh em build.sbt
    enablePlugins(JmhPlugin)
  3. adicionar à 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. Execute benchmark com
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

Os resultados são

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

o que parece indicar que preceder a a Liste então invertê-lo no final é uma ordem de magnitude mais rápido do que continuar acrescentando a a Vector.


1

Enquanto estava sobre os ombros de gigantes ...

Uma biblioteca de terceiros sólida seria mais ideal, mas se você precisar de algo rápido e baseado em biblioteca padrão, a seguinte variante fornece:

  • Repetições
  • O último resultado ganha para várias repetições
  • Tempo total e tempo médio para várias repetições
  • Remove a necessidade de provedor de tempo / instantâneo como um 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
  }
}

Também vale a pena notar que você pode usar o Duration.toCoarsestmétodo para converter para a maior unidade de tempo possível, embora eu não tenha certeza de como isso é amigável com uma pequena diferença de tempo entre as execuções, por exemplo

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

Você pode usar 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 irá mostrar a você ns, então será difícil de ver. Portanto, sugiro que você possa usar currentTimeMillis em vez dele.


O fato de os nanossegundos serem difíceis de ver é um motivo ruim para escolher entre os dois. Existem algumas diferenças importantes além da resolução. Por um lado, currentTimeMillis pode mudar e até mesmo retroceder durante os ajustes de relógio que o sistema operacional realiza periodicamente. Outra é que o nanoTime pode não ser thread-safe: stackoverflow.com/questions/351565/…
Chris
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.