15

我对 haskell 有一定的了解,但我总是不确定应该使用什么样的编译指示和优化以及在哪里使用。像

  • 比如何时使用SPECIALIZEpragma 以及它有什么性能提升。
  • 在哪里使用RULES。我听说人们正在考虑不触发的特定规则?我们如何检查?
  • 什么时候使函数的参数严格,什么时候有帮助?我知道使参数严格会使参数被评估为正常形式,那么为什么我不应该对所有函数参数添加严格性呢?我该如何决定?
  • 如何查看和检查我的程序中是否存在空间泄漏?构成空间泄漏的一般模式是什么?
  • 如何查看是否存在懒惰过多的问题?我总是可以检查堆分析,但我想知道懒惰伤害的一般原因、例子和模式是什么?

是否有任何关于高级优化(无论是在较高级别还是在非常低级别)的资源,特别是针对 haskell 的?

4

2 回答 2

18

比如何时使用SPECIALIZEpragma 以及它有什么性能提升。

如果你有一个(类型类)多态函数,你让编译器专门化一个函数,并期望它经常在类的一个或几个实例上被调用。

专业化删除了使用它的字典查找,并且通常可以进一步优化,类成员函数通常可以被内联,并且它们受到严格性分析,两者都可能带来巨大的性能提升。如果唯一可能的优化是消除字典查找,那么收益通常不会很大。

从 GHC-7 开始,给函数一个{-# INLINABLE #-}pragma 可能更有用,这使得它的(几乎没有改变,执行一些规范化和去糖)源在接口文件中可用,所以函数可以被专门化,甚至可能内联在呼叫站点。

在哪里使用RULES。我听说人们正在考虑不触发的特定规则?我们如何检查?

您可以使用-ddump-rule-firings命令行选项检查触发了哪些规则。这通常会转储大量已触发的规则,因此您必须搜索一下您自己的规则。

你使用规则

  • 当您对特殊类型的函数有更高效的版本时,例如

    {-# RULES
    "realToFrac/Float->Double"  realToFrac   = float2Double
      #-}
    
  • 当某些函数可以替换为特殊参数的更有效版本时,例如

    {-# 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
      #-}
    
  • 当根据一般规律重写表达式时可能会产生更好优化的代码,例如

    {-# RULES
    "map/map"  forall f g. (map f) . (map g) = map (f . g)
      #-}
    

后一种风格的广泛使用RULES在融合框架中,例如在text库中,对于 中的列表函数,使用规则实现base了不同类型的融合(融合)。foldr/build

什么时候使函数的参数严格,什么时候有帮助?我知道使参数严格会使参数被评估为正常形式,那么为什么我不应该对所有函数参数添加严格性呢?我该如何决定?

使参数严格将确保它被评估为弱头范式,而不是范式。

您不要使所有参数都严格,因为某些函数的某些参数必须是非严格的才能工作,而如果所有参数都严格,有些函数的效率会降低。

例如,它的第二个参数必须是非严格的,才能在无限列表上工作,更一般地说,每个函数在第二个参数中必须 是非严格的,才能在无限列表上工作。在有限列表上,在第二个参数中使用 non-strict 函数可以显着提高效率 ( )。partitionfoldrfoldr (&&) True (False:replicate (10^9) True)

如果您知道必须先评估该论点,然后才能完成任何有价值的工作,那么您就可以使论点变得严格。在很多情况下,GHC 的严格度分析器可以自己完成,但当然不是全部。

一个非常典型的情况是循环或尾递归中的累加器,其中增加严格性可以防止在途中构建巨大的 thunk。

我不知道在哪里增加严格的硬性规则,对我来说这是一个经验问题,一段时间后你就会知道在哪些地方增加严格可能有帮助,在哪里有害。

根据经验,对小数据(如Int)进行评估是有意义的,但也有例外。

如何查看和检查我的程序中是否存在空间泄漏?构成空间泄漏的一般模式是什么?

第一步是使用该+RTS -s选项(如果该程序已启用 rtsopts 链接)。这显示了总体使用了多少内存,并且您通常可以判断您是否有泄漏。通过使用该选项运行程序可以获得更多信息输出,该+RTS -hT选项生成一个可以帮助定位空间泄漏的堆配置文件(此外,该程序需要与启用的 rtsopts 链接)。

如果需要进一步分析,则需要在启用分析的情况下编译程序(-rtsops -prof -fprof-auto在较旧的 GHC 中,该-fprof-auto选项不可用,该-prof-auto-all选项是那里最接近的对应关系)。

然后使用各种分析选项运行它并查看生成的堆配置文件。

空间泄漏的两个最常见原因是

  • 太懒惰
  • 过于严格

第三位可能是不需要的共享,GHC 几乎没有消除常见的子表达式,但它偶尔会共享长列表,即使在不需要的地方也是如此。

为了找到泄漏的原因,我再次知道没有硬性规定,有时,可以通过在一个地方增加严格性或在另一个地方增加惰性来修复泄漏。

如何查看是否存在懒惰过多的问题?我总是可以检查堆分析,但我想知道懒惰伤害的一般原因、例子和模式是什么?

通常,在可以逐步建立结果的情况下需要惰性,而在处理完成之前无法交付任何结果的情况下不需要惰性,例如在左折叠或通常在尾递归函数中。

于 2012-09-21T17:27:15.997 回答
3

我建议阅读有关PragmasRewrite Rules的 GHC 文档,因为它们解决了您关于 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 中。延迟处理资源本质上会增加所需资源的挂钟时间。这可能对缓存性能不利,如果其他人想要独占权使用相同的资源,这显然很糟糕。

于 2012-09-21T18:34:23.177 回答