2016-12-21 46 views
7

GHC在將總和類型傳遞給函數時是否解包過?例如,讓我們說,我們有以下類型:總和類型函數參數的GHC調用約定

data Foo 
    = Foo1 {-# UNPACK #-} !Int {-# UNPACK #-} !Word 
    | Foo2 {-# UNPACK #-} !Int 
    | Foo3 {-# UNPACK #-} !Word 

然後我定義一個函數,在其Foo參數嚴格:

consumeFoo :: Foo -> Int 
consumeFoo x = case x of ... 

在運行時,當我打電話consumeFoo,我能預計會發生? GHC calling convention是在寄存器中傳遞參數(或者在堆棧太多時在堆棧上)。我可以看到兩種方法可以傳遞參數:

  1. 指向堆上的Foo的指針作爲一個參數傳入。
  2. 使用Foo的三個參數表示法,一個參數表示使用的數據構造函數,另外兩個表示數據構造函數中可能的IntWord值。

我寧願第二個表示,但我不知道它是否實際上會發生什麼。我知道UnpackedSumTypes登陸GHC 8.2,但目前還不清楚它是否符合我的要求。如果我已經寫了這樣的功能:

consumeFooAlt :: (# (# Int#, Word# #) | Int# | Word# #) -> Int 

然後,我期望評估(2)將會發生什麼。而拆包金額頁的Unpacking section表明,我能做到這一點還有:

data Wrap = Wrap {-# UNPACK #-} !Foo 
consumeFooAlt2 :: Wrap -> Int 

而這也應該有我想要的表現,我想。

所以我的問題是,沒有使用包裝類型或原始解壓縮的總和,我怎麼能保證,當我把它作爲參數傳遞給一個函數時,一個和解被解壓到寄存器(或堆棧中)?如果可能的話,GHC 8.0已經可以做什麼,或者它只能在GHC 8.2中使用?

回答

7

第一:保證優化和GHC混合不好。由於高水平,很難預測GHC在每種情況下都會產生的代碼。唯一確定的方法就是看看Core。如果你正在開發一個性能與GHC性能相關的應用程序,那麼你需要熟悉核心I.

我不知道GHC中的任何優化都完全符合你的描述。下面是一個例子程序:

module Test where 

data Sum = A {-# UNPACK #-} !Int | B {-# UNPACK #-} !Int 

consumeSum :: Sum -> Int 
consumeSum x = case x of 
    A y -> y + 1 
    B y -> y + 2 

{-# NOINLINE consumeSumNoinline #-} 
consumeSumNoinline = consumeSum 

{-# INLINE produceSumInline #-} 
produceSumInline :: Int -> Sum 
produceSumInline x = if x == 0 then A x else B x 

{-# NOINLINE produceSumNoinline #-} 
produceSumNoinline :: Int -> Sum 
produceSumNoinline x = if x == 0 then A x else B x 

test :: Int -> Int 
--test x = consumeSum (produceSumInline x) 
test x = consumeSumNoinline (produceSumNoinline x) 

讓我們在,如果我們不內聯consumeSum也不produceSum會發生什麼先看看。這裏是核心:

test :: Int -> Int 
test = \ (x :: Int) -> consumeSumNoinline (produceSumNoinline x) 

(與ghc-core test.hs -- -dsuppress-unfoldings -dsuppress-idinfo -dsuppress-module-prefixes -dsuppress-uniques生產)

在這裏,我們可以看到,GHC(在這種情況下8.0)不拆箱作爲函數參數傳遞的總和類型。如果我們內聯consumeSumproduceSum,則沒有任何變化。

但是如果我們內嵌兩個,然後將下面的代碼生成:

test :: Int -> Int 
test = 
    \ (x :: Int) -> 
    case x of _ { I# x1 -> 
    case x1 of wild1 { 
     __DEFAULT -> I# (+# wild1 2#); 
     0# -> lvl1 
    } 
    } 

這裏發生了什麼的是通過內聯,GHC與結束:

\x -> case (if x == 0 then A x else B x) of 
    A y -> y + 1 
    B y -> y + 2 

它通過案例的-case(if只是一個特殊的case)變成:

\x -> if x == 0 then case (A x) of ... else case (B x) of ... 

現在是用已知構造的情況下,這樣GHC可以在編譯時間縮短的情況下與結束了:

\x -> if x == 0 then x + 1 else x + 2 

所以完全消除的構造。


綜上所述,筆者認爲,GHC沒有之前的8.2版本,這也適用於函數參數的「拆箱和」類型的任何概念。獲得「拆箱」總和的唯一方法是通過內聯完全消除構造函數。

2

如果您需要這樣的優化,您最簡單的解決方案就是自己做。 我覺得其實有很多方法可以實現這一點,但一個是:

data Which = Left | Right | Both 
data Foo = Foo Which Int Word 

這種類型的任何字段的拆包是完全無關的,「表示的形狀」的問題,這就是你真的在問。枚舉已經高度優化 - 每個構造函數都只創建一個值 - 所以添加此字段不會影響性能。這種類型的解包表示正是你想要的 - 一個字爲Which構造函數,每個字段一個。

如果你寫你的功能,在適當的方式,你得到的正確代碼:

data Which = Lft | Rgt | Both 
data Foo = Foo Which {-# UNPACK #-} !Int {-# UNPACK #-} !Word 

consumeFoo :: Foo -> Int 
consumeFoo (Foo w l r) = 
    case w of 
    Lft -> l 
    Rgt -> fromIntegral r 
    Both -> l + fromIntegral r 

生成的核心是相當明顯的:

consumeFoo :: Foo -> Int 
consumeFoo = 
    \ (ds :: Foo) -> 
    case ds of _ { Foo w dt dt1 -> 
    case w of _ { 
     Lft -> I# dt; 
     Rgt -> I# (word2Int# dt1); 
     Both -> I# (+# dt (word2Int# dt1)) 
    } 
    } 

然而,對於簡單的程序,如:

consumeFoos = foldl' (+) 0 . map consumeFoo 

這種優化沒有區別。正如在其他的答案是指出,內部功能consumeFoo是內聯:

Rec { 
$wgo :: [Foo] -> Int# -> Int# 
$wgo = 
    \ (w :: [Foo]) (ww :: Int#) -> 
    case w of _ { 
     [] -> ww; 
     : y ys -> 
     case y of _ { 
      Lft dt -> $wgo ys (+# ww dt); 
      Rgt dt -> $wgo ys (+# ww (word2Int# dt)); 
      Both dt dt1 -> $wgo ys (+# ww (+# dt (word2Int# dt1))) 
     } 
    } 
end Rec } 

Rec { 
$wgo :: [Foo] -> Int# -> Int# 
$wgo = 
    \ (w :: [Foo]) (ww :: Int#) -> 
    case w of _ { 
     [] -> ww; 
     : y ys -> 
     case y of _ { Foo w1 dt dt1 -> 
     case w1 of _ { 
      Lft -> $wgo ys (+# ww dt); 
      Rgt -> $wgo ys (+# ww (word2Int# dt1)); 
      Both -> $wgo ys (+# ww (+# dt (word2Int# dt1))) 
     } 
     } 
    } 
end Rec } 

其中,在當低層次的工作,解壓後的數據幾乎每一種情況,就是無論如何,因爲你的大部分功能都很小,並且成本很低。