如何将常量适用表格制作成一个不变的适用表格,以阻止它在程序的整个生命周期内保留?
我尝试过这种方法:
-- | 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,他正好问了这个问题 - 但不幸的是,这个答案指的是一个死链接。
答案 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
问题中命名我的函数会立即表明此常量是序列中的第三个zeroTrues
,oneTrue
,twoTrues
,threeTrues
等 - 确实如此。因此,将twoTrues
推广到函数nTrues
中,该函数采用参数n 并删除twoTrues
,将从程序中消除一个CAF。
碰巧,在这种情况下,我只考虑了案例zeroTrues
,oneTrue
和twoTrues
,因为这就是我所需要的,但我的程序自然可以延伸至nTrues
&gt;处理n
2 - 如此推广到nTrues
意味着对zeroTrues
,oneTrue
等用户“一直向上推广”是有意义的。但情况并非总是如此。< / 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 value
,usual undefined
为"Hello World!"
,而unusual undefined
为undefined
。
从人的角度来看,unusual
非常不寻常,因为它会强制评估与最终结果无关的值。如果在任何情况下你确实需要这样的话,那么简单地调用seq
会更容易。此外,由于Haskell在默认情况下是懒惰的,如果要定义严格的函数,则最好记录此行为。因此,如果您在没有其他文档的情况下看到此类签名,则您有权假设它以usual
方式实现。