【问题标题】:Why is this simple Haskell program so slow? [duplicate]为什么这个简单的 Haskell 程序这么慢? [复制]
【发布时间】:2019-07-20 13:00:56
【问题描述】:

在这个打印从 1 到 10000000 的所有数字的简单程序中,一个 Haskell 版本和一个 C 版本,为什么 Haskell 这么慢以及哪些命令有助于学习如何提高 Haskell 程序的性能强>?

以下是一份报告,其中包含重现我激动人心的事件所需的所有详细信息,制作报告时会打印包括 Makefile 的来源的来源:

$ make -B report
cat Foo.hs
import Data.Foldable
main = traverse_ print [1..10000000]
cat Fooc.c
#include <stdio.h>

int main()
{
    for (int n = 0; n < 10000000; ++n)
    {
        printf("%d\n", n+1);
    }
}
ghc -O3 Foo.hs -o Foo
time ./Foo | tail -n1
3.45user 0.03system 0:03.49elapsed 99%CPU (0avgtext+0avgdata 4092maxresident)k
0inputs+0outputs (0major+290minor)pagefaults 0swaps
10000000
cc -O3    Fooc.c   -o Fooc
time ./Fooc | tail -n1
0.63user 0.02system 0:00.66elapsed 99%CPU (0avgtext+0avgdata 1468maxresident)k
0inputs+0outputs (0major+63minor)pagefaults 0swaps
10000000
cat Makefile
.PHONY: printFoo printFooc printMakefile
printFoo: Foo.hs
    cat $^

printFooc: Fooc.c
    cat $^

printMakefile: Makefile
    cat $^

Fooc: CFLAGS=-O3
Fooc: Fooc.c
Foo: Foo.hs
    ghc -O3 $^ -o $@

.PHONY: timeFoo timeFooc
timeFoo: Foo
    time ./$^ | tail -n1
timeFooc: Fooc
    time ./$^ | tail -n1

.PHONY: report
report: printFoo printFooc timeFoo timeFooc printMakefile

【问题讨论】:

  • 请使用Int64,或Int 而不是Integer
  • 还请注意,您的两个计时都包括两个不同的东西:生成值的计算和打印它们的 I/O。如果您将它们分开,您的结果会提供更多信息。例如print (last [1::Int .. 10000000])) 将只打印列表的最后一个元素(同时使用Int 而不是Integer,正如其他人所建议的那样)。
  • 人们,如果您要发表评论,请确保您确实有观点! Integer 与固定大小类型的对比与这里的性能问题无关。也没有未装箱的列表结构。事实上,print 在 C 中似乎确实比 printf 慢得多。但是 a) 担心 printing 的性能是很愚蠢的,因为如果您有这么多数字,那么don't print them 很重要! (对于大量数据,始终使用二进制格式。) ...
  • ...因此说“Haskell 很慢”是非常不公平的,只是一个并不重要的特定任务很慢b) 这不是那么慢。因素 5,许多语言只能梦想接近 C - 在性能真正很重要的任务中!根据经验,写得很好的 Haskell 比 C 慢 2 倍;有时,即使是一个简单的版本也接近于 C,只要付出一些努力,几乎总能实现。
  • 我应该指出,这两个程序的编写经济成本相同,理解成本也相似,因此“您编写的代码很慢”的评论信息量不大。

标签: performance haskell


【解决方案1】:

在我的系统上,您的 Haskell 代码大约需要 3.2 秒。

注意你的 C 代码需要...

time ./fooc | tail -n1
ld: warning: directory not found for option '-L/opt/local/lib'
10000000
./fooc  0.92s user 0.03s system 33% cpu 2.863 total
tail -n1  2.85s user 0.01s system 99% cpu 2.865 total

所以请注意time a | b 的区别以及它与time (a | b) 的区别。

Haskell 速度慢的部分原因是(其中一些是假设)

  1. 默认情况下print 和底层putStrLn 使用String,这是一个字符链表。
  2. UTF 编码
  3. RTS 差异

对于 1,使用 Text 的打包变体并没有太大的不同,可能是由于问题 2。

对于 2,ByteString 变体(打包字节而不是字符)更能代表您的 C 程序正在做什么:

-- Using functions from the Relude package
main = traverse_ putBSLn (show <$> [(1::Int)..10000000])

结果

10000000
./foo  1.55s user 0.08s system 56% cpu 2.904 total

因此 CPU 时间更接近于您的 C 程序,这让我推测这种差异主要是因为您在 Haskell 的前奏中默认使用的例程中内置了不必要的 UTF8 处理。

死胡同:

  • 我尝试了NoBuffering 和大号BlockBuffering,但没有成功。
  • 构造一个大字节串并通过一次调用进行打印并没有更好的效果(惰性或严格字节串)。
  • 通过Text 而不是String 进行打印只带来了最基本的改进。
  • 直接呈现到 ByteString,而不是将值 showed 打包到字符串中。我预计,如果做得好,这可能是一场胜利。

编辑:我不敢相信我忘记了 Builder,这是一种构建字节串的优化方法,在某些情况下,它可以很好地融合以减少分配。 Builder 是我已经展示的上述示例的基础,但直接使用它可以进行一些手动优化。

{-# LANGUAGE OverloadedStrings #-}
import Data.ByteString.Builder
import System.IO (stdout)
import Data.Foldable

main :: IO ()
main = do
    traverse_ (hPutBuilder stdout . (<>"\n") . intDec) [(1::Int)..10000000]

表演时间:

./foo  1.05s user 0.13s system 38% cpu 3.048 total
tail -n1  3.02s user 0.01s system 99% cpu 3.047 total

确实,这比之前对 hPut 的许多单独调用更有效,因为正如 hPutBuilder 所说:

这个函数比 hPut 更有效。 toLazyByteString 因为在许多情况下不需要进行缓冲区分配。此外,短 Builder 的多次执行结果在 Handles 缓冲区中串联,因此避免了不必要的缓冲区刷新。

所以我应该补充: 4. Haskell 在这种情况下很慢,因为有时计算不会融合,最终导致分配过多,这不是免费的。

【讨论】:

  • 对我的测试产生重大影响的另一件事:用换行符和 all 构造完整的字符串,然后为整个 String 调用一次 putStr(Ln) 而不是每行一次。在将字符实际转储到输出缓冲区之前,putStrLn 中似乎有一堆重要内容。 putBSLn 可能有类似的问题,您可以获得第二次便宜的胜利——尽管除非您使用惰性字节串,否则内存成本可能需要考虑!
  • 要考虑的另一件事是 RTS 设置时间。甚至main = pure () 也可能只需要一些时间,因为 RTS 需要设置 GC 等(虽然没有对此进行测试)。
  • main = pure () 报告时间为 0.00。所以基本的 RTS 设置不是问题。
  • 感谢您,但问题的关键部分缺失:如何找出此类性能问题的原因(以及剩下的 2/5 秒)。人们需要了解下一步要改进什么,否则必须确定没有任何改进是切实可行的。
猜你喜欢
  • 1970-01-01
  • 2017-01-10
  • 2012-06-13
  • 1970-01-01
  • 1970-01-01
  • 2018-07-17
  • 1970-01-01
  • 2014-03-01
  • 1970-01-01
相关资源
最近更新 更多