2014-08-30 46 views
4

我建立一個簡單的例子來檢查@inline標註行爲:製作方法其實內聯

import scala.annotation.tailrec 

object InlineTest extends App { 
    @inline 
    private def corec(x : Int) : Int = rec(x - 1) 

    @tailrec 
    private def rec(x : Int) : Int = 
    if (x < 3) x else { 
     if (x % 3 == 0) 
     corec(x-1) 
     else 
     rec(x-4) 
    } 

    @tailrec 
    private def rec1(x : Int) : Int = 
    if (x < 3) x else { 
     if (x % 3 == 0) { 
     val arg = x - 1 
     rec1(arg - 1) 
     } else 
     rec1(x-4) 
    } 

    Console.println(rec(args(0).toInt)) 
} 

這個例子編譯時沒有警告,無論tailrec註釋是有效,因爲我以爲。但是當我真的執行代碼時,它給了我一個stackoverflow異常。這意味着由於內聯方法沒有內聯,尾遞歸優化失敗。

我有從原來的不同之處僅手動地執行「內聯」變換控制功能rec1。由於這個函數按預期運行良好,避免了使用尾遞歸的stackoverflow異常。

是什麼阻止註釋的方法被內聯?

回答

7

這確實不行。 @inline的處理方式比尾部調用的處理方式晚,因此阻止了您希望的優化。

在編譯器的所謂的「tailcalls」階段,編譯器試圖使得尾遞歸調用取出,並通過環取代,以變換方法。請注意,只能通過這種方式消除同一功能內的呼叫。所以,在這裏,編譯器將能夠在功能rec轉換成能或多或少等同於以下:

@tailrec 
    private def rec(x0: Int) : Int = 
    var x: Int = x0 
    while (true) { 
     if (x < 3) x else { 
     if (x % 3 == 0) 
      return corec(x-1) 
     else { 
      x = x-4 
      continue 
     } 
     } 
    } 

注意,要corec呼叫沒有消除,因爲你調用的另一種方法。當時在編譯器中,@inline註解甚至沒有看到。

但爲什麼不編譯器警告你它不能消除通話,因爲你已經把@tailrec?因爲它只檢查它是否實際上能夠將所有調用替換爲當前方法。它不關心計劃中其他的方法是否調用rec(即使該方法,在這種情況下corec,本身被稱爲rec)。

所以,你得到任何警告,但仍叫corec不是尾部調用消除。

當你在編譯器管道後的@inline治療,並假設它確實選擇跟隨你的建議內聯corec,它將再次修改rec內聯的corec身體,這給出了以下:

@tailrec 
    private def rec(x0: Int) : Int = 
    var x: Int = x0 
    while (true) { 
     if (x < 3) x else { 
     if (x % 3 == 0) 
      return rec((x-1) - 1) 
     else { 
      x = x-4 
      continue 
     } 
     } 
    } 

決不再介意這將創建一個新的尾遞歸調用。太晚了。它不會被淘汰。因此,堆棧溢出。

您可以使用TailCalls對象及其方法將相互尾遞歸方法變成循環。見details in the API

+0

我知道互相遞歸沒有被scala編譯器優化。所以我選擇它來檢查函數內聯。如果內聯工作,那麼遞歸將被優化,如果不起作用,那麼該方法將失敗,並帶有stackoverflow。此外,如果內聯註釋對優化JVM有任何益處,那麼這裏有幾個問題。我認爲內聯可能會打開進一步優化的大門,但事實上,我認爲這太多了。 – ayvango 2014-08-30 13:17:42

1

請注意,有一個建議正在進行中(scala PR 567),以引入實際的inline關鍵字。 (9月2016)

請參閱 「SIP NN: Inline Definitions and Meta Expressions (Public Draft)

它將爲工作:

具體數值定義,例如

inline val x = 4 

具體方法,例如的內聯方法,例如

inline def square(x: Double) = x * x 

參數標記的內嵌

inline def pow(b: Double, inline n: Int): Double = { 
    if (n == 0) 1 
    else pow(b, n - 1) 
} 

價值和方法定義是有效的最後;他們不能被覆蓋。
內聯成員也不會覆蓋其他成員。相反,每個內聯成員都成爲所有其他具有相同名稱的成員的重載替代方案。

內聯定義僅在編譯時存在;在對象佈局中沒有爲它們分配存儲,也沒有在對象方法表中爲它們生成代碼。
這意味着可以有一個內聯成員與其他具有相同名稱的成員具有相同的類型擦除。