Haskell或F#高吞吐量二进制I/O.

use*_*460 11 .net io f# haskell

这两种语言中二进制I/O库的性能有多好>我正在考虑重新编写一个丑陋(但非常快)的C++代码,它使用标准的fread和fwrite函数处理大约5-10GB的二进制文件.对于F#和Haskell中的优化实现,我应该期待什么减速因子?

编辑:这是计算零字节的C实现(堆上分配的缓冲区).

#include <stdio.h>
#include <stdlib.h>

#define SIZE 32*1024
int main(int argc, char* argv[])
{
    FILE *fp;
    char *buf;
    long i = 0, s = 0, l = 0;
    fp = fopen(argv[1], "rb");
    if (!fp) {
        printf("Openning %s failed\n", argv[1]);
        return -1;
    }
    buf = (char *) malloc(SIZE);
    while (!feof(fp)) {
        l = fread(buf, 1, SIZE, fp);
        for (i = 0; i &lt l; ++i) {
            if (buf[i] == 0) {
                ++s;
            }
        }
    }
    printf("%d\n", s);
    fclose(fp);
    free(buf);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

结果:


$ gcc -O3 -o ioc io.c
$ ghc --make -O3 -o iohs io.hs
Linking iohs ...
$ time ./ioc 2.bin
462741044

real    0m16.171s
user    0m11.755s
sys     0m4.413s
$ time ./iohs 2.bin
4757708340

real    0m16.879s
user    0m14.093s
sys     0m2.783s
$ ls -lh 2.bin
-rw-r--r-- 1  14G Jan  4 10:05 2.bin
Run Code Online (Sandbox Code Playgroud)

Don*_*art 9

Haskell使用基于ByteString的惰性 IO,使用"二进制"解析器应该与执行相同作业的C代码在相同的数据类型上具有相同的性能.

要注意的关键包:

  • @Jon,根据你目前的结果(格林尼治标准时间0:47),它看起来像懒惰的ByteString和.NET几乎相同.也就是说,如果您的计时来自每个程序的单次运行. (2认同)
  • 这真是最好的答案.任何具有良好FFI和体面库的现代语言都应该能够通过硬件限制来实现简单的IO.这是Tim Bray的宽幅基准测试公平结果之一. (2认同)

Dan*_*att 8

考虑到这篇文章需要:

  • 哈斯克尔
  • 代码优化
  • 绩效基准

......可以肯定地说我已经超越了我的头脑.然而,当我进入我的脑海时,我总是学到一些东西,所以这里就是这样.

Data.ByteString.Lazy.*通过Hoogle围绕Haskell模块进行了探索,并找到了用于测量惰性ByteString长度的长度函数.因此实施:

length :: ByteString -> Int64
length cs = foldlChunks (\n c -> n + fromIntegral (S.length c)) 0 cs
Run Code Online (Sandbox Code Playgroud)

嗯.Jon确实说过"...... 在F#中折叠文件是其快速的主要原因......"(我的重点).而且这个length功能似乎也是使用厚实的折叠来实现的.因此,看起来这个函数更像是与Jon的F#代码进行"苹果对苹果"的比较.

它在实践中有所作为吗?我将Jon的例子与以下内容进行了比较:

import System
import Data.List
import Data.ByteString.Lazy as B

main =
    getArgs
    >>= B.readFile . Data.List.head
    >>= print . B.length
Run Code Online (Sandbox Code Playgroud)

Jon的Haskell示例在我的机器上为1.2 GB文件:10.5s

'矮胖'版本:1.1s

Haskell代码的"矮胖"版本快了近十倍.这表明它可能比Jon的优化F#代码快几倍.

编辑

虽然我不一定完全同意Jon对我的例子的批评,但我想尽可能地使它成为可能的.因此,我已经描述了以下代码:

import System
import Data.List
import Data.ByteString.Lazy as B

main =
    getArgs
    >>= B.readFile . Data.List.head
    >>= print . B.count 0
Run Code Online (Sandbox Code Playgroud)

此代码将目标文件的内容加载到ByteString中,然后"计数"每个0值字节的出现.除非我遗漏了某些东西,否则该程序必须加载并评估目标文件的每个字节.

上述程序的运行速度始终比Jon提交的最快的Haskell程序快4倍,复制在此作为参考(如果已更新):

import System
import Data.Int
import Data.List
import Data.ByteString.Lazy as B

main =
    getArgs
    >>= B.readFile . Data.List.head
    >>= print . B.foldl (\n c -> n + 1) (0 :: Data.Int.Int64)
Run Code Online (Sandbox Code Playgroud)

  • @JonH这篇文章有多个标志,仅供参考 - 我强烈建议你减轻你对这些主题的修辞,因为我已经厌倦了你留下的mod旗帜. (6认同)
  • @Jon:在这一点上,我建议差异更多地是因为I/O的变幻莫测,而不是生成代码之间的任何显着差异(特别是如果你只使用一次数据点运行).我怀疑Char8和普通ByteString之间的区别是那么重要. (2认同)
  • @Jon:不太好.传递给`foldl`的函数是`\nc - > n + 1`.这个函数可能会形成一个大的形式`((((n + 1)+1)+1)...`,这将在打印折叠结果时进行评估.严格的'foldl'`在每一步都减少了它的输出,防止了这个thunk积累.GHC注意到在这种情况下不会构建thunk因此它没有任何区别; 否则这个表达式肯定会打击堆栈.从来没有使用`c`,这就是我的意思(它不需要被评估为`Char`,但它可能仍然可以加载,检查核心是否确定). (2认同)

Jon*_*rop 2

在这里写了关于这个的博客。

  • 抱歉,但是读完评论后,[我无法抗拒](http://xkcd.com/285/)。:) (3认同)
  • @Jon:请尽量保持理性。底层函数是用什么语言编写的并没有什么区别。表演就是表演。您在 F# 代码中调用的“ReadByte”函数很可能是用 C# 编写的。这是否意味着您的示例不能代表 F# 性能? (3认同)
  • @Jon Harrop:[需要引用] (2认同)
  • @丹尼尔:我很合理。OP 希望将使用“fread”处理大文件的代码从 C++ 重写为 Haskell 或 F#。这意味着数据处理必须在 Haskell/F# 中完成,基准测试才具有代表性,但它可以像 C++ 一样调用低级 IO。因此,您可以使用 ByteString 进行 IO,但必须在 Haskell/F# 中进行处理。如果您还通过选择恰好有标准库函数的任务来用 C 进行处理,那么您的结果并不代表一般 IO 吞吐量,而是代表特殊情况。 (2认同)
  • @mokus:“它不会以任何方式破坏抽象......我不知道这不是一件好事。”。拥有该功能是一件好事。假装它代表了 Haskell 用于大文件通用处理的 IO 吞吐量是一件坏事。 (2认同)