如何在Scala中分析方法?


117

分析Scala方法调用的标准方法是什么?

我需要的是方法周围的钩子,可以用来启动和停止计时器。

在Java中,我使用方面编程AspectJ来定义要分析的方法,并注入字节码以实现相同的目的。

Scala中是否有更自然的方法,我可以在函数之前和之后定义一堆函数,而又不会在过程中丢失任何静态类型?


如果AspectJ在Scala上表现不错,请使用AspectJ。为什么要重新发明轮子?上面的使用自定义流控制的答案无法满足AOP的基本要求,因为要使用它们,您需要修改代码。这些也可能引起关注:java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-cercerns-in-scala
Ant Kutschera


你对什么感兴趣呢?您是否想知道某种方法在生产环境中需要花费多长时间?然后,您应该查看指标库,而不要像接受的答案那样自己滚动测量。如果要“一般”(即在您的开发环境中)调查哪种代码变体更快,请使用sbt-jmh,如下所示。
jmg

Answers:


214

是否要执行此操作而不更改要测量其时序的代码?如果您不介意更改代码,则可以执行以下操作:

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 }

这很整洁,我可以在不更改任何代码的情况下做同样的事情吗?
sheki 2012年

此解决方案无法自动执行;Scala如何知道您想要的时间?
杰斯珀

1
这并非完全正确-您可以自动将内容包装在REPL中
oxbow_lakes 2012年

1
几乎是完美的,但是您也必须对可能的异常做出反应。t1finally条款中进行计算
juanmirocks '16

2
您可以花些时间def time[R](label: String)(block: => R): R = {在标签上添加标签:然后将标签添加到println
Glenn'devalias'17

34

除了Jesper的答案,您还可以在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

现在-让我们在其中包装任何东西

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

好-我们需要进入省电模式

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

收起来

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

我不知道为什么要打印5次

从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
为了使任何人现在都:wrap
不必担心

25

3个标杆库Scala的,你可以利用的。

由于链接站点上的URL可能会更改,因此我在下面粘贴了相关内容。

  1. SPerformance-性能测试框架,旨在自动比较性能测试并在Simple Build Tool中进行操作。

  2. scala-benchmarking-template -SBT模板项目,用于基于Caliper创建Scala(微型)基准。

  3. 指标 -捕获JVM和应用程序级别的指标。所以你知道发生了什么


21

这是我用的:

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 可能会有用。

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
请注意,test.Benchmark是@deprecated(“此类将被删除。”,“ 2.10.0”)。
Tvaroh

5

我从Jesper那里获得了解决方案,并在同一代码的多次运行中为其添加了一些聚合

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

假设您要对两个函数counter_new和计时counter_old,以下是用法:

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

希望这会有所帮助


4

我使用一种易于在代码块中移动的技术。问题的关键是计时器的开始和结束都在同一行上,因此实际上是简单的复制和粘贴。另一个好处是,您可以在同一行中定义时间对字符串的含义。

用法示例:

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

代码:

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

优点:

  • 无需将代码包装为块或在行内操作
  • 探索时可以轻松地在代码行之间移动计时器的开始和结束

缺点:

  • 完全没有光泽的代码
  • 显然,如果您不“关闭”计时器,则该对象会泄漏映射条目,例如,如果您的代码未到达给定计时器启动的第二次调用。

这是伟大的,但不应该使用是:Timelog.timer("timer name/description")
schoon

4

ScalaMeter是一个很好的库,可以在Scala中执行基准测试

下面是一个简单的例子

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

如果在Scala工作表中执行上述代码片段,则将获得运行时间(以毫秒为单位)

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

3

我喜欢@wrick答案的简单性,但也想要:

  • 探查器处理循环(为了保持一致和方便)

  • 更精确的计时(使用nanoTime)

  • 每次迭代的时间(不是所有迭代的总时间)

  • 只返回ns / iteration-不是元组

这是在这里实现的:

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

为了获得更高的准确性,一个简单的修改就允许JVM Hotspot预热循环(未定时)来定时小片段:

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
}

这不是答案,最好将其写为评论
nedim

1
@nedim这个问题的解决方案是-您想要计时的任何东西的包装器。OP想要调用的任何函数都可以放在包装器中,也可以放在调用他的函数的块中,这样他“可以定义一个在函数之前和之后要调用的函数,而不会丢失任何静态类型”
Brent Faust

1
你是对的。抱歉,我必须忽略了代码。当我的编辑获得审核时,我可以撤消降级投票。
尼迪姆

3

推荐的对Scala代码进行基准测试的方法是通过sbt-jmh

“不要相信任何人,把一切都放在长凳上。” -JMH(Java Microbenchmark Harness)的sbt插件

例如,许多主要的Scala项目都采用了这种方法,

  • Scala编程语言本身
  • 多蒂(Scala 3)
  • 用于函数式编程的cats
  • 适用于IDE的Metals语言服务器

基于简单的包装计时器System.nanoTime不是一个可靠的方法标杆:

System.nanoTimeString.intern现在一样糟糕:您可以使用它,但要明智地使用它。计时器引入的延迟,粒度和可伸缩性影响可能会并且将在不适当严格的情况下影响您的测量。这是为什么System.nanoTime应该通过基准测试框架从用户中抽象出来的众多原因之一

此外,JIT预热,垃圾收集,系统范围的事件等考虑因素可能会在测量中引入不可预测性

需要减轻许多影响,包括预热,消除死代码,分叉等等。幸运的是,JMH已经处理了很多事情,并且具有Java和Scala的绑定。

根据Travis Brown的回答,这里是如何为Scala设置JMH基准的示例

  1. 将jmh添加到 project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. 在启用JMH插件 build.sbt
    enablePlugins(JmhPlugin)
  3. 添加 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. 执行基准测试
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

结果是

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

这似乎表明在前面加上a List,然后在最后将其反转比在后面继续添加要快几个数量级Vector


1

站在巨人的肩膀上...

一个可靠的第三方库将是更理想的选择,但是如果您需要基于std-library的快速库,可以使用以下变体:

  • 重复次数
  • 最后结果胜出多次
  • 多次重复的总时间和平均时间
  • 无需将时间/即时提供者作为参数

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

同样值得一提的是,您可以使用该Duration.toCoarsest方法将其转换为最大的时间单位,尽管我不确定这与两次运行之间的微小时差有多友好。

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

您可以使用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
}

用法:

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

nanoTime将显示给您ns,因此很难看到。因此,我建议您可以使用currentTimeMillis代替它。


很难看到纳秒级,这是在两者之间进行选择的可怜原因。除分辨率外,还有一些重要的区别。首先,在操作系统定期执行的时钟调整过程中,currentTimeMillis可能会更改甚至倒退。另一个是nanoTime可能不是线程安全的:stackoverflow.com/questions/351565/…–
克里斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.