2013-10-07 54 views
4

斯卡拉的REPL是一個美妙的遊樂場,可以交互地測試某些代碼段。最近,我一直在使用REPL進行一些性能比較,以反覆執行操作和相對測量掛鐘時間。將Scala的REPL用於比較性能基準測試是否合理?

這是我最近爲了幫助回答的SO問題,這樣的例子[1] [2]:

// Figure out the perfomance difference between direct method invocation and reflection-based method.invoke 

def invoke1[T,U](obj:Any, method:Method)(param:T):U = method.invoke(obj,Seq(param.asInstanceOf[java.lang.Object]):_*) match { 
    case x: java.lang.Object if x==null => null.asInstanceOf[U] 
    case x => x.asInstanceOf[U] 
} 

def time[T](b: => T):(T, Long) = { 
    val t0 = System.nanoTime() 
    val res = b 
    val t = System.nanoTime() - t0 
    (res,t) 
} 

class Test { 
    def op(l:Long): Long = (2 until math.sqrt(l).toInt).filter(x=>l%x==0).sum 
} 

val t0 = new Test 

val method = classOf[Test].getMethods.find(_.getName=="op").get 

def timeDiff = { 
    val (timeDirectCall,res) = time { (0 to 1000000).map(x=>t0.op(x)) } 
    val (timeInvoke, res2) = time { (0 to 1000000).map(x=>{val res:Long=invoke1(t0,method)(x);res}) } 
    (timeInvoke-timeDirectCall).toDouble/timeDirectCall.toDouble 
} 


//scala> timeDiff 
//res60: Double = 2.1428745665357445 
//scala> timeDiff 
//res61: Double = 2.1604176409796683 

在我已經生成隨機的數據點的MM併發模型比較了一個又一個的情況下開源項目。 REPL非常適合在沒有代碼編譯測試周期的情況下使用不同的配置。

我知道常見的基準缺陷,如JIT優化和熱身需求。

我的問題是:

  • 有沒有考慮到任何REPL特定元素使用 當執行宏觀基準的比較微觀?

  • 這些測量值在相對使用時是否可靠?即他們可以回答這個問題:A快於B

  • 編譯器是否預先執行了相同的代碼,並編譯好了jit ?

  • 要注意的其他問題?

[1] Scala reflection: How to pass an object's method as parameter to another method

[2] https://gist.github.com/maasg/6808879

+0

REPL將你的代碼包裝到它自己的內環中(所以你可以重新定義vals/vars/functions/classes/objects並做其他討厭的事情),所以基本上你要測量的是編譯代碼的時間,包裝它的時間最後由於[一堆理由],實際執行時間充滿了不同的波動(但你說你知道最後一個組件不可靠)。 **顯然這種測量不可靠**。 –

+0

@ om-nom-nom換行和編譯基本上是一次性的,這會佔用一些開銷,但是對於任何正在測試的選項來說,這將是相同的開銷,所以相對分數應該仍然具有代表性,或者不是?例如在上面的例子中,它顯示了一個粗略的2倍慢,這是足夠的信息。 – maasg

回答

6

這是一個很大的問題。我無法想象爲什麼有人會低估它。

其中一條評論是完全錯誤的這一事實表明REPL需要在scala-lang.org的常見問題解答或教程中提供一個地方。快速搜索後我找不到描述性文件。

答案是肯定的,REPL做你期望的。

Here is an old page爲什麼這個問題很有趣:REPL感覺是動態的,但實際上是靜態編譯的。正如對鏈接頁面的即時評論所言,它「跨越兩個世界」。

REPL將每一行編譯到它自己的包裝對象中。每個這樣的對象從交互式會話的歷史記錄中導入符號,這就是代碼如何神奇地引用回前面的行。所有東西都被編譯,所以當它運行時,它在JVM上本地運行,可以這麼說;沒有一個額外的解釋器層。這是REPL的殺手級設計功能。

這就是爲什麼你的問題的答案是肯定的,你的代碼以編譯代碼的速度運行。調用方法不需要重新編譯所有歷史記錄。

Here's another old link顯示其他人對時序和微基準標記有同樣的問題。

目前有an open issue可以定製REPL如何包裝代碼行。微代碼標記是一個有趣的用例,代碼可以包裝在任意框架中進行基準測試。即將推出。

基準框架應該照顧熱身。由於提交給REPL的每個表達式都是單獨編譯的(雖然是由同一個編譯器編譯的),但您會注意到第一次可以調用一個方法並且第二次調用方法(通過scalac進行模內聯)。

警告:

使用-Yrepl-class-based還是要小心,不要把計算在包裝對象的靜態初始化。

Here is some sample confusionhere is the same question,隱藏較少。

+0

感謝您的偉大的答案和指針。據你所知,代碼':paste''d和行之間是否有區別,就像它被封裝一樣?我應該更喜歡另一種方法嗎? – maasg

+0

@maasg粘貼的代碼被包裝在單個對象和編譯單元中(這就是爲什麼必須粘貼伴隨對象)。在2.11中:加載文件是逐行加載的,但是:粘貼文件是開庭的。我只是將我的-i init.script更改爲:加載imports.script,這比編譯每行更快。引用對象需要通常的$ MODULE deref,但幾乎不會影響性能。所以有一些編譯時邊緣情況,但是在該級別的運行時沒有開銷。 –

+0

typo:s /:顯然加載imports.script /:粘貼imports.script。加速repl啓動的一種方法是將初始化時的運行次數減少爲1次。 –