关于功能:如何在Scala中分析方法?

How to profile methods in Scala?

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

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

在Java中,我使用方面编程(AspectJ)来定义要被配置的方法和注入字节码来实现相同的方法。

在scala中,是否有一种更自然的方法,在这种方法中,我可以在函数前后定义一组要调用的函数,而不会在过程中丢失任何静态类型?


是否要在不更改要测量其计时的代码的情况下执行此操作?如果您不介意更改代码,那么可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 }


除了Jesper的答案之外,您还可以在repl中自动包装方法调用:

1
2
3
4
5
6
7
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

现在-我们把东西包在里面

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

好的-我们需要处于电源模式

1
2
3
4
5
6
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>    **

包掉

1
2
3
4
5
6
7
8
9
10
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开始更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
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


您可以利用scala的三个基准库。

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

  • 性能测试框架,旨在自动比较性能测试并在简单的构建工具中工作。

  • scala基准模板-用于创建基于caliper的scala(micro-)基准的SBT模板项目。

  • 度量—捕获JVM—和应用程序级度量。所以你知道发生了什么事


  • 我用的是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)

    testing.Benchmark可能有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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_newcounter_old计时,下面是用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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

    希望这有帮助


    我使用的技术很容易在代码块中移动。关键是相同的行开始和结束计时器-所以它实际上是一个简单的复制和粘贴。另一个好处是,你可以定义时间对你意味着什么,作为一个字符串,在同一行。

    示例用法:

    1
    2
    3
    Timelog("timer name/description")
    //code to time
    Timelog("timer name/description")

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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()
      }

    赞成的意见:

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

    欺骗:

    • 对于完全功能的代码来说不那么闪亮
    • 显然,如果不"关闭"计时器,这个对象会泄漏映射项,例如,如果您的代码没有进入给定计时器启动的第二次调用。


    scalameter是在scala中执行基准测试的好库。

    下面是一个简单的例子

    1
    2
    3
    4
    5
    6
    7
    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工作表中执行上述代码段,则会得到以毫秒为单位的运行时间。

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

    我喜欢@wrick回答的简单性,但也希望:

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

    • 更精确的计时(使用纳米时间)

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

    • 只返回ns/迭代-不是元组

    这是在这里实现的:

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

    为了更加精确,一个简单的修改允许一个jvm热点预热循环(不是定时的)来定时小片段:

    1
    2
    3
    4
    5
    6
    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
    }


    站在巨人的肩膀上…

    一个可靠的第三方库会更理想,但是如果您需要一些基于快速和标准库的东西,下面的变体提供了:

    • 重复次数
    • 最后一个结果是多次重复获胜
    • 多次重复的总时间和平均时间
    • 不再需要时间/即时提供程序作为参数

    .

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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方法转换为尽可能大的时间单位,尽管我不确定这有多友好,但运行之间的时间差很小,例如。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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>

    您可以使用System.currentTimeMillis

    1
    2
    3
    4
    5
    6
    7
    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
    }

    用途:

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

    奈米时间将向你展示ns,所以很难看到。所以我建议你可以用currentTimeMillis代替它。