微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Haskell State monad vs state 作为参数性能测试

如何解决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 可以优化为:

  1. 我们可以使用修改
  2. 的严格版本
  3. 我们不需要地图列表输出,所以我们可以使用traverse_代替

所以新的优化状态函数是:

sumState:: [Int] -> Int
sumState xs = execState (traverse f xs) 0
    where f n = modify (n+)

运行时间约为 350 毫秒。这是一个巨大的改进。太震撼了。

为什么修改后的 sumStatesum1 具有更好的性能?可以优化 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 融合(也称为捷径融合)实现了这种优化。 sumfoldl'traverse(用于列表)函数是使用 foldr 函数实现的,而 [1..x] 是使用 build 函数实现的。 foldrbuild 函数具有特殊的优化规则,因此可以将它们融合。您的自定义 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 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?