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-concerns-in-scala
Ant Kutschera


あなたは何に興味がありますか?特定のメソッドが実稼働環境でどれだけかかるか知りたいですか?次に、メトリックライブラリを確認し、承認された回答のように自分で測定をロールバックしないでください。「一般的に」、つまり開発環境でより高速なコードバリアントを調査する場合は、以下に示すようにsbt-jmhを使用します。
jmg

回答:


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 }

これは素晴らしいですが、コードを変更せずに同じことを実行できますか?
シェキ

このソリューションでは自動的には行われません。Scalaはあなたが時間をかけたいものをどのようにして知るのでしょうか?
Jesper

1
これは厳密には当てはまりません
。REPL

1
ほぼ完璧ですが、起こりうる例外にも対応する必要があります。条項t1内で計算finally
juanmirocks

2
カレーdef time[R](label: String)(block: => R): R = {println
リング

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.

OK-パワーモードにする必要があります

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機能 REPLから削除されました:-\
ches

25

Scalaには、利用できる3つのベンチマークライブラリがあります。

リンク先サイトのURLは変更される可能性が高いため、以下の関連コンテンツを貼り付けております。

  1. SPerformance-パフォーマンステストを自動的に比較し、Simple Build Tool内で作業することを目的としたパフォーマンステストフレームワーク。

  2. scala-benchmarking-template -Caliperに基づいてScala(マイクロ)ベンチマークを作成するためのSBTテンプレートプロジェクト。

  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
Testing.Benchmarkは@deprecated( "このクラスは削除されます。"、 "2.10.0")であることに注意してください。
Tvaroh 2013

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

2つの関数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

私はコードブロック内を移動しやすい手法を使用しています。重要なのは、タイマーの開始と終了がまったく同じ行であるということです。つまり、実際には単純なコピーと貼り付けです。もう1つの優れた点は、タイミングの意味を文字列としてすべて同じ行に定義できることです。

使用例:

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

長所:

  • コードをブロックとしてラップしたり、行内で操作したりする必要はありません
  • 探索的である場合、タイマーの開始と終了をコード行間で簡単に移動できます

短所:

  • 完全に機能するコードの光沢が少ない
  • タイマーを「閉じない」場合、たとえば、コードが特定のタイマー開始の2番目の呼び出しに到達しない場合、このオブジェクトは明らかにマップエントリをリークします。

これは素晴らしいですが、次のような使い方はすべきではありませんTimelog.timer("timer name/description")
スクーン2017

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 Worksheetで上記のコードスニペットを実行すると、実行時間をミリ秒単位で取得します

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ホットスポットウォームアップループ(タイミングなし)が可能になります。

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 '27

1
@nedim解決策は質問に与えられます-あなたが時間を計りたいもののためのラッパー。OPが呼び出したい関数はラッパー、または関数を呼び出すブロックに配置できるため、「静的型付けを失うことなく、関数の前後に呼び出される関数の束を定義できます」
ブレントファウスト

1
あなたが正しいです。すみません、コードを見落としていたに違いありません。編集内容を確認したら、反対票を取り消すことができます。
nedim

3

Scalaコードのベンチマークに推奨されるアプローチは、sbt-jmhによるものです。

「誰も信用せず、すべてをベンチに掛けなさい。」-JMH(Java Microbenchmark Harness)のsbtプラグイン

このアプローチは、主要なScalaプロジェクトの多くで採用されています。たとえば、

  • Scalaプログラミング言語自体
  • Dotty(Scala 3)
  • 関数型プログラミングのためのライブラリ
  • IDE用のメタル言語サーバー

に基づく単純なラッパータイマーSystem.nanoTimeは、ベンチマークの信頼できる方法ではありません

System.nanoTimeString.intern今と同じくらい悪いです。あなたはそれを使うことができますが、それを賢く使ってください。タイマーによって導入されるレイテンシ、粒度、およびスケーラビリティの影響は、適切な厳密さなしに行われた場合、測定に影響する可能性があります。これは、System.nanoTimeフレームワークのベンチマークによってユーザーから抽象化する必要がある多くの理由の1つです

さらに、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付加し、最後にそれを反転することは、aに付加し続けるよりも桁違いに速いことを示しているようVectorです。


1

巨人の肩の上に立っている間...

堅牢なサードパーティライブラリの方が理想的ですが、すばやく標準ライブラリに基づいたものが必要な場合は、次のバリアントが提供します。

  • 繰り返し
  • 最後の結果が複数回の繰り返しで勝ちます
  • 複数回の繰り返しの合計時間と平均時間
  • パラメータとしての時間/インスタントプロバイダの必要性を排除

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を使用することをお勧めします。


ナノセカンドが見づらいのは、この2つの間の選択の理由としては不十分です。解像度以外にもいくつかの重要な違いがあります。まず、currentTimeMillisは、OSが定期的に実行するクロック調整中に変更されたり、逆戻りしたりする可能性があります。もう一つは、nanoTimeは、スレッドセーフではないかもしれないということです。stackoverflow.com/questions/351565/...
クリス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.