【问题标题】:Sum all numbers from one to a billion in Haskell在 Haskell 中将所有数字从 1 到 10 亿求和
【发布时间】:2016-01-27 02:03:52
【问题描述】:

目前我正在追赶 Haskell,到目前为止我印象非常深刻。作为一个超级简单的测试,我编写了一个程序来计算总和到十亿。为了避免创建列表,我写了一个应该是尾递归的函数

summation start upto 
  | upto == 0 = start
  | otherwise = summation (start+upto) (upto-1)

main = print $ summation 0 1000000000

使用 -O2 运行它,我的机器上的运行时间约为 20 秒,这让我很惊讶,因为我认为编译器会更加优化。作为比较,我写了一个简单的 c++ 程序

#include <iostream>

int main(int argc, char *argv[]) {
  long long result = 0;
  int upto = 1000000000;

  for (int i = 0; i < upto; i++) {
    result += i;
  }
  std::cout << result << std::end;
  return 0;
}

在没有优化的情况下使用 clang++ 编译运行时间约为 3 秒。所以我想知道为什么我的 Haskell 解决方案这么慢。有人有想法吗?

在 OSX 上:

clang++ --version:

Apple LLVM version 7.0.2 (clang-700.1.81)
Target: x86_64-apple-darwin15.2.0
Thread model: posix

ghc --version:

The Glorious Glasgow Haskell Compilation System, version 7.10.3

【问题讨论】:

  • 值得注意的是,它是不必要的。我意识到这可能是对递归等的简单测试,但欧拉的三角数公式在这里是相关的。我们不要忘记,简单的数学运算比其他方法要快得多。
  • 另外,你的 start 变量被误导了
  • 默认为任意精度算术。这总是比 32 位算法慢。

标签: performance haskell


【解决方案1】:

添加类型签名将我的运行时间从 14.35 秒降至 0.27。它现在比我机器上的 C++ 更快。当性能很重要时,不要依赖类型默认。 Ints 不适合例如在 Web 应用程序中对域进行建模,但如果您想要一个紧密的循环,它们就很棒。

module Main where

summation :: Int -> Int -> Int
summation start upto 
  | upto == 0 = start
  | otherwise = summation (start+upto) (upto-1)

main = print $ summation 0 1000000000


[1 of 1] Compiling Main             ( code/summation.hs, code/summation.o )
Linking bin/build ...
500000000500000000
14.35user 0.06system 0:14.41elapsed 100%CPU (0avgtext+0avgdata 3992maxresident)k
0inputs+0outputs (0major+300minor)pagefaults 0swaps

Linking bin/build ...
500000000500000000
0.27user 0.00system 0:00.28elapsed 98%CPU (0avgtext+0avgdata 3428maxresident)k
0inputs+0outputs (0major+171minor)pagefaults 0swaps

【讨论】:

  • 哇,谢谢!我没想到类型注释会产生如此巨大的影响:)
  • @Moe 我能感觉到你,但这有点像说你对数据的重要性感到惊讶。让类型默认就像让编译器决定你的数据在 Haskell 中的结构。很多时候,尤其是在开发或例如代码期间,这很好。允许推断类型也很好,特别是如果您在其他地方有签名来指示类型。但是,如果您关心性能...是的,请写下类型:)
  • 正如另一个问题提到的,当Haskell没有其他线索你想要什么样的数字时,任意大小的Integer是默认值。
  • @Moe,真正起作用的并不是类型签名。事实上,您告诉 Haskell 使用机器大小的整数 (Int) 而不是任意精度的整数 (Integer),这是默认值。在这种情况下,最直接的方法是添加指定 Int 的类型签名。
  • 现在用-fllvm 编译并观察你的0.27user 下降到0.00user。不断折叠!
【解决方案2】:

除非您想查看未优化(非-O2)的视图,否则请跳过删除线。

我们来看看评价:

summation start upto 
  | upto == 0 = start
  | otherwise = summation (start+upto) (upto-1)

main = print $ summation 0 1000000000

-->

summation 0 1000000000

-->

summations (0 + 1000000000) 999999999

-->

summation (0 + 1000000000 + 999999999) 999999998

-->

summation (0 + 1000000000 + 999999999 + 999999998) 999999997

编辑:我没有看到您使用-O2 进行编译,所以上述情况没有发生。累加器,即使没有任何严格的注释,大多数时候只要有适当的优化级别就足够了。

哦不!您将 10 亿个数字存储在一个您没有进行评估的大型 thunk 中!叮当!有很多使用累加器和严格性的解决方案 - 似乎大多数 stackoverflow 答案与这个问题附近的任何东西都足以教你除了库函数之外的那些,比如fold{l,r},它可以帮助你避免编写自己的原始递归函数。由于您可以环顾四周和/或询问这些概念,因此我将直接回答这个问题。

如果您真的想以正确的方式执行此操作,那么您将使用列表并了解 Haskell 编译器可以进行“森林砍伐”,这意味着十亿元素列表从未实际分配:

main = print (sum [0..1000000000])

然后:

% ghc -O2 x.hs
[1 of 1] Compiling Main             ( x.hs, x.o )
Linking x ...
% time ./x
500000000500000000
./x  16.09s user 0.13s system 99% cpu 16.267 total

很酷,但为什么要 16 秒?默认情况下,这些值是整数(GHC 编译器的 GMP 整数),这比机器 Int 慢。让我们使用Int

% cat x.hs
main = print (sum [0..1000000000] :: Int)
tommd@HalfAndHalf /tmp% ghc -O2 x.hs && time ./x
500000000500000000
./x  0.31s user 0.00s system 99% cpu 0.311 total

【讨论】:

  • 不,严格性分析器将捕获 thunk 泄漏(因为 OP 构建了优化)。
  • 是的,我刚刚注意到问题中的-O2
  • 是的,我以艰难的方式了解了 -O2 :),但是将类型注释添加到 sum 的好方法(或者这在列表中吗?)。我现在没有那个
  • @Moe 类型注释覆盖了整个表达式f x :: a 表示应用于x 的函数fa 类型。这与(f x) :: a 相同。也许称它为“结果的类型”更简单。
猜你喜欢
  • 2021-12-08
  • 1970-01-01
  • 2019-03-11
  • 1970-01-01
  • 2010-12-23
  • 1970-01-01
  • 2021-12-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多