Scala에서 메서드를 프로파일 링하는 방법은 무엇입니까?


117

Scala 메서드 호출을 프로파일 링하는 표준 방법은 무엇입니까?

내가 필요한 것은 메소드를 둘러싼 후크이며,이를 사용하여 타이머를 시작하고 중지 할 수 있습니다.

Java에서는 aspect 프로그래밍, aspectJ를 사용하여 프로파일 링 할 메소드를 정의하고 동일한 작업을 수행하기 위해 바이트 코드를 삽입합니다.

Scala에 더 자연스러운 방법이 있습니까? 프로세스에서 정적 유형을 잃지 않고 함수 전후에 호출 할 함수를 정의 할 수 있습니까?


AspectJ가 Scala와 잘 어울리면 AspectJ를 사용하세요. 바퀴를 재발 명하는 이유는 무엇입니까? 사용자 지정 흐름 제어를 사용하는 위의 답변은 코드를 수정해야하기 때문에 AOP의 기본 요구 사항을 달성하지 못합니다. 이것도 흥미로울
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 }

이것은 깔끔합니다. 코드 변경없이 똑같이 할 수 있습니까?
sheki

이 솔루션에서는 자동으로 수행되지 않습니다. 스칼라는 당신이 원하는 시간을 어떻게 알 수 있습니까?
Jesper 2012

1
이것은 엄격하게 사실이 아닙니다. REPL에서 자동으로 항목을 래핑 할 수 있습니다.
oxbow_lakes

1
거의 완벽하지만 가능한 예외에도 대응해야합니다. 계산은 t1내에서 finally
juanmirocks

2
약간의 커링을 사용하여 인쇄물에 레이블을 추가 할 수 있습니다. def time[R](label: String)(block: => R): R = {그런 다음 레이블을 추가 할 수 있습니다.println
Glenn 'devalias'Jan

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 용 벤치마킹 라이브러리 에는 세 가지 가 있습니다 .

링크 된 사이트의 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

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 Worksheet에서 위의 코드 조각을 실행하면 밀리 초 단위의 실행 시간을 얻을 수 있습니다.

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

3

나는 @wrick의 대답의 단순함을 좋아하지만 또한 원했습니다.

  • 프로파일 러는 루프를 처리합니다 (일관성과 편의성을 위해)

  • 보다 정확한 타이밍 (nanoTime 사용)

  • 반복 당 시간 (모든 반복의 총 시간이 아님)

  • ns / 반복 만 반환-튜플이 아님

이것은 여기에서 달성됩니다.

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
}

이 답변 아니라, 코멘트로 작성하는 가장 좋은 것입니다
네딤

1
@nedim 질문에 대한 해결책이 주어집니다. 시간을 내고 싶은 모든 것에 대한 래퍼입니다. OP가 호출하려는 모든 함수는 래퍼 또는 그의 함수를 호출하는 블록에 배치 할 수 있으므로 "정적 유형을 잃지 않고 함수 전후에 호출 할 함수를 정의 할 수 있습니다"
Brent Faust

1
당신이 옳습니다. 죄송합니다. 코드를 간과 한 것 같습니다. 내 편집 내용이 검토되면 반대 투표를 취소 할 수 있습니다.
nedim

3

Scala 코드 벤치마킹에 권장되는 접근 방식은 sbt-jmh를 사용하는 것입니다.

"아무도 믿지 말고 모든 것을 벤치마킹하십시오." -JMH 용 sbt 플러그인 (Java Microbenchmark Harness)

이 접근 방식은 많은 주요 Scala 프로젝트에서 사용됩니다.

  • 스칼라 프로그래밍 언어 자체
  • Dotty (Scala 3)
  • 함수형 프로그래밍을위한 고양이 라이브러리
  • 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하고 끝에서 되 돌리는 것이 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를 사용할 수 있습니다.


나노초는보기 어렵 기 때문에 둘 중 하나를 선택하는 데는 좋지 않습니다. 해상도 외에 몇 가지 중요한 차이점이 있습니다. 우선 currentTimeMillis는 OS가 주기적으로 수행하는 클럭 조정 중에 변경 될 수 있으며 심지어 뒤로 갈 수도 있습니다. 다른 하나는 nanoTime이 스레드로부터 안전하지 않을 수 있다는 것입니다. stackoverflow.com/questions/351565/…
Chris
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.