如何在 Scala 中分析方法?

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

我需要的是围绕一个方法的挂钩,使用这个方法可以启动和停止 Timers。

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

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

54953 次浏览

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

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 中自动包装方法调用:

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

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

Scala 的三个基准测试库,你可以利用。

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

  1. SPerformance -性能测试框架,旨在自动比较性能测试,并在 Simple Build Tool 内工作。

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

  3. 度量 -捕获 JVM 和应用程序级度量

我用的是这个:

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)

我使用了一种很容易在代码块中移动的技术。关键是同一行开始和结束计时器-所以它实际上是一个简单的复制和粘贴。另一个好处是,您可以将计时对您的意义定义为一个字符串,所有这些都在同一行中。

示例用法:

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

优点:

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

缺点:

  • 对于完全功能化的代码来说不那么闪亮
  • 显然,如果不“关闭”计时器,这个对象会泄露映射条目, 例如,如果您的代码没有到达给定计时器启动的第二次调用。

我喜欢@wrick 的简洁回答,但也想要:

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

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

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

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

这是在这里实现的:

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
}

而站在巨人的肩膀上..。

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

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

.

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>

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

纳米时间会显示 ns,所以很难看到。所以我建议您可以使用 currentTimeMillis 来代替它。

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

我从 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_newcounter_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

希望这能有帮助

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

“不要相信任何人,放弃一切”-sbt plugin for JMH (Java 微基准安全带)

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

基于 System.nanoTime的简单包装定时器是基准测试的 不是可靠的方法:

System.nanoTime现在和 String.intern一样糟糕: 你可以使用它, 延迟、粒度和可伸缩性效应 定时器的引入可能并将影响您的测量,如果这样做 没有适当的严谨性。这是许多原因之一 应该通过基准测试从用户中抽象出 System.nanoTime 架构

此外,诸如 准时热身、垃圾收集、系统范围的事件等因素可能会将 引入了不可预测性纳入度量:

需要减轻大量的影响,包括预热、死代码 排除,叉子等等。幸运的是,JMH 已经照顾了很多 并且对 Java 和 Scala 都有绑定。

这里基于 Travis Brown 的回答提供了一个关于如何为 Scala 设置 JMH 基准测试的 例子

  1. 将 jmh 添加到 project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
    
  2. Enable jmh plugin in build.sbt
    enablePlugins(JmhPlugin)
    
  3. Add to 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 with
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"
    

The results are

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

这似乎表明,与继续追加到一个 Vector相比,预先追加到一个 List,然后在末尾逆转它的数量级更快。

使用名称和秒添加 on = > 方法

profile[R](block: => R,methodName : String): R = {
val n = System.nanoTime()
val result = block
val n1 = System.nanoTime()
println(s"Elapsed time: ${TimeUnit.MILLISECONDS.convert(n1 - n,TimeUnit.NANOSECONDS)}ms - MethodName: ${methodName}")
result
}