Как профилировать методы в Scala?

95

Что такое стандартный способ профилирования вызовов методов Scala?

Мне нужны крючки вокруг метода, используя которые я могу использовать для запуска и остановки таймеров.

В Java я использую аспектное программирование, aspectJ, чтобы определить методы для профилирования и вставить байт-код для достижения того же.

Есть ли более естественный способ в Scala, где я могу определить кучу функций, которые будут вызываться до и после функции без потери статического ввода в процессе?

Теги:
function
profiling
aspect

11 ответов

158
Лучший ответ

Вы хотите сделать это, не изменяя код, для которого вы хотите измерить тайминги? Если вы не возражаете изменить код, вы можете сделать что-то вроде этого:

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 }
  • 0
    Это здорово, могу ли я сделать то же самое без какого-либо изменения кода?
  • 0
    Не автоматически с этим решением; как бы Scala узнала, на что бы ты хотел рассчитывать?
Показать ещё 5 комментариев
31

В дополнение к ответу 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
  • 7
    Чтобы избавить кого-то от беспокойства, функция :wrap была удалена из REPL: - \
  • 2
    @ches см. обновление для $intp.setExecutionWrapper .
19

Есть три библиотеки сравнения для Scala, которые вы можете использовать.

Поскольку URL-адреса на связанном сайте могут измениться, я вставляю соответствующий контент ниже.

  • SPerformance - платформа тестирования производительности, нацеленная на автоматическое сравнение тестов производительности и работы внутри Simple Build Tool.

  • scala-benchmarking-template - проект шаблона SBT для создания Scala (микро-) эталонных тестов на основе суппорта.

  • Metrics - Захват метрик JVM и уровня приложения. Итак, вы знаете, что происходит

17

Это то, что я использую:

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

Плюсы:

  • Не нужно обертывать код как блок или манипулировать внутри строк
  • может легко перемещать начало и конец таймера между линиями кода при его поисковом

Минусы:

  • менее блестящий для абсолютно функционального кода
  • очевидно, что этот объект течет на карте, если вы не закрываете таймеры, например если ваш код не попадает во второй вызов для заданного запуска таймера.
  • 0
    Это здорово, но разве не следует использовать: Timelog.timer("timer name/description") ?
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 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
}
  • 0
    Это не ответ, лучше написать его как комментарий.
  • 1
    @nedim Решение дается на вопрос - обертка для всего, что вы хотите время. Любые функции, которые ОП хотел бы вызвать, могут быть помещены в обертку или в блок, который вызывает его функции, так что он «может определить группу функций, вызываемых до и после функции, без потери статической типизации»
Показать ещё 1 комментарий
2

Я принял решение от 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

Надеюсь, это полезно

1

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
1

Стоя на плечах гигантов...

Твердая сторонняя библиотека была бы более идеальной, но если вам нужно что-то быстрое и основанное на std-библиотеке, следующий вариант обеспечивает:

  • Повторения
  • Последний результат выигрывает для нескольких повторений
  • Общее время и среднее время для нескольких повторений
  • Устраняет необходимость поставщика времени/времени в качестве параметра

.

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> 
0

Вы можете использовать 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.

  • 0
    Трудно увидеть наносекунды - плохая причина для выбора между ними. Есть несколько важных отличий, помимо разрешения. Например, currentTimeMillis может измениться и даже вернуться назад во время настройки часов, которую периодически выполняет ОС. Другая причина в том, что nanoTime не может быть потокобезопасным: stackoverflow.com/questions/351565/…

Ещё вопросы

Сообщество Overcoder
Наверх
Меню