【发布时间】: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,所以在推出这个标准之后,这就是我所拥有的:
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%。
接下来是提问时间:
- 以前是否讨论过这个问题?类似的东西已经实现了吗?
- 真的是减少 map4 的评估次数提高了速度吗?
- 这可以自动化吗?
- 我可以按块进行评估吗?即:如果 (f x) 被完全评估,则完全评估 (f x4) 之前的所有内容。
- 这种展开还有其他形式吗?
- 这会导致函数大小的膨胀程度如何?
- 关于为什么这不是一个好主意的短评?
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