【问题标题】:GHC Calling Convention for Sum Type Function ArgumentsSum 类型函数参数的 GHC 调用约定
【发布时间】:2023-04-06 07:01:01
【问题描述】:

GHC 是否在将 sum 类型传递给函数时对其进行解包?例如,假设我们有以下类型:

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 才有的事情?

【问题讨论】:

    标签: haskell ghc


    【解决方案1】:

    首先:保证优化和 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)
    

    让我们首先看看如果我们不内联 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)没有拆箱作为函数参数传递的 sum 类型。如果我们内联 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-of-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 }
      

      在处理低级、未打包的数据时,几乎在所有情况下,这都是目标,因为您的大多数函数都很小而且内联成本很低。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-10-24
        • 1970-01-01
        相关资源
        最近更新 更多