首页 文章

总和类型函数参数的GHC调用约定

提问于
浏览
7

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 的三参数表示,一个参数表示使用的数据构造函数,另外两个表示数据构造函数中可能的 IntWord 值 .

我更喜欢第二种表示,但我不知道它是否真的发生了什么 . 我知道在GHC 8.2中登陆UnpackedSumTypes,但目前还不清楚它是否符合我的要求 . 如果我把函数写成:

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

那么我希望评估(2)会发生什么 . 并且解压缩的页面的Unpacking section表示我也可以这样做:

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

我认为那应该也有我想要的代表性 .

所以我的问题是,在不使用包装器类型或原始解压缩总和的情况下,当我将它作为参数传递给函数时,如何保证将总和解压缩到寄存器(或堆栈)?如果有可能,它是GHC 8.0已经可以做的事情,还是仅在GHC 8.2中可用的东西?

2 回答

  • 2

    第一:保证优化和GHC不能很好地混合 . 由于高水平,很难预测GHC在每种情况下都会产生的代码 . 唯一可以确定的方法是查看Core . 如果您正在使用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)
    

    设's first look at what happens if we don' t inline consumeSumproduceSum . 这是核心:

    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
    

    通过案例( 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之前没有任何“未装箱的”类型的概念,这也适用于函数参数 . 获得“未装箱”总和的唯一方法是通过内联完全消除构造函数 .

  • 7

    如果您需要这样的优化,最简单的解决方案就是自己完成 . 我认为实际上有很多方法可以做到这一点,但其中一个是:

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

    解压缩此类型的任何字段与'shape of the representation'的问题完全无关,这正是您真正要求的问题 . 枚举已经过高度优化 - 每个构造函数只创建一个值 - 因此添加此字段不会影响性能 . 这种类型的解压缩表示正是您想要的 - 一个单词用于 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 }
    

    几乎在每种情况下使用低级别的解压缩数据时都是目标,因为大多数功能都很小,内联成本很低 .

相关问题