cod*_*hot 2 performance haskell
在这个琐碎的程序中,可以打印从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
Run Code Online (Sandbox Code Playgroud)
在我的系统上,您的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
Run Code Online (Sandbox Code Playgroud)
所以,做笔记的区别time a | b是什么意思VS和time (a | b)。
Haskell之所以缓慢,部分原因是(其中一些是假设)
print,其基础putStrLn用途String是链接的字符列表。对于1,使用Text的打包变体的性能差别不大,可能是由于问题2所致。
对于2,ByteString变体(用字节代替字符)更能代表您的C程序正在做什么:
-- Using functions from the Relude package
main = traverse_ putBSLn (show <$> [(1::Int)..10000000])
Run Code Online (Sandbox Code Playgroud)
结果是
10000000
./foo 1.55s user 0.08s system 56% cpu 2.904 total
Run Code Online (Sandbox Code Playgroud)
因此,CPU时间更接近您的C程序,这使我假设这种差异主要是由于在Haskell的序言中默认使用的例程中内置了不必要的UTF8处理。
死角:
NoBuffering和大BlockBuffering,没有运气。Text而不是String仅打印进行了最大的改进。showed 打包到String中。我希望,如果做得好,这可能是一个胜利。编辑:我不敢相信我忘记了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]
Run Code Online (Sandbox Code Playgroud)
在执行:
./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
Run Code Online (Sandbox Code Playgroud)
的确,这比以前的许多单独调用hPut更为有效,因为正如hPutBuilder所说:
此功能比hPut更有效。toLazyByteString,因为在许多情况下无需进行缓冲区分配。此外,短生成器的几次执行结果在“句柄”缓冲区中并置,因此避免了不必要的缓冲区刷新。
因此,我应该补充:4. Haskell在这种情况下运行缓慢,因为有时计算不会融合,并且最终会产生多余的分配,这不是免费的。