Scala'da yöntemler nasıl profillenir?


117

Scala yöntem çağrılarını profillemenin standart bir yolu nedir?

İhtiyacım olan şey, Zamanlayıcıları başlatmak ve durdurmak için kullanabileceğim bir yöntemin etrafındaki kancalar.

Java'da, profili oluşturulacak yöntemleri tanımlamak ve aynı şeyi elde etmek için bayt kodu enjekte etmek için en-boy oranı programlamasını (directionJ) kullanıyorum.

Scala'da, süreçte herhangi bir statik yazımı kaybetmeden bir işlevden önce ve sonra çağrılacak bir dizi işlevi tanımlayabileceğim daha doğal bir yol var mı?


AspectJ, Scala ile iyi çalışıyorsa AspectJ'yi kullanın. Neden tekerleği yeniden icat etmelisiniz? Yukarıdaki özel akış kontrolünü kullanan cevaplar, AOP'nin temel gereksinimlerini karşılayamaz çünkü bunları kullanmak için kodunuzu değiştirmeniz gerekir. Bunlar da ilgi çekici olabilir: java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-concerns-in-scala
Ant Kutschera


Ne ile ilgileniyorsun? Üretim ortamında belirli bir yöntemin ne kadar sürdüğünü bilmek ister misiniz? O zaman metrik kitaplıklarına bakmalı ve kabul edilen cevapta olduğu gibi ölçümü kendiniz almamalısınız. Hangi kod değişkeninin "genel olarak" daha hızlı olduğunu araştırmak istiyorsanız, yani geliştirme ortamınızda, aşağıda gösterildiği gibi sbt-jmh kullanın.
jmg

Yanıtlar:


214

Zamanlamalarını ölçmek istediğiniz kodu değiştirmeden bunu yapmak ister misiniz? Kodu değiştirmenin bir sakıncası yoksa, şöyle bir şey yapabilirsiniz:

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 }

Bu harika, herhangi bir kod değişikliği yapmadan aynı şeyi yapabilir miyim?
sheki

Bu çözümle otomatik olarak değil; Scala neye zaman ayırmak istediğinizi nasıl bilecek?
Jesper

1
Bu kesinlikle doğru değil - her şeyi otomatik olarak
REPL'e sarabilirsiniz

1
Neredeyse mükemmel, ancak olası istisnalara da tepki vermelisiniz. Hesapla t1bir dahilinde finallymaddesi
juanmirocks

2
Baskılarınıza biraz def time[R](label: String)(block: => R): R = {println
köri ile bir

34

Jesper'ın cevabına ek olarak, REPL'de yöntem çağrılarını otomatik olarak kaydırabilirsiniz:

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

Şimdi - buna her şeyi saralım

scala> :wrap time
wrap: no such command.  Type :help for help.

Tamam - güç modunda olmamız gerekiyor

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

Sarın

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

Neden 5 kez basılmış şeyler hakkında hiçbir fikrim yok

2.12.2'den itibaren güncelleme:

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
Şimdi merak etme zahmetinden kurtulmak için, :wrapözellik REPL'den kaldırıldı : - \
ches

25

Scala için yararlanabileceğiniz üç kıyaslama kitaplığı vardır .

Bağlantı verilen sitedeki URL'ler büyük olasılıkla değişeceğinden, ilgili içeriği aşağıya yapıştırıyorum.

  1. SPerformance - Performans testlerini otomatik olarak karşılaştırmayı ve Basit Yapım Aracı içinde çalışmayı amaçlayan Performans Testi çerçevesi.

  2. scala-benchmarking-template - Calipere dayalı Scala (mikro) kıyaslamaları oluşturmak için SBT şablon projesi.

  3. Metrikler - JVM ve uygulama düzeyindeki ölçümleri yakalama. Yani neler olduğunu biliyorsun


21

Kullandığım şey bu:

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 faydalı olabilir.

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
Test.Benchmark'ın @deprecated olduğunu unutmayın ("Bu sınıf kaldırılacaktır.", "2.10.0").
Tvaroh

5

Çözümü Jesper'dan aldım ve aynı kodun birden fazla çalıştırılmasında ona biraz toplama ekledim

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

Eğer zaman iki işlevi istediğinizi varsayalım counter_newve counter_oldaşağıdaki kullanım şöyledir:

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

Umarım bu yardımcı olur


4

Kod bloklarında hareket etmesi kolay bir teknik kullanıyorum. İşin püf noktası, aynı satırın zamanlayıcıyı başlatması ve bitirmesidir - yani bu gerçekten basit bir kopyalayıp yapıştırmadır. Diğer güzel şey de, zamanlamanın sizin için ne anlama geldiğini aynı satırda bir dizge olarak tanımlamanızdır.

Örnek kullanım:

Timelog("timer name/description")
//code to time
Timelog("timer name/description")

Kod:

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

Artıları:

  • Kodu bir blok olarak sarmaya veya satırlar içinde işlem yapmaya gerek yok
  • keşif amaçlı iken zamanlayıcının başlangıcını ve sonunu kod satırları arasında kolayca taşıyabilir

Eksileri:

  • tamamen işlevsel kod için daha az parlak
  • Açıkçası bu nesne, zamanlayıcıları "kapatmazsanız", örneğin kodunuz belirli bir zamanlayıcı başlangıcı için ikinci çağrıya ulaşmazsa harita girişlerini sızdırır.

Bu harika ama kullanım olmamalı: Timelog.timer("timer name/description")?
schoon

4

ScalaMeter , Scala'da kıyaslama yapmak için güzel bir kütüphanedir

Aşağıda basit bir örnek var

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 Çalışma Sayfasında yukarıdaki kod parçacığını çalıştırırsanız, çalışma süresini milisaniye cinsinden alırsınız.

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

3

@ Wrick'in cevabının basitliğini beğendim ama aynı zamanda şunu da istedim:

  • profil oluşturucu döngüyü işler (tutarlılık ve rahatlık için)

  • daha doğru zamanlama (nanoTime kullanarak)

  • yineleme başına süre (tüm yinelemelerin toplam süresi değil)

  • sadece ns / yineleme döndür - bir demet değil

Bu, burada elde edilir:

def profile[R] (repeat :Int)(code: => R, t: Long = System.nanoTime) = { 
  (1 to repeat).foreach(i => code)
  (System.nanoTime - t)/repeat
}

Daha da fazla doğruluk için basit bir değişiklik, küçük parçacıkların zamanlaması için bir JVM Hotspot ısınma döngüsüne (zamanlanmamış) izin verir:

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
}

Bu bir cevap değil, yorum olarak yazmak en iyisi
nedim

1
@nedim Çözüm soruya verilir - zamanlamak istediğiniz herhangi bir şey için bir paketleyici. OP'nin çağırmak isteyeceği herhangi bir işlev sarmalayıcıya veya işlevlerini çağıran bloğa yerleştirilebilir, böylece "herhangi bir statik yazmayı kaybetmeden bir işlevden önce ve sonra çağrılacak bir dizi işlevi tanımlayabilir"
Brent Faust

1
Haklısın. Üzgünüm, kodu gözden kaçırmış olmalıyım. Düzenlemem gözden geçirildiğinde, olumsuz oyu geri alabilirim.
nedim

3

Scala kodunu karşılaştırmak için önerilen yaklaşım sbt-jmh yoluyladır.

"Kimseye güvenmeyin, her şeyi yedekleyin." - JMH için sbt eklentisi (Java Microbenchmark Harness)

Bu yaklaşım, büyük Scala projelerinin çoğu tarafından benimsenmiştir, örneğin,

  • Scala programlama dilinin kendisi
  • Dotty (Scala 3)
  • fonksiyonel programlama için cats kütüphanesi
  • IDE'ler için metal dil sunucusu

Basit sarmalayıcı zamanlayıcı System.nanoTime, güvenilir bir kıyaslama yöntemi değildir :

System.nanoTimeşu String.internanki kadar kötü : Kullanabilirsiniz, ancak akıllıca kullanın. Zamanlayıcılar tarafından sunulan gecikme, ayrıntı düzeyi ve ölçeklenebilirlik etkileri, uygun titizlik olmadan yapılırsa ölçümlerinizi etkileyebilir ve etkileyecektir. Bu, System.nanoTimeçerçeveler karşılaştırılarak kullanıcılardan soyutlanması gereken birçok nedenden biridir.

Ayrıca, JIT ısınması , çöp toplama, sistem çapında olaylar vb. Gibi hususlar, ölçümlere öngörülemezlik getirebilir :

Isınma, ölü kodların ortadan kaldırılması, çatallanma vb. Dahil olmak üzere tonlarca etkinin azaltılması gerekir. Neyse ki, JMH zaten birçok şeyle ilgileniyor ve hem Java hem de Scala için bağları var.

Travis Brown'ın cevabına dayanarak, burada Scala için JMH karşılaştırmasının nasıl ayarlanacağına dair bir örnek verilmiştir

  1. Jmh ekle project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. Jmh eklentisini etkinleştirin build.sbt
    enablePlugins(JmhPlugin)
  3. a ekle 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. Karşılaştırmayı şununla yürütün:
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

Sonuçlar

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

Bu, a'nın başına eklenmesi Listve sonunda tersine çevrilmesinin , a'ya eklemeye devam etmekten daha hızlı olduğunu gösterir Vector.


1

Devlerin omuzlarında dururken ...

Sağlam bir 3. taraf kitaplığı daha ideal olacaktır, ancak hızlı ve standart kitaplık tabanlı bir şeye ihtiyacınız varsa, aşağıdaki değişken şunları sağlar:

  • tekrarlar
  • Son sonuç , birden fazla tekrar için kazanır
  • Birden fazla tekrar için toplam süre ve ortalama süre
  • Zaman / anında sağlayıcı ihtiyacını bir parametre olarak ortadan kaldırır

.

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

Ayrıca Duration.toCoarsest, mümkün olan en büyük zaman birimine dönüştürmek için yöntemi kullanabileceğinizi de belirtmek gerekir , ancak bunun ne kadar kolay olduğundan emin değilim.

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

Şunları kullanabilirsiniz 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
}

Kullanımı:

time{
    //execute somethings here, like methods, or some codes.
}  

nanoTime size gösterecek ns, bu yüzden görmek zor olacaktır. Bu yüzden bunun yerine currentTimeMillis'i kullanmanızı öneririm.


Nanosaniyelerin görülmesi zor, ikisi arasında seçim yapmak için zayıf bir neden. Çözünürlük dışında bazı önemli farklılıklar var. Birincisi, currentTimeMillis, işletim sisteminin periyodik olarak gerçekleştirdiği saat ayarlamaları sırasında değişebilir ve hatta geriye gidebilir. Bir diğeri ise nanoTime'ın iş parçacığı için güvenli olmayabileceğidir: stackoverflow.com/questions/351565/…
Chris
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.