Як профілювати методи у Scala?


117

Який стандартний спосіб профілювання викликів методу Scala?

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

У Java я використовую програмування аспектів, аспектJ, щоб визначити методи, які потрібно профілювати, і ввести байт-код, щоб досягти того ж.

Чи існує більш природний спосіб у 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 }

Це акуратно, чи можу я зробити те ж саме без будь-якої зміни коду?
шекі

Не автоматично з цим рішенням; як Скала знає, що ви хочете встигнути?
Jesper

1
Це не зовсім вірно - ви можете автоматично загортати речі у відповідь
oxbow_lakes

1
Майже ідеально, але ви повинні реагувати і на можливі винятки. Обчисліть t1в рамках finallyпункту
juanmirocks

2
Ви можете додати етикетку до своїх відбитків за допомогою десерту: def time[R](label: String)(block: => R): R = {потім додайте етикетку доprintln
Glenn 'devalias'

34

Окрім відповіді Джеспера, ви можете автоматично перегорнути виклики методу у відповідь:

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

8
Щоб вибачити когось із проблем дивуватися зараз, :wrapфункція була видалена з REPL: - \
ches

25

Для Scala є три бібліотеки бенчмаркінгу, якими ви можете скористатися.

Оскільки URL-адреси на пов’язаному веб-сайті, ймовірно, змінюються, я вставляю відповідний вміст нижче.

  1. SPerformance - Рамка тестування продуктивності, спрямована на автоматичне порівняння тестів продуктивності та роботи всередині простого інструменту побудови.

  2. scala-benchmarking-template - шаблон проекту SBT для створення Scala (мікро-) орієнтирів на основі суппорта.

  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
Майте на увазі, що тестування.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"):?
школа

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

Це не відповідь, найкраще було б написати це як коментар
неділя

1
@nedim Рішення задається питанням - обгорткою для всього, що ви хочете вчасно. Будь-які функції, на які хотів би зателефонувати ОП, можуть бути розміщені в обгортці або в блоці, який викликає його функції, щоб він "міг визначити купу функцій, які потрібно викликати до і після функції, не втрачаючи статичного набору тексту"
Brent Faust

1
Ти правий. Вибачте, я, мабуть, пропустив код. Коли моя редакція буде переглянута, я можу скасувати протокол.
недім

3

Рекомендований підхід до тестування коду Scala здійснюється через sbt-jmh

"Нікому не довіряйте, все лавіть". - плагін sbt для JMH (Java Microbenchmark Harness)

Такого підходу застосовують багато великих проектів Scala, наприклад,

  • Сама мова програмування Scala
  • Dotty (Scala 3)
  • бібліотека для котів для функціонального програмування
  • Мовний сервер металів для IDE

Проста обгортка таймер на основі System.nanoTimeє не надійні методом бенчмаркінг:

System.nanoTimeтак само погано, як String.internзараз: ти можеш ним користуватися, але використовувати розумно. Ефекти затримки, зернистості та масштабованості, що вводяться таймерами, можуть і впливатимуть на ваші вимірювання, якщо це зробити без належної суворості. Це одна з багатьох причин, чому System.nanoTimeслід абстрагуватися від користувачів, використовуючи схеми тестування

Крім того, такі аспекти , як розминка JIT , збирання сміття, загальносистемні події тощо, можуть внести непередбачуваність у вимірювання:

Тони ефектів потрібно пом'якшити, включаючи розминку, усунення мертвого коду, розгортання тощо. На щастя, JMH вже піклується про багато речей і має прив’язки як для Java, так і для Scala.

На основі відповіді Тревіса Брауна ось приклад того, як встановити контрольний показник JMH для Scala

  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

що, здається, вказує на попереднє додавання до Listа потім повернення його в кінці на порядок швидше, ніж продовжувати додавати до а Vector.


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> 

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 може змінюватися та навіть йти назад під час регулювання годин, які ОС періодично виконує. Інше - nanoTime може не бути безпечним для потоків: stackoverflow.com/questions/351565/…
Кріс,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.