何时使用各种语言编译指示和优化?

时间:2012-09-21 16:07:44

标签: performance optimization haskell pragma

我对haskell有一点了解,但我总是不确定我应该使用什么样的pragma和优化以及在哪里。喜欢

  • 喜欢何时使用SPECIALIZE pragma以及它有什么性能提升。
  • 使用地点RULES。我听说人们采取了不解雇的特定规则?我们如何检查?
  • 何时使函数的参数严格,何时有帮助?我理解,使参数严格将使参数被评估为正常形式,那么为什么我不应该对所有函数参数添加严格性?我该如何决定?
  • 如何查看并检查我的程序中是否有空格泄漏?构成空间泄漏的一般模式是什么?
  • 如何查看是否有太多懒惰的问题?我总是可以检查堆分析,但我想知道懒惰会伤害的一般原因,示例和模式是什么?

是否有任何关于高级优化(包括更高和更低级别)的消息来源,特别是对于haskell?

2 个答案:

答案 0 :(得分:18)

  

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

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

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

从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 fusion)是使用规则实现的。

  

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

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

你没有使所有参数都严格,因为有些函数在某些参数中必须是非严格的,而且如果在所有参数中都是严格的,那么某些函数的效率会降低。

对于example partition,其第二个参数必须是非严格的才能在无限列表中工作,更常见的是foldr中使用的每个函数在第二个参数中都必须是非严格的在无限列表上工作的论据。在有限列表中,在第二个参数中使用非严格函数可以使其显着提高效率(foldr (&&) 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几乎不会消除常见的子表达式,但它偶尔会在不需要的地方共享长列表。

为了找到泄漏的原因,我再次知道没有严格的规则,偶尔可以通过在一个地方增加严格性或在另一个地方添加懒惰来解决泄漏。

  

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

一般来说,需要懒惰才能逐步建立结果,并且在处理完成之前无法传递结果的部分,例如左侧折叠或尾部递归函数。

答案 1 :(得分:3)

我建议您阅读PragmasRewrite Rules上的GHC文档,因为它们解决了有关SPECIALIZE和RULES的许多问题。

简要回答您的问题:

  • SPECIALIZE用于强制编译器为特定类型构建特定版本的多态函数。优点是在这种情况下应用函数将不再需要字典。缺点是它会增加程序的大小。专门化对于“内循环”中调用的函数特别有价值,对于不经常调用的顶级函数它实际上是无用的。有关与INLINE的互动,请参阅GHC documentation

  • 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。懒惰地处理资源固有地增加了所需资源的挂钟时间。这可能对缓存性能有害,如果其他东西想要使用相同资源的专有权,这显然是不好的。