何时使用不同的语言 pragma 和优化?

15

我对Haskell有一定的了解,但常常不确定应该在何处使用哪种pragma和优化。比如说:

  • 何时使用SPECIALIZE pragma以及它能带来什么性能提升。
  • 何时使用RULES,人们谈论特定规则未触发?我们如何检查?
  • 何时使函数参数强制求值有所帮助?我知道强制求值会使得参数被求值为正常形式,那么为什么不在所有函数参数上都添加严格性呢?我该如何决策?
  • 如何查看和检查程序中是否存在空间泄漏?哪些一般模式构成了空间泄漏?
  • 如何检查过度的惰性是否存在问题?我可以随时检查堆分析,但我想知道惰性伤害的一般原因、示例和模式是什么?

是否有任何资源讨论高级优化(包括较高和非常低的水平),尤其是特定于Haskell?


RHW 第25章?http://book.realworldhaskell.org/read/profiling-and-optimization.html - Don Stewart
@DonStewart 谢谢您... 我已经读过RWH了.. 问题是我知道它们是做什么的,但我不知道它们的使用何时重要何时不重要。我问这个问题是为了找出其他来源以进一步增强我的理解。 - Satvik
2个回答

18
当你有一个(类型类)多态函数,并且期望它在类的一个或多个实例中被频繁调用时,你可以让编译器对其进行特化。
特化会移除使用处的字典查找,并且通常能够进一步优化,类成员函数通常可以被内联,而且它们会受到严格性分析的影响,这两者都可能带来巨大的性能提升。如果唯一可能的优化是消除字典查找,那么收益通常不会很大。
截至GHC-7,更有用的做法可能是给函数加上{-# INLINABLE #-} pragma,使其(几乎不变,只进行了一些规范化和解糖)源代码在接口文件中可用,因此函数可以被特化,甚至可能在调用点内联。
人们谈论特定规则未触发时应该在哪里使用RULES。我们如何检查?
您可以使用-ddump-rule-firings命令行选项来检查已触发的规则。通常会转储大量已触发的规则,因此您需要搜索一下自己的规则。
您使用规则。
  • when you have a more efficient version of a function for special types, e.g.

    {-# RULES
    "realToFrac/Float->Double"  realToFrac   = float2Double
      #-}
    
  • when some functions can be replaced with a more efficient version for special arguments, e.g.

    {-# RULES
    "^2/Int"        forall x. x ^ (2 :: Int) = let u = x in u*u
    "^3/Int"        forall x. x ^ (3 :: Int) = let u = x in u*u*u
    "^4/Int"        forall x. x ^ (4 :: Int) = let u = x in u*u*u*u
    "^5/Int"        forall x. x ^ (5 :: Int) = let u = x in u*u*u*u*u
    "^2/Integer"    forall x. x ^ (2 :: Integer) = let u = x in u*u
    "^3/Integer"    forall x. x ^ (3 :: Integer) = let u = x in u*u*u
    "^4/Integer"    forall x. x ^ (4 :: Integer) = let u = x in u*u*u*u
    "^5/Integer"    forall x. x ^ (5 :: Integer) = let u = x in u*u*u*u*u
      #-}
    
  • when rewriting an expression according to general laws might produce code that's better to optimise, e.g.

    {-# RULES
    "map/map"  forall f g. (map f) . (map g) = map (f . g)
      #-}
    
在融合框架中,例如在text库中以及base中的列表函数中广泛使用后一种风格的RULES。另一种不同类型的融合(foldr/build融合)也是使用规则实现的。

何时使函数参数严格,这样做有什么帮助?我知道使参数严格会使参数被计算为正常形式,那么为什么我不应该将严格性添加到所有函数参数中?我如何决定是否要使参数严格?

使参数严格将确保其被计算为弱头正常形式,而不是正常形式。
您不会使所有参数都严格,因为某些函数必须在某些参数中是非严格的才能正常工作,有些函数如果在所有参数中都是严格的,则效率较低。

对于 examplepartition 必须在其第二个参数上是非严格的才能在无限列表上工作,更一般地,foldr 中使用的每个函数都必须在第二个参数上是非严格的才能在无限列表上工作。在有限列表上,使函数在第二个参数上非严格化可能会使它变得极其高效(foldr (&&) True (False:replicate (10^9) True))。

如果您知道该参数在可行的工作之前必须被评估,则可以使参数强制执行。在许多情况下,GHC 的严格性分析器可以自行完成此操作,但当然不是所有情况都适用。

一个非常典型的案例是循环或尾递归中的累加器,其中添加严格性可以防止在路上构建巨大的thunk。

我没有硬性规定应该在哪里添加严格性,对我来说这是一种经验问题,经过一段时间后,您会学习到在哪些地方添加严格性可能有帮助,在哪些地方可能有害。

作为经验法则,保持小数据(例如Int)保持评估状态是有意义的,但也有例外情况。

我如何查看并检查程序中是否存在空间泄漏?构成空间泄漏的一般模式是什么?
第一步是使用+RTS -s选项(如果程序启用了rtsopts)。这将显示整个程序使用了多少内存,您通常可以通过此来判断是否存在泄漏。 更详细的输出可以通过使用+RTS -hT选项运行程序来获得,该选项生成堆文件,有助于定位空间泄漏(还需要链接启用了rtsopts的程序)。
如果需要进一步分析,则需要启用编译时的profiling(-rtsops -prof -fprof-auto,在旧版本的GHC中,-fprof-auto选项不可用,-prof-auto-all选项是最接近的对应项)。
然后您可以使用各种profiling选项运行它,并查看生成的堆文件。
空间泄漏的两个最常见原因是
1. 过度惰性 2. 过度严格
第三个原因可能是不必要的共享, GHC 很少进行公共子表达式消除,但它偶尔会共享长列表,即使不需要。
对于找到泄漏原因,我再次不知道硬性规定,有时,在一个地方添加严格性或在另一个地方添加惰性可以修复泄漏。
“如何查看是否存在过多惰性的问题?我总是可以检查堆分析,但我想知道惰性伤害的一般原因、示例和模式。”
通常,惰性是希望结果可以逐步构建的地方,而在没有处理完成之前无法传递结果的地方(例如在左折叠或通常在尾递归函数中)是不需要的。

3
我建议阅读 GHC 文档中关于 PragmasRewrite Rules 的内容,因为它们涉及到了 SPECIALIZE 和 RULES 的许多问题。
简要回答你的问题:
  • SPECIALIZE用于强制编译器为特定类型构建多态函数的专门版本。优点是在这种情况下应用函数将不再需要字典。缺点是它会增加程序的大小。对于在“内部循环”中调用的函数,专业化特别有价值,而对于不经常调用的顶级函数则基本无用。有关与INLINE的交互,请参阅GHC文档

  • RULES允许您指定您知道但编译器无法自行推断的重写规则。常见的例子是{-# RULES "mapfusion" forall f g xs. map f (map g xs) = map (f.g) xs #-},它告诉GHC如何融合map。由于与INLINE的干扰,让GHC使用规则可能会很棘手。7.19.3介绍了如何避免冲突以及如何在通常避免规则时强制GHC使用规则。

  • 严格参数对于像尾递归函数中的累加器这样的东西非常重要。您知道最终将完全计算该值,并且构建延迟计算的闭包堆栈完全违背了其目的。必须自然地避免强制执行严格性,因为函数可能应用于必须懒处理的值,例如无限列表。通常,最好的想法是最初仅在明显有用的地方(如累加器)强制执行严格性,然后仅在分析显示需要时添加更多。

  • 我的经验是,大多数导致空间泄漏的问题来自于非严格的累加器和非评估的大型数据结构中的懒惰值,尽管我确信这对您编写的程序类型是特定的。尽可能使用未打包数据结构可以解决许多问题。

  • 除了惰性导致空间泄漏的情况外,应避免使用惰性的主要情况是在IO中。惰性处理资源固有地增加了所需资源的墙钟时间量。这对缓存性能可能不利,如果其他东西想要独占使用相同的资源,则显然是不好的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接