Haskell中的基本输入/输出性能

4
另一个微基准测试:为什么这个“循环”(使用ghc -O2 -fllvm编译,7.4.1,Linux 64位3.2内核,重定向到/dev/null)会这样?
mapM_ print [1..100000000]

相比于使用非缓冲系统调用write(2)的普通C循环,Haskell的速度大约慢了5倍?我正在努力收集Haskell的陷阱。

即使这个缓慢的C语言解决方案也比Haskell快得多。

int i;
char buf[16];
for (i=0; i<=100000000; i++) {
    sprintf(buf, "%d\n", i);
    write(1, buf, strlen(buf));
}

4
在普通 C 语言中使用 write(2) 非缓冲系统调用的简单 for 循环,是否比实际代码更容易编写和理解? - n. m.
6
请展示C代码。这并不明显,有几种可能性。 - Daniel Fischer
1
我重构了你的代码为 let xs = unlines $ map show [1..100000000] in return ()。这个代码实现了相同的效果,但是运行速度更快。 - AndrewC
1
我认为没有效果并不等同于向句柄写入数据,即使该句柄被重定向到无用的位置。 - Thomas M. DuBuisson
1
@AndrewC虽然我同意您的观点,但我想指出这些问题有助于像我这样的新手将这些小细节牢记在脑海中。 - gphilip
显示剩余7条评论
3个回答

10

好的,在我的电脑上,使用gcc -O3编译的C代码大约需要21.5秒才能运行,而原始的Haskell代码需要大约56秒。因此,不是5的倍数,略高于2.5。

第一个非微不足道的差异在于

mapM_ print [1..100000000]

使用 Integer 会稍微慢一些,因为它需要在前面进行检查,然后使用封装的 Int,而 IntShow 实例在未封装的 Int# 上完成转换工作。

添加类型签名,以便 Haskell 代码适用于 Int

mapM_ print [1 :: Int .. 100000000]

将时间缩短到47秒,略高于C代码所需时间的两倍以上。

另一个重要区别在于show函数生成一个Char链表,而不是填充一个连续的字节缓冲区。这也更慢一些。

然后,这个Char链表被用来填充一个字节缓冲区,然后写入stdout处理器中。

因此,Haskell代码比C代码做了更多、更复杂的事情,因此它需要更长的时间。

无可否认,希望有一种更简单的方法可以更直接(从而更快地)输出这些内容。然而,正确的处理方式是使用更适合的算法(也适用于C代码)。对以下代码进行简单更改即可:

putStr . unlines $ map show [0 :: Int .. 100000000]

近乎减半了所需时间,如果需要更快的速度,可以使用更快的ByteString I/O并高效构建输出,就像applicative's answer中所示。


理论上说,将数字打印成文本并不是最优的选择,虽然这样做很理想。对于真正高吞吐量的需求,Haskell 和 C 都需要二进制存储,而 Data.Binary 的速度非常快。 - leftaroundabout
1
没错。但有时候你仍然想输出文本,如果有更多简单的选项可以使其更快,那就太好了(虽然我从来没有真正需要过,我不做太多的I/O)。 - Daniel Fischer

8
在我的(相当慢且过时的)计算机上,结果如下:
$ time haskell-test > haskell-out.txt
real    1m57.497s
user    1m47.759s
sys     0m9.369s
$ time c-test > c-out.txt
real    7m28.792s
user    1m9.072s
sys     6m13.923s
$ diff haskell-out.txt c-out.txt
$

我已经调整了列表,使得C和Haskell都从0开始。

没错,你没看错。Haskell的速度比C快好几倍。或者更确切地说,使用正常缓冲的Haskell比使用write(2)非缓冲系统调用的C更快。

(当将输出测量到/dev/null而不是实际磁盘文件时,C大约快1.5倍,但谁在乎/dev/null的性能呢?)

技术数据:Intel E2140 CPU, 2个核心, 1.6 GHz, 1M高速缓存, Gentoo Linux, gcc4.6.1, ghc7.6.1.


1
出色的现实世界观点。因此,如果我想要打印到文件,我将使用 Haskell,但对于所有打印到/dev/null的需求,我一定会使用 C。 - AndrewC

5
标准的Haskell方法是使用构建器单子将巨大的bytestring传输到操作系统中。
import Data.ByteString.Lazy.Builder  -- requires bytestring-0.10.x
import Data.ByteString.Lazy.Builder.ASCII -- omit for bytestring-0.10.2.x
import Data.Monoid
import System.IO

main = hPutBuilder stdout $ build  [0..100000000::Int]

build = foldr add_line mempty
   where add_line n b = intDec n <> charUtf8 '\n' <> b

这给了我:

 $ time ./printbuilder >> /dev/null
 real   0m7.032s
 user   0m6.603s
 sys    0m0.398s

与您使用的Haskell方法相比,有所不同。
$ time ./print >> /dev/null
real    1m0.143s
user    0m58.349s
sys 0m1.032s

也就是说,相对于Daniel Fischer令人惊讶的悲观主义,做出比mapM_ print好9倍以上的效果轻而易举。你需要知道的一切都在这里:http://hackage.haskell.org/packages/archive/bytestring/0.10.2.0/doc/html/Data-ByteString-Builder.html 我不会将其与您的C语言进行比较,因为我的结果比Daniel和n.m.的速度慢得多,所以我认为可能有些地方出了问题。
编辑:将导入与所有版本的bytestring-0.10.x保持一致。以下内容或许更加清晰 - unlines . map show Builder等效形式:
main = hPutBuilder stdout $ unlines_ $ map intDec [0..100000000::Int]
 where unlines_ = mconcat . map (<> charUtf8 '\n')

我无法编译你的解决方案,可以吗?intDec是从哪里来的? - Cartesius00
1
这段内容与 Data.ByteString.Builder 相关。现在正在研究它,最新版本已经进行了重新排列;如果您使用的是任何版本的 bytestring-0-10,将该行替换为 import Data.ByteString.Lazy.Builder; import Data.ByteString.Lazy.Builder.ASCII 应该可以工作。 - applicative

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接