如何解决Haskell State monad vs state 作为参数性能测试
我开始学习 State Monad,一个想法困扰着我。我们可以将所有内容包装到 state monad 中,而不是将 accumulator 作为参数传递。
所以我想比较使用 State monad 与将其作为参数传递之间的性能。
所以我创建了两个函数:
func descendingOrder(of number: Int) -> Int {
let s = String(number).sorted().reversed() // sort and reverse the string descendinng
let arr = s.map{ String($0) } // convert characters to array of strings
let joined = arr.joined() // join them in 1 string
let res = Int(joined)! // convert string to int
return res
}
和
sum1 :: Int -> [Int] -> Int
sum1 x [] = x
sum1 x (y:xs) = sum1 (x + y) xs
我在输入数组 [1..1000000000] 上比较了它们。
- sumState 运行时间约为 15 秒
- sum1 大约 5 秒
我们可以看到明显的赢家,但我意识到 sumState 可以优化为:
所以新的优化状态函数是:
sumState:: [Int] -> Int
sumState xs = execState (traverse f xs) 0
where f n = modify (n+)
运行时间约为 350 毫秒。这是一个巨大的改进。太震撼了。
为什么修改后的 sumState 比 sum1 具有更好的性能?可以优化 sum1 以匹配甚至比 sumState 更好吗?
我还尝试了 sum 的其他不同实现
- 使用内置的 sum 函数,这给了我大约 240 毫秒的时间 ((sum [1..x] ::Int))
- 使用严格的 foldl',这给了我大约 240 毫秒的相同结果(隐式 [Int] -> Int)
这是否真的意味着最好使用 foldl 函数或 State monad 来传递累加器而不是将其作为参数传递给函数?
感谢您的帮助。
编辑:
每个函数都在单独的文件中,有自己的主函数,并用“-O2”标志编译。
sumState:: [Int] -> Int
sumState xs = execState (traverse_ f xs) 0
where f n = modify' (n+)
运行时间是在 Linux 上通过 time 命令测量的。
解决方法
为了解释为什么 traverse
更慢:traverse f xs
有类型 State [()]
并且 [()]
(单位元组列表)是在总和。这会阻止进一步优化,并且如果您不使用惰性状态会导致内存泄漏。
更新:我认为 GHC 应该能够注意到从未使用过的单位元组列表,所以我打开了 a GHC issue。
在这两种情况下,为了获得最佳性能,我们希望将求和与枚举 [1..x]
组合(或融合)到一个紧密的递归循环中,该循环简单地递增和相加直到达到 x
。生成的代码如下所示:
sumFromTo :: Int -> Int -> Int -> Int
sumFromTo s x y
| x == y = s + x
| otherwise = sumFromTo (s + x) (x + 1) y
这避免了对列表 [1..x]
的分配。
base
库使用 foldr/build
融合(也称为捷径融合)实现了这种优化。 sum
、foldl'
和 traverse
(用于列表)函数是使用 foldr
函数实现的,而 [1..x]
是使用 build
函数实现的。 foldr
和 build
函数具有特殊的优化规则,因此可以将它们融合。您的自定义 sum1
函数不使用 foldr
,因此永远无法以这种方式与 [1..x]
融合。
具有讽刺意味的是,困扰您实现 sumState
的问题也是 sum1
的问题。你没有严格的积累,所以你像这样建立了thunk:
sum 0 [1,2,3]
sum (0 + 1) [2,3]
sum ((0 + 1) + 2) [3]
sum (((0 + 1) + 2) + 3) []
(((0 + 1) + 2) + 3)
((1 + 2) + 3)
(3 + 3)
6
如果对 sum1
添加严格性,您应该会看到效率的显着提高,因为您消除了对 thunk (((0 + 1) + 2) + 3)
的非尾递归评估,这是 {{1} 的代价高昂的部分}}。使用严格的累积可以提高效率:
sum1
应该为您提供与 sum1 x [] = []
sum1 x (y : xs) = x `seq` sum1 (x + y) xs
相当的性能(尽管在另一个答案中指出,GHC 可能无法正确使用融合来为您提供列表 sum
中的 sum
真正神奇的性能}}).
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。