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.