2016-12-21 33 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 } 

其中,在当低层次的工作,解压后的数据几乎每一种情况,就是无论如何,因为你的大部分功能都很小,并且成本很低。