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是在寄存器中传递参数(或者在存在太多时在栈上传递参数)。我可以看到论证传递的两种方式:
Foo
的指针作为一个参数传入。Foo
的三参数表示,一个参数表示使用的数据构造函数,另外两个表示数据构造函数中可能的Int
和Word
值。 我更喜欢第二种表示,但我不知道它是否真的发生了什么。我知道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中可用的东西?
答案 0 :(得分:7)
第一:保证优化和GHC混合不好。由于高水平,很难预测GHC在每种情况下都会产生的代码。唯一可以确定的方法是看核心。如果您正在使用GHC开发一个性能极其依赖的应用程序,那么您需要熟悉Core 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)不会取消作为函数参数传递的和类型。如果我们内联consumeSum
或produceSum
,则不会发生任何变化。
如果我们内联两者,则会生成以下代码:
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
通过案例(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版之前没有任何“unboxed sum”类型的概念,它也适用于函数参数。获得“未装箱”总和的唯一方法是通过内联完全消除构造函数。
答案 1 :(得分: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 }
VS。
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 }
在几乎所有使用低级别解压缩数据的情况下,无论如何都是目标,因为您的大多数功能都很小,内联成本很低。