Làm thế nào để cấu hình các phương pháp trong Scala?


117

Một cách chuẩn để cấu hình các cuộc gọi phương thức Scala là gì?

Những gì tôi cần là kết nối xung quanh một phương pháp, sử dụng mà tôi có thể sử dụng để bắt đầu và dừng Bộ hẹn giờ.

Trong Java, tôi sử dụng lập trình khía cạnh, khía cạnhJ, để xác định các phương thức được cấu hình và chèn bytecode để đạt được điều tương tự.

Có cách nào tự nhiên hơn trong Scala, nơi tôi có thể xác định một loạt các hàm được gọi trước và sau một hàm mà không mất bất kỳ thao tác nhập tĩnh nào trong quá trình này không?


Nếu AspectJ chơi tốt với Scala, hãy sử dụng AspectJ. Tại sao phải phát minh lại bánh xe? Các câu trả lời sử dụng điều khiển luồng tùy chỉnh ở trên không đạt được các yêu cầu cơ bản của AOP vì để sử dụng chúng, bạn cần sửa đổi mã của mình. Những điều này cũng có thể được quan tâm: java.dzone.com/articles/real-world-scala-managing-cros blog.fakod.eu/2010/07/26/cross-cutting-concerns-in-scala
Ant Kutschera


Bạn quan tâm đến điều gì? Bạn có muốn biết một phương pháp nhất định mất bao lâu trong môi trường sản xuất. Sau đó, bạn nên xem thư viện số liệu và không tự đo lường cuộn như trong câu trả lời được chấp nhận. Nếu bạn muốn điều tra xem biến thể mã nào nhanh hơn "nói chung", tức là trong môi trường phát triển của bạn, hãy sử dụng sbt-jmh như được trình bày bên dưới.
jmg

Câu trả lời:


214

Bạn có muốn làm điều này mà không thay đổi mã mà bạn muốn đo thời gian không? Nếu bạn không ngại thay đổi mã, thì bạn có thể làm điều gì đó như sau:

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 }

Điều này thật gọn gàng, tôi có thể làm điều tương tự mà không cần thay đổi mã nào không?
sheki

Không tự động với giải pháp này; Làm thế nào để Scala biết bạn muốn tính giờ?
Jesper

1
Đây không phải là đúng sự thật - bạn có thể tự động quấn thứ trong REPL
oxbow_lakes

1
Gần như hoàn hảo, nhưng bạn cũng phải phản ứng khi có những trường hợp ngoại lệ. Tính t1trong vòng một finallykhoản
juanmirocks

2
Bạn có thể thêm một nhãn để in của bạn với một số currying: def time[R](label: String)(block: => R): R = {sau đó thêm nhãn vàoprintln
Glenn 'devalias'

34

Ngoài câu trả lời của Jesper, bạn có thể tự động gói các lời gọi phương thức trong 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

Bây giờ - hãy gói bất cứ thứ gì vào cái này

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

OK - chúng ta cần ở chế độ nguồn

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

Kết thúc

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

Tôi không hiểu tại sao những thứ được in ra 5 lần

Cập nhật kể từ 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
Để tránh cho bất kỳ ai khỏi thắc mắc bây giờ, :wraptính năng này đã bị xóa khỏi REPL: - \
ches

25

ba thư viện đo điểm chuẩn cho Scala mà bạn có thể sử dụng.

Vì các URL trên trang được liên kết có thể thay đổi, tôi sẽ dán nội dung có liên quan bên dưới.

  1. SPerformance - Khung kiểm tra hiệu suất nhằm so sánh tự động các bài kiểm tra hiệu suất và hoạt động bên trong Công cụ xây dựng đơn giản.

  2. scala-benchmark-benchmark-template - Dự án mẫu SBT để tạo các điểm chuẩn Scala (vi mô) dựa trên Calibre.

  3. Chỉ số - Nắm bắt các chỉ số JVM và cấp ứng dụng. Vì vậy, bạn biết những gì đang xảy ra


21

Đây là những gì tôi sử dụng:

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 có thể hữu ích.

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
Hãy lưu ý rằng testing.Benchmark là @deprecated ("Lớp này sẽ bị xóa.", "2.10.0").
Tvaroh

5

Tôi lấy giải pháp từ Jesper và thêm một số tổng hợp vào nó trên nhiều lần chạy cùng một mã

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

Giả sử bạn muốn bấm giờ cho hai hàm counter_newcounter_oldcách sử dụng như sau:

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

Hy vọng rằng điều này là hữu ích


4

Tôi sử dụng một kỹ thuật dễ di chuyển trong các khối mã. Điểm mấu chốt là cùng một dòng chính xác bắt đầu và kết thúc bộ đếm thời gian - vì vậy nó thực sự là một bản sao và dán đơn giản. Một điều tốt đẹp khác là bạn có thể xác định thời gian có ý nghĩa như thế nào đối với bạn dưới dạng một chuỗi, tất cả đều nằm trong cùng một dòng.

Ví dụ sử dụng:

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

Mật mã:

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

Ưu điểm:

  • không cần phải bọc mã dưới dạng một khối hoặc thao tác trong các dòng
  • có thể dễ dàng di chuyển bắt đầu và kết thúc bộ đếm thời gian giữa các dòng mã khi đang khám phá

Nhược điểm:

  • kém sáng bóng hơn cho mã chức năng hoàn toàn
  • rõ ràng đối tượng này làm rò rỉ các mục bản đồ nếu bạn không "đóng" bộ hẹn giờ, ví dụ: nếu mã của bạn không nhận được lệnh gọi thứ hai cho một lần bắt đầu bộ hẹn giờ nhất định.

Điều này thật tuyệt, nhưng không nên sử dụng Timelog.timer("timer name/description"):?
schoon

4

ScalaMeter là một thư viện tuyệt vời để thực hiện đo điểm chuẩn trong Scala

Dưới đây là một ví dụ đơn giản

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

Nếu bạn thực thi đoạn mã trên trong Scala Worksheet, bạn sẽ có thời gian chạy tính bằng mili giây

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

3

Tôi thích sự đơn giản trong câu trả lời của @ wrick, nhưng cũng muốn:

  • hồ sơ xử lý vòng lặp (để nhất quán và thuận tiện)

  • thời gian chính xác hơn (sử dụng nanoTime)

  • thời gian mỗi lần lặp (không phải tổng thời gian của tất cả các lần lặp)

  • chỉ trả về ns / lần lặp - không phải là một bộ

Điều này đạt được ở đây:

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

Để có độ chính xác cao hơn nữa, một sửa đổi đơn giản cho phép vòng lặp khởi động Điểm phát sóng JVM (không tính thời gian) để định thời gian cho các đoạn trích nhỏ:

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
}

Đây không phải là một câu trả lời, nó sẽ là tốt nhất để viết nó như một bình luận
Nedim

1
@nedim Giải pháp được đưa ra cho câu hỏi - một trình bao bọc cho bất cứ thứ gì bạn muốn về thời gian. Bất kỳ hàm nào OP muốn gọi có thể được đặt trong trình bao bọc hoặc trong khối gọi các hàm của anh ta để anh ta "có thể xác định một loạt các hàm được gọi trước và sau một hàm mà không mất bất kỳ thao tác nhập tĩnh nào"
Brent Faust

1
Bạn đúng rồi. Xin lỗi, tôi phải đã bỏ qua mã. Khi chỉnh sửa của tôi được xem xét, tôi có thể hoàn tác phản đối.
nedim

3

Cách tiếp cận được đề xuất để đánh giá mã Scala điểm chuẩn là thông qua sbt-jmh

"Không tin tưởng ai, băng ghế mọi thứ." - plugin sbt cho JMH (Java Microbenchmark khai thác)

Cách tiếp cận này được thực hiện bởi nhiều dự án Scala lớn, ví dụ,

  • Bản thân ngôn ngữ lập trình Scala
  • Dotty (Scala 3)
  • thư viện mèo để lập trình chức năng
  • Máy chủ ngôn ngữ kim loại cho IDE

Đơn giản wrapper timer dựa trên System.nanoTimekhông phải là một phương pháp đáng tin cậy của điểm chuẩn:

System.nanoTimetệ như String.internbây giờ: bạn có thể sử dụng nó, nhưng hãy sử dụng nó một cách khôn ngoan. Các hiệu ứng về độ trễ, độ chi tiết và khả năng mở rộng do bộ hẹn giờ đưa vào có thể và sẽ ảnh hưởng đến các phép đo của bạn nếu được thực hiện mà không có sự nghiêm ngặt thích hợp. Đây là một trong nhiều lý do tại sao System.nanoTimenên được tóm tắt từ người dùng bằng các khung điểm chuẩn

Hơn nữa, các cân nhắc như khởi động JIT , thu gom rác, các sự kiện trên toàn hệ thống, v.v. có thể đưa ra các phép đo không thể đoán trước :

Rất nhiều tác động cần được giảm thiểu, bao gồm khởi động, loại bỏ mã chết, phân nhánh, v.v. May mắn thay, JMH đã đảm nhận nhiều thứ và có các ràng buộc cho cả Java và Scala.

Dựa trên câu trả lời của Travis Brown, đây là một ví dụ về cách thiết lập điểm chuẩn JMH cho Scala

  1. Thêm jmh vào project/plugins.sbt
    addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
  2. Bật plugin jmh trong build.sbt
    enablePlugins(JmhPlugin)
  3. Thêm vào 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. Thực thi điểm chuẩn với
    sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 bench.VectorAppendVsListPreppendAndReverse"

Kết quả là

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

điều này dường như cho biết việc thêm trước vào a Listvà sau đó đảo ngược nó ở cuối là thứ tự cường độ nhanh hơn so với việc tiếp tục thêm vào a Vector.


1

Khi đứng trên vai những người khổng lồ ...

Một thư viện của bên thứ 3 chắc chắn sẽ lý tưởng hơn, nhưng nếu bạn cần thứ gì đó nhanh chóng và dựa trên thư viện std, thì biến thể sau sẽ cung cấp:

  • Lặp lại
  • Kết quả cuối cùng thắng nếu lặp lại nhiều lần
  • Tổng thời gian và thời gian trung bình cho nhiều lần lặp lại
  • Loại bỏ nhu cầu về nhà cung cấp thời gian / tức thì như một tham số

.

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

Cũng cần lưu ý rằng bạn có thể sử dụng Duration.toCoarsestphương pháp để chuyển đổi sang đơn vị thời gian lớn nhất có thể, mặc dù tôi không chắc điều này thân thiện như thế nào với chênh lệch thời gian nhỏ giữa các lần chạy, ví dụ:

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

Bạn có thể sử dụng 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
}

Sử dụng:

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

nanoTime sẽ hiển thị cho bạn ns, vì vậy bạn sẽ khó thấy. Vì vậy, tôi đề nghị bạn có thể sử dụng currentTimeMillis thay vì nó.


Khó nhìn thấy nano giây là một lý do kém để chọn giữa hai. Có một số khác biệt quan trọng bên cạnh độ phân giải. Thứ nhất, currentTimeMillis có thể thay đổi và thậm chí quay ngược lại trong quá trình điều chỉnh đồng hồ mà hệ điều hành thực hiện định kỳ. Khác là nanoTime có thể không được chủ đề an toàn: stackoverflow.com/questions/351565/...
Chris
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.