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
解决方案
在我的系统上,您的 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 速度慢的部分原因是(其中一些是假设)
- 默认情况下
print
,底层putStrLn
使用String
的是字符的链表。 - UTF 编码
- 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 而不是通过将值打包
show
到字符串中。我希望,如果做得好,这可能是一场胜利。
编辑:我不敢相信我忘记了 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 在这种情况下很慢,因为有时计算不会融合,你最终会得到多余的分配,这不是免费的。
推荐阅读
- minio - 在 Minio 减少冗余存储类中将奇偶校验驱动器数设置为 0
- sql - Access SQL 语句:如果没有有效的条目,则返回最后一个
- c# - 如何在谷歌购物中使用 API KEY
- python - 将列表拆分为具有条件的多个列表
- python - 找出 Groupby 操作之间的差异
- javascript - 阻止 javascript 访问小部件的 DOM 元素
- python - Django:我的函数返回一个对象而不是返回值
- sql - 在 Altibase DBMS 中测量执行时间
- amazon-web-services - AWS Lambda ListVersionsByFunction 返回的版本是否已排序?
- r - 按 r 中的列值标记 nmds 图