【问题标题】:Is there an optimization similar to loop unroll for functional programming?是否有类似于函数式编程的循环展开的优化?
【发布时间】:2013-09-30 03:31:21
【问题描述】:

免责声明:我对 ghc 编译管道知之甚少,但我希望通过这篇文章了解更多信息,例如,比较命令式与函数式是否与代码编译相关。

如您所知,loop unrolling 通过复制其中的代码来减少循环的迭代次数。这提高了性能,因为它减少了跳转次数(以及与之相关的惩罚)和 AFAIR,创建了更大的代码块,为更好的Register renaming 优化留出了空间。

我想知道,函数式编程是否有与循环展开等效的方法?我们能否“展开”一个函数,打开/扩展它的定义,首先减少对所述函数的调用次数和/或创建更大的代码块——然后为更多代码重写优化留出空间(如寄存器重命名或一些 FP等价的)?

会“展开”或“扩展”函数定义的东西,例如使用函数评估(可能与某种策略混合)以便在空间与时间之间进行权衡。

我想到的一个例子:

 map1 _ [] = []
 map1 f (x:xs) = (f x): map f xs

将展开到

map2 _ [] = []
map2 f (x:x1:xs) = (f x):(f x1):map2 f xs
map2 f (x:xs) = (f x): map2 f xs

再一次:

map4 _ [] = []
map4 f (x:x1:x2:x3:xs) = (f x):(f x1):(f x2):(f x3):map4 f xs
map4 f (x:x1:x2:xs) = (f x):(f x1):(f x2):map4 f xs
map4 f (x:x1:xs) = (f x):(f x1):map4 f xs
map4 f (x:xs) = (f x): map4 f xs

有两件事在起作用:map4 的多个案例(以及列表上的后续测试)可能会降低性能,或者减少 map4 的调用次数会提高性能。也许这可以减少惰性评估造成的一些持续开销?

Well that doesn't seems to hard to code a test for,所以在推出这个标准之后,这就是我所拥有的:

ImgUr album

Problem size 5*10^6

map  105.4 ms
map2 93.34 ms
map4 89.79 ms

Problem size 1*10^7

map  216.3 ms
map2 186.8 ms
map4 180.1 ms

Problem size 5*10^7

map  1050 ms
map2 913.7 ms
map4 899.8 ms

嗯,展开似乎有些效果^1! map4 似乎快了 16%。

接下来是提问时间:

  1. 以前是否讨论过这个问题?类似的东西已经实现了吗?
  2. 真的是减少 map4 的评估次数提高了速度吗?
  3. 这可以自动化吗?
  4. 我可以按块进行评估吗?即:如果 (f x) 被完全评估,则完全评估 (f x4) 之前的所有内容。
  5. 这种展开还有其他形式吗?
  6. 这会导致函数大小的膨胀程度如何?
  7. 关于为什么这不是一个好主意的短评?

1:我也展开了 fib,因为这种优化也会以这种形式发生,但性能提升是在欺骗(非常)糟糕的算法。

【问题讨论】:

  • 你的意思是内联吗?因为听起来你的意思是内联。在这种情况下是的,GHC 像大多数高性能编译器一样积极内联,不管是什么语言。
  • 我不认为内联做我所说的,尽管是相似的。 AFAIU,内联将内联 f in (f x),通过减少函数调用有效地提高速度,但以牺牲代码大小为代价。但是 IMO 它不会在 (f x):(map f xs) 中内联映射,因为它的行为与 f 非常不同。另外,我的测试代码是用 -O 编译的,所以即使我误认为内联行为,它也不会起作用。
  • 我不相信这会有所作为。使用O2 编译会导致它们都给出相同的结果。展开不会减少 thunk 的数量(如果 map f (x:xs) 产生一个,f x:map f xs 也是如此)。我不确定还有什么会受到影响。

标签: haskell ghc compiler-optimization loop-unrolling


【解决方案1】:

您是否进行了优化编译?对我来说,使用 -O2,这些 sn-ps 之间并没有真正的区别:map1map2map4 分别运行了 279、267 和 285 毫秒(为了比较,map 本身运行时间为 278 毫秒)。所以这对我来说只是测量噪音而不是改进。

也就是说,您可能想看看this GHC plugin,它似乎是关于循环展开的。

纯函数式语言和命令式语言往往具有非常不同的优化技术,这很可悲,但这是事实。例如,您可能想看看流融合和森林砍伐——这两种技术非常简洁,但不能很好地转化为命令式语言。

至于“关于为什么这不是一个好主意的任何短评?”,嗯,我可以马上想到一个:

*Main> map succ (1:undefined)
[2*** Exception: Prelude.undefined
*Main> map4 succ (1:undefined)
*** Exception: Prelude.undefined

在许多情况下,为了提高性能而使函数更严格是可以的,但是在这里我对性能的提升不是很清楚,map 通常用于依赖懒惰的方式。

【讨论】:

  • 我用 -O 编译,在写这篇文章之前我用 O2 运行了一次,结果相似。我会再次运行它。
  • pastebin.com/Cj5JUT1c 使用 O2 编译,n=5*10^6,我仍然看到比 map1 有所改进。标准发生了一些事情,夸大了运行第一个基准测试的估计时间,但它不会影响平均值。我会看看那个展开插件。是的,我期待 undefined 突然出现在某个地方,但是啊,我认为 haskellers 无论如何都知道如何处理它。受控的严格性(bangpatterns、seq 和 likes)浮现在脑海中。
  • 射击,我在 lpastie 中输入了错误的代码。当我将 map4 与 map1 交换时(以查看速度提高是否是由于 map1 首先出现,使其速度变慢),我忘记在第二部分进行更改。尝试使用正确的地图重新运行代码。
  • @MdxBhmt “我认为haskellers无论如何都知道如何处理它。”处理严格性涉及修改代码。让每个想要使用map 的人编写自己的修改版本是不合理的。
  • 我没有建议改变地图。首先,我试图质疑这种优化是否重要,它可以应用在哪里以及如何应用,然后我们可以讨论它是如何被启动的,无论是通过编译指示还是由编译器确定——再一次,这是后者的方式。我想了解函数 map4 vs map 发生了什么。
【解决方案2】:

除了已经提到的 ghc 展开插件之外,还有一个 page on the GHC trac 讨论了剥离/展开。 “Open Issues”和“References”部分特别揭示了进一步研究材料的来源。

【讨论】:

    【解决方案3】:

    循环展开是一种相当钝的武器。例如,我绝不希望您的 map 示例被展开。它完全由返回列表的内存分配及其单元格中的 thunk 控制。我并不关心寄存器分配器是否有更多需要咀嚼的东西。 (是否展开像foldl' 这样的折叠可能是另一个问题。)

    GHC 可以通过内联递归函数来实现循环展开。但它尽量不这样做:事实上,它永远不会内联递归组的“循环断路器”。否则,无法保证内联会终止。见Section 4 of "Secrets of the GHC inliner"

    GHC 确实在其LiberateCase 传递(使用-O2 运行)中应用了有限形式的循环剥离(或者更确切地说,部分冗余消除):

    f :: (Int, Int) -> Int
    f p = g [0..snd p]
      where
        g :: [Int] -> Int
        g [] = 0
        g (x:xs) = fst p + x + g xs
    

    在这里,GHC 将剥离循环的一个迭代,以将 fst p 投影移出循环,并改为引用未装箱的 Int#。核心:

    Lib.$wf
      = \ (ww_sNF :: Int) (ww1_sNJ :: GHC.Prim.Int#) ->
          case GHC.Enum.eftInt 0# ww1_sNJ of {
            [] -> 0#;
            : x_atE xs_atF ->
              case ww_sNF of { GHC.Types.I# x1_aLW ->
              case x_atE of { GHC.Types.I# y_aLZ ->
              letrec {
                $wg_sNB [InlPrag=NOUSERINLINE[2], Occ=LoopBreaker]
                  :: [Int] -> GHC.Prim.Int#
                [LclId, Arity=1, Str=<S,1*U>, Unf=OtherCon []]
                $wg_sNB
                  = \ (w_sNx :: [Int]) ->
                      case w_sNx of {
                        [] -> 0#;
                        : x2_Xud xs1_Xuf ->
                          case x2_Xud of { GHC.Types.I# y1_XMG ->
                          case $wg_sNB xs1_Xuf of ww2_sNA { __DEFAULT ->
                          GHC.Prim.+# (GHC.Prim.+# x1_aLW y1_XMG) ww2_sNA
                          }
                          }
                      }; } in
              case $wg_sNB xs_atF of ww2_sNA { __DEFAULT ->
              GHC.Prim.+# (GHC.Prim.+# x1_aLW y_aLZ) ww2_sNA
              }
              }
              }
          }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-06
      相关资源
      最近更新 更多