Performance Haskell显式递归与迭代`
在Haskell中使用Performance Haskell显式递归与迭代`,performance,loops,haskell,ghc,compiler-optimization,Performance,Loops,Haskell,Ghc,Compiler Optimization,在Haskell中使用iterate编写函数时,我发现使用显式递归的等效版本似乎要快得多——尽管我认为在Haskell中显式递归应该受到反对 类似地,我希望GHC能够适当地内联/优化列表组合器,以便生成的机器代码至少与显式递归的执行类似 这里有一个(不同的)例子,它也显示了我观察到的减速 steps mn及其变体steps'计算Collatz步数n达到1所需的步数,尝试m后放弃 steps使用显式递归,而steps'使用列表函数 import Data.List (elemIndex) impo
iterate
编写函数时,我发现使用显式递归的等效版本似乎要快得多——尽管我认为在Haskell中显式递归应该受到反对
类似地,我希望GHC能够适当地内联/优化列表组合器,以便生成的机器代码至少与显式递归的执行类似
这里有一个(不同的)例子,它也显示了我观察到的减速
steps mn
及其变体steps'
计算Collatz步数n
达到1所需的步数,尝试m
后放弃
steps
使用显式递归,而steps'
使用列表函数
import Data.List (elemIndex)
import Control.Exception (evaluate)
import Control.DeepSeq (rnf)
collatz :: Int -> Int
collatz n
| even n = n `quot` 2
| otherwise = 3 * n + 1
steps :: Int -> Int -> Maybe Int
steps m = go 0
where go k n
| n == 1 = Just k
| k == m = Nothing
| otherwise = go (k+1) (collatz n)
steps' :: Int -> Int -> Maybe Int
steps' m = elemIndex 1 . take m . iterate collatz
main :: IO ()
main = evaluate $ rnf $ map (steps 800) $ [1..10^7]
我通过对10^7
之前的所有值进行评估来测试这些值,每个值在800
步骤后都放弃。在我的机器上(用ghc-O2
编译),显式递归只花了不到4秒的时间(3.899s
),但列表组合器花了大约5倍的时间(19.922s
)
为什么显式递归在这种情况下要好得多,有没有一种方法可以在不使用显式递归的情况下编写它,同时保持性能?更新:我提交了这个bug
如果将elemIndex
和findIndex
的定义复制到模块中,问题就会消失:
import Control.Exception (evaluate)
import Control.DeepSeq (rnf)
import Data.Maybe (listToMaybe)
import Data.List (findIndices)
elemIndex :: Eq a => a -> [a] -> Maybe Int
elemIndex x = findIndex (x==)
findIndex :: (a -> Bool) -> [a] -> Maybe Int
findIndex p = listToMaybe . findIndices p
collatz :: Int -> Int
collatz n
| even n = n `quot` 2
| otherwise = 3 * n + 1
steps' :: Int -> Int -> Maybe Int
steps' m = elemIndex 1 . take m . iterate collatz
main :: IO ()
main = evaluate $ rnf $ map (steps' 800) $ [1..10^7]
问题似乎是,为了使GHC获得正确的聚变,这些必须是内联的。不幸的是,它们在Data.OldList
中都没有标记为Inlineable
允许
findIndex
参与融合的更改相对较新(请参阅),其中listToMaybe
被重新实现为foldr
。因此,它可能还没有看到很多测试。您是否考虑过检查您对GHC的预期是否正确?如果不是,您的迭代版将生成、匹配和收集数十亿个元素,这将大大有助于解释差异。@最终,我相当肯定我对GHC的期望过高,但我相当肯定Haskell的懒惰可以防止在迭代
中生成数十亿个不必要的元素,然后再使用它们。可以公平地假设每个迭代
在生成的元素不超过800个之后就会停止,但800*10^7仍然是数十亿。我很难理解这是如何证明差异的,由于两个版本都必须进行相同数量的计算,因此这两种实现的计算结果分别是25秒和53秒/foo+RTS-s
显示快速版本在堆上分配了1200041152字节
vs151781624928字节
,这确实表明正在生成和搜索列表。算术计算的数量通常不是工作量的一个很好的衡量标准,因为与内存访问或分支预测失误相比,算术计算的数量是多么便宜,或者CS分析经常认为算术计算是免费的。很少看到这类问题揭示语言本身的缺陷。我觉得我在尼斯湖水下瞥见了什么东西!哇!我原以为我对iterate
融合的理解有缺陷,而不是遗漏了一个内联词。@AdamSmith但这不是语言的缺陷,只是GHC,一个特殊的Haskell编译器的缺陷。@Cactus挑剔:)但你是对的,GHC/=Haskell,即使很容易理解为什么有人会这么认为。