如何在Haskell中使CAF不是CAF?

时间:2011-05-22 21:34:57

标签: haskell ghc compiler-optimization

如何将常量适用表格制作成一个不变的适用表格,以阻止它在程序的整个生命周期内保留?

我尝试过这种方法:

-- | Dummy parameter to avoid creating a CAF
twoTrues :: () -> [[[Bool]]]
twoTrues _ = map (++ (True : repeat False)) . trueBlock <$> [1..]

但它似乎不起作用 - 配置文件显示它仍然保留并且仍然将其标记为CAF。

我已经找到了一个相关的Google结果,a reply by Simon Peyton-Jones给了Neil Mitchell,他正好问了这个问题 - 但不幸的是,这个答案指的是一个死链接。

7 个答案:

答案 0 :(得分:15)

完整示例

这是一个显示情况的小例子:

module A where

big :: () -> [Int]
big _ = [1..10^7]

看起来像一个功能,对吗?但是GHC做了什么?它将枚举浮动到顶层!

A.big1 :: [Int]
[ Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
         ConLike=False, Cheap=False, Expandable=False,
         Guidance=IF_ARGS [] 7 0}]
A.big1 =
  case A.$wf1 10 A.big2 of ww_sDD { __DEFAULT ->
  eftInt 1 ww_sDD
  }

A.big :: () -> [Int]
[Arity=1,    
 Unf=Unf{Src=InlineStable, TopLvl=True, Arity=1, Value=True,
         ConLike=True, Cheap=True, Expandable=True,
         Guidance=ALWAYS_IF(unsat_ok=True,boring_ok=True)
         Tmpl= \ _ -> A.big1}]
A.big = \ _ -> A.big1

哎呀!


那我们该怎么办?

关闭优化

有效,-Onot,但不可取:

A.big :: () -> [Int]
[GblId, Arity=1]
A.big =
  \ _ ->
    enumFromTo
      @ Int
      $fEnumInt
      (I# 1)
      (^
         @ Int
         @ Type.Integer
         $fNumInt
         $fIntegralInteger
         (I# 10)
         (smallInteger 7))

不要内联,更多功能

所有内容添加到一个函数中,包括enumFromTo,将参数传递给worker:

big :: () -> [Int]
big u = myEnumFromTo u 1 (10^7)
{-# NOINLINE big #-}

myEnumFromTo :: () -> Int -> Int -> [Int]
myEnumFromTo _ n m = enumFromTo n m
{-# NOINLINE myEnumFromTo #-}

现在我们终于无CAF了!即使使用-O2

A.myEnumFromTo [InlPrag=NOINLINE]
  :: () -> Int -> Int -> [Int]
A.myEnumFromTo =
  \ _ (n_afx :: Int) (m_afy :: Int) ->
    $fEnumInt_$cenumFromTo n_afx m_afy

A.big [InlPrag=NOINLINE] :: () -> [Int]
A.big = \ (u_abx :: ()) -> A.myEnumFromTo u_abx A.$s^2 lvl3_rEe

耶。


什么不起作用?

关闭-ffull-laziness

完整的懒惰转换向外浮动定义。它默认为-O1或更高版本。我们尝试使用-fno-full-laziness将其关闭。但是,它不起作用。

答案 1 :(得分:8)

概括。如果你有一个常量值,你可以将它推广到某个变量的函数吗?在twoTrues问题中命名我的函数会立即表明此常量是序列中的第三个zeroTruesoneTruetwoTruesthreeTrues等 - 确实如此。因此,将twoTrues推广到函数nTrues 中,该函数采用参数n 并删除twoTrues,将从程序中消除一个CAF。

碰巧,在这种情况下,我只考虑了案例zeroTruesoneTruetwoTrues,因为这就是我所需要的,但我的程序自然可以延伸至nTrues&gt;处理n 2 - 如此推广到nTrues意味着对zeroTruesoneTrue等用户“一直向上推广”是有意义的。但情况并非总是如此。< / p>

注意:可能仍有其他CAF需要处理,无论是在代码中,还是由GHC的“优化”(在这些病态情况下都不是真正的优化)产生的。

然而,这个答案可能涉及程序员的更多工作,而不是严格必要的工作。正如唐的答案所示,实际上并没有必要概括。

另一方面,在某些情况下,推广常量可以使您更清楚您实际在做什么,并有助于可重用性。它甚至可以揭示以更好的系统方式和/或更有效地计算一系列值的方法。

关于这个特殊情况的说明(可以忽略):在这种特殊情况下,我不想让nTrues 本身成为无限列表(这将是一个CAF再次,重新引入原始问题!)而不是一个功能。一个原因是虽然twoTrues可能以无限列表的形式有用,但我无法看到nTrues以{{1}}的形式在我的应用程序中是如何有用的(在我的应用程序中)无限的名单。

答案 2 :(得分:5)

通过引入虚拟参数,您还必须使其看起来实际上取决于参数。否则,GHC的聪明才智可能会再次成为CAF。

我建议如下:

twoTrues u = map (++ (True : repeat False)) . trueBlock <$> [(u `seq` 1)..]

答案 3 :(得分:5)

这似乎是一个长期存在的问题http://hackage.haskell.org/trac/ghc/ticket/917。在我看来,这是一个关键的。

答案 4 :(得分:3)

您需要隐藏rhs是优化程序中CAF的事实。 这样的事情应该做到。

twoTrues :: () -> [[[Bool]]]
twoTrues u = map (++ (True : repeat (false u))) . trueBlock <$> [1..]

{-# NOINLINE false #-}
false :: () -> Bool
false _ = False

答案 5 :(得分:0)

最简单的解决方案可能是告诉编译器内联它。 (注意:这个答案是未经测试的。如果它不起作用,请在下面评论。)

即使(假设)编译器由于某种原因拒绝内联它,您也可以使用cpp代替#defining:

#define twoTrues (map (++ (True : repeat False)) . trueBlock <$> [1..])

(当然,使用这种方法,它不会出现在模块的界面中,因此您只能在该模块中使用它。)

-cpp选项告诉GHC使用cpp。

预处理输入文件

如果您在 n&gt; 1 位置引用twoTrues,则内联将意味着重复代码 n 次。然而,虽然这在理论上是不受欢迎的,但在实践中它可能不会成为一个重大问题。

答案 6 :(得分:0)

每当您使用()作为参数时,您要说的实际上是

  

虽然我在这里声明了一个参数,但我对它的含义并不感兴趣,而且我不会对它的价值做任何事情。

你并不感兴趣,因为()没有任何有趣的东西;你不会对它做任何事情,因为你对()无能为力。

问题在于编译器有权优化它,因为只有一个可能的值要传递,所以它的使用总是可以预测的,所以为什么不假设它?但它将它移回CAF并使这个想法不起作用。

幸运的是,还有另一种方法可以做到这一点。请查看twoTrues的以下修改:

twoTrues :: a -> [[[Bool]]]
twoTrues _ = map (++ (True : repeat False)) . trueBlock <$> [1..]

现在您可以像这样使用twoTrues

map concat $ twoTrues()

由于a是未使用的类型参数,因此调用者可以传递任何内容。因为你不知道它会是什么,所以你不知道你能用它做什么。这实际上是迫使你忽略它的价值。所以它基本上声明了我之前提到的相同陈述。

原因是,您现在可以将任何内容(包括undefined)传递给该函数。但这并不重要,实际上这种可能性使得这个技巧可行,因为编译器无法再预测这个函数的使用方式。当人类用户看到这个功能时,他们应该知道你在这里要说什么,并总结传递()是最简单的,但即使他们没有并传递其他东西,它也不会破坏任何东西,因为Haskell是懒惰的附加参数根本无法评估。

那么如果()被用作结果呢?这更糟糕。因为返回()意味着你的函数根本不做任何事情(在Haskell中,函数的所有效果都应该在它的返回值中表示),编译器有权断定你的函数是不必要的。

结论是,()作为一种类型不应出现在类型签名中,除非与其他类型一起使用(即在IO ()中)。

修改

现在有人可能想知道,如果只有一种方法可以从a -> String实现String,为什么编译器不能断定它们是相同的。答案结果是你实际上有两种方法来实现它。

usual :: a -> String
usual _ = "Hello World!"

unusual :: a -> String
unusual a = seq a "Hello World!"

对于几乎所有输入,usual value = unusual valueusual undefined"Hello World!",而unusual undefinedundefined

从人的角度来看,unusual非常不寻常,因为它会强制评估与最终结果无关的值。如果在任何情况下你确实需要这样的话,那么简单地调用seq会更容易。此外,由于Haskell在默认情况下是懒惰的,如果要定义严格的函数,则最好记录此行为。因此,如果您在没有其他文档的情况下看到此类签名,则您有权假设它以usual方式实现。