如何在将1到1000万的整数列表写入文件时获得良好的性能?

gat*_*ado 7 linux performance haskell

我想要一个能编写序列的程序,

1
...
10000000
Run Code Online (Sandbox Code Playgroud)

到一个文件.什么是最简单的代码可以编写,并获得不错的性能?我的直觉是存在一些缺乏缓冲的问题.我的C代码以100 MB/s的速度运行,而通过引用,Linux命令行实用程序dd9 GB/s 3 GB/s的速度运行(对不起,请注意 - 请注意 - 我对大图订单更感兴趣 - 虽然很高).

人们会认为这将是一个现在解决的问题...即任何现代编译器都会立即编写这样的程序,表现得相当好......

C代码

#include <stdio.h>

int main(int argc, char **argv) {
    int len = 10000000;
    for (int a = 1; a <= len; a++) {
        printf ("%d\n", a);
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我正在编译clang -O3.调用putchar('\n')8次的性能框架可获得相当的性能.

Haskell代码

一个naiive Haskell实现以13 MiB/sec的速度运行,并使用ghc -O2 -optc-O3 -optc-ffast-math -fllvm -fforce-recomp -funbox-strict-fields.(我没有重新编译我的库-fllvm,也许我需要这样做.)代码:

import Control.Monad
main = forM [1..10000000 :: Int] $ \j -> putStrLn (show j)
Run Code Online (Sandbox Code Playgroud)

我最好用Haskell刺伤的速度更慢,达到17 MiB/sec.问题是我找不到一个很好的方法来转换VectorByteStrings(也许有一个使用iteratees的解决方案?).

import qualified Data.Vector.Unboxed as V
import Data.Vector.Unboxed (Vector, Unbox, (!))

writeVector :: (Unbox a, Show a) => Vector a -> IO ()
writeVector v = V.mapM_ (System.IO.putStrLn . show) v

main = writeVector (V.generate 10000000 id)
Run Code Online (Sandbox Code Playgroud)

看起来编写ByteString很快,正如这段代码所示,编写相同数量的字符,

import Data.ByteString.Char8 as B
main = B.putStrLn (B.replicate 76000000 '\n')
Run Code Online (Sandbox Code Playgroud)

它的速度为1.3 GB/s,速度不是很快dd,但显然要好得多.

Dan*_*her 7

一些完全不科学的基准测试首先:

所有程序都使用默认优化级别编译(对于gcc为-O3,对于GHC为-O2)并运行

time ./prog > outfile
Run Code Online (Sandbox Code Playgroud)

作为基准,C程序需要1.07秒才能生成~76MB(78888897字节)的文件,吞吐量大约为70MB/s.

  1. "天真"的Haskell程序(forM [1 .. 10000000] $ \j -> putStrLn (show j))耗时8.64秒,大约8.8MB/s.
  2. 相同forM_而不是forM花了5.64s,大约13.5MB/s.
  3. ByteString从dflemstr的回答版本了9.13s,约8.3MB /秒.
  4. Text从dflemstr的回答版本了5.64s,约13.5MB /秒.
  5. Vector问题的版本耗时5.54秒,约为13.7MB/s.
  6. main = mapM_ (C.putStrLn . C.pack . show) $ [1 :: Int .. 10000000],其中CData.ByteString.Char8,花了4.25s,约17.9MB /秒.
  7. putStr . unlines . map show $ [1 :: Int .. 10000000] 花了3.06s,大约24.8MB/s.
  8. 手动循环,

    main = putStr $ go 1
      where
        go :: Int -> String
        go i
            | i > 10000000 = ""
            | otherwise = shows i . showChar '\n' $ go (i+1)
    
    Run Code Online (Sandbox Code Playgroud)

    耗时2.32秒,约32.75MB/s.

  9. main = putStrLn $ replicate 78888896 'a' 花了1.15秒,大约66MB/s.
  10. main = C.putStrLn $ C.replicate 78888896 'a'其中CData.ByteString.Char8,把0.143s,约530MB/s的,大致为懒惰同一附图ByteString秒.

我们可以从中学到什么?

首先,不要使用forMmapM除非你真的想收集结果.表现,这很糟糕.

然后,ByteString输出可以非常快(10.),但是如果ByteString输出的结构很慢(3.),则最终的代码比天真String输出慢.

3有什么可怕的?好吧,所有涉及String的都很短.所以你得到一份清单

Chunk "1234567" Empty
Run Code Online (Sandbox Code Playgroud)

并且在任何两个这样的之间Chunk "\n" Empty放置a,然后将结果列表连接起来,这意味着Empty... (Chunk "1234567" (Chunk "\n" (Chunk "1234568" (...))))构建a时所有这些都被抛弃.这是很多浪费的构造 - 解构 - 重建正在进行中.可以通过严格的s和使用(以及换行)来实现Text与固定的"天真" String版本相当的速度.通过消除昂贵的单例,可以获得比6更好的性能.如果你将换行符粘贴到s上,而不是使用,则连接必须处理一半稍长的s,这会得到回报.packByteStringfromChunksData.List.intersperseString\k -> shows k "\n"showByteString

我对文本或向量的内部不够熟悉,提供的不仅仅是关于观察到的性能的原因的半教育猜测,所以我将它们排除在外.可以说,与固定的天真String版本相比,性能增益最多是微不足道的.

现在,6表明ByteString输出比String输出快,足以在这种情况下额外的pack输入工作得到补偿.但是,不要因为相信总是这样而被愚弄.如果String包装很长,则包装可能比String输出需要更多时间.

但千万调用putStrLn,无论是StringByteString版本,采取了很多的时间.抓取stdout Handle一次并String在非IO代码中构造输出更快.unlines已经做得很好,但我们仍然受到列表建设的影响map show [1 .. 10^7].不幸的是,编译器没有设法消除它(但它消除了[1 .. 10^7],这已经相当不错).所以让我们自己做,导致8.这不是太可怕,但仍然需要C程序的两倍多.

可以通过低级别直接填充ByteStrings而不通过Stringvia 来制作更快的Haskell程序show,但我不知道C速度是否可达.无论如何,那个低级别的代码并不是很漂亮,所以我会饶恕你所拥有的东西,但是如果速度很重要,有时必须得到一个人的手.

  • 嗯,10倍C的表现?我想,抓一点.在我不起眼的机器上,我每秒可以获得大约2亿个分区; 将数字的数字设置为10M需要大约6000万个分区.如果编译器用乘法和移位替换除法,那么可能会加速三倍.你仍然只是获得数字的天真C时间的10%,没有任何(字节)字符串构造和输出.如果你变得非常丑陋,你可能会打败它,但我不相信,但是并没有这么大. (2认同)