【问题标题】:Coaxing loop-invariant code motion out of GHC从 GHC 中哄骗循环不变的代码运动
【发布时间】:2012-04-26 21:15:07
【问题描述】:

我一直在为 GHC 中的低级手动循环优化而苦苦挣扎。我的程序包含一些执行数值计算的循环。真实数据被包装在其他数据结构中,程序被分解为“循环控制流函数”和“计算函数”,这样一些数据结构字段最终会在内部循环中被读取。我希望 GHC 将这些读数移出内部循环。这是代码的简化版本,用于显示发生了什么。

data D = D !Double !C
data C = C Double

-- This function is called in every loop iteration.
-- Parameter 'c' is loop-invariant.
exampleLoopBody i a c =
  case c of C b -> a + b * fromIntegral i

-- The body of this function is a counted loop that should be optimized
foo x =
  case x
  of D acc0 c ->
    let loop i acc =
          if i > 100
          then acc
          else loop (i+1) (exampleLoopBody i acc c)
    in loop 0 acc0

每次循环迭代都会计算case c of C b,但这是冗余计算,因为c 是循环不变的。我可以通过在循环之外放置一个冗余的 case 表达式来让 GHC 将其解除:

foo x =
  case x
  of D acc0 c ->
    case c             -- This case statement inserted for optimization purposes
    of C b -> b `seq`  -- It will read 'b' outside of the loop
      let loop i acc =
           if i > 100
           then acc
           else loop (i+1) (exampleLoopBody i acc c)
      in loop 0 acc0

编译器内联exampleLoopBody。之后,内部 case 语句是多余的并被消除:

foo x =
  case x
  of D acc0 c ->
    case c
    of C b -> b `seq`
      let loop i acc =
            if i > 100
            then acc
            else loop (i+1) (acc + b * fromIntegral i) -- The inlined case expression disappears
      in loop 0 acc0

seq 的目的是确保 case 表达式不是死代码。 seq 检查b 是否为_|_。 GHC 注意到,由于 b 已被计算,因此在循环体中重用该值很有用。

现在,问题来了:我真的希望所有相关的数据字段都是严格的。如果我在数据定义中插入严格注释,像这样,

data C = C !Double

那么就 GHC 而言,seqcase c of C b 无效。 GHC 删除了它们,我明白了:

foo x =
  case x
  of D acc0 c ->
    let loop i acc =
          if i > 100
          then acc
          else loop (i+1) (case c of C b -> acc + b * fromIntegral i) -- Evaluate the case in every iteration
     in loop 0 acc0

这段代码在每次迭代中都会评估case c of C b,这正是我试图避免的。

如果我不能依赖seq,我不知道如何强制b 在循环体之外计算。在这种情况下我可以使用一些技巧吗?

【问题讨论】:

  • 您使用的是哪个 GHC 版本?在 7.4.1 和 7.2.2 的每次迭代中,如果没有 case,我都会得到很好的核心。纯未装箱Double#s.
  • @DanielFischer 我使用的是 7.0.3,在通过简化程序后,Doubles 之一被装箱。仅供参考,我的真实用例实际上涉及静态大小的向量,例如Cons Double (Cons Double Nil)。我可以尝试在较新的 GHC 中运行它,看看会发生什么。
  • 嗯,我也得到了 7.0.4 的未装箱循环。 $wfoo 工人有一个装箱的参数(也有 7.4.1 和 7.2.2),但循环本身是 letreced 在里面并采用未装箱的 Double# (另一个 Double# 是静态的,甚至)。
  • 是的,$wfoo 为我提供了一个装箱参数,循环获取并返回未装箱的 Double#s。 $wfoo 的 boxed 参数被循环体内的 case 拆箱。
  • 对我来说,case 在循环体之外。它可能是 32 位与 64 位的东西吗?你的操作系统是什么?

标签: optimization haskell


【解决方案1】:

您可以尝试重新排列参数并将循环变体部分移动到 lambda 中:

-- note the order of the arguments changed
exampleLoopBody (C b) =
  \i a -> a + b * fromIntegral i

foo (D acc0 c) =
    let
       loopBody = exampleLoopBody c 
       loop i acc =
          if i > 100
         then acc
         else loop (i+1) (loopBody i acc)
   in loop 0 acc0

此外,这段代码此时会构建一个大型未计算表达式,因此您可能希望每次循环都强制使用累加器参数。

【讨论】:

    【解决方案2】:

    这看起来基本上是 newtype 被放入该语言的全部原因。只需将data C = C !Double 更改为newtype C = C Double 并编写原始版本的代码。 C 类型值上的所有 case 表达式都将被删除。作为旁注,您在示例中的代码模式:

    case foo of
        D acc0 c -> case c of
            C b -> ...
    

    可以写得更简洁:

    case foo of
        D acc0 (C b) -> ...
    

    【讨论】:

    • 我在提问时可能过于简化了我的问题。在实际程序中,foo 是一个多态函数。 c 的类型不同,可能有多个字段。
    猜你喜欢
    • 2016-05-25
    • 2018-08-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多