2

我发现以下 Haskell 代码使用 100% CPU 并在我的 Linux 服务器上完成大约需要 14 秒。

{-# LANGUAGE OverloadedStrings #-}
module Main where

import qualified Data.ByteString.Lazy.Char8 as L
import System.IO

str = L.pack "FugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFuga\n"

main = do
  hSetBuffering stdout (BlockBuffering (Just 1000))
  sequence (take 1000000 (repeat (L.hPutStr stdout str >> hFlush stdout)))
  return ()

另一方面,非常相似的 Python 代码在大约 3 秒内完成相同的任务。

import sys

str = "FugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFuga\n"

def main():
    for i in xrange(0, 1000000):
        print str,
        sys.stdout.flush()
        # doIO()

main()

通过使用 strace,我发现在 Haskell 版本中每次调用 hFlush 时都会调用 select。另一方面,在 Python 版本中不调用 select。我想这是 Haskell 版本慢的原因之一。

有什么方法可以提高 Haskell 版本的性能吗?

我已经尝试省略 hFlush,它确实大大降低了 CPU 使用率。但是这个解决方案不能令人满意,因为它不会冲洗。

谢谢。

已编辑

非常感谢您的帮助!通过将序列和重复更改为 replicateM_,运行时间从 14 秒减少到 3.8 秒。

但现在我有另一个问题。我问了上面的问题,因为当我从上面的程序中删除 hFlush 时,尽管它使用序列和重复重复 I/O,但它运行得很快。

为什么只有 sequence 和 hFlush 的组合使它变慢?

为了确认我的新问题,我改变了我的程序如下进行分析。

{-# LANGUAGE OverloadedStrings #-}
module Main where

import qualified Data.ByteString.Char8 as S
import System.IO
import Control.Monad

str = S.pack "FugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFuga\n"

doIO = S.hPutStr stdout str >> hFlush stdout
doIO' = S.hPutStr stdout str >> hFlush stdout
doIOWithoutFlush = S.hPutStr stdout str

main = do
  hSetBuffering stdout (BlockBuffering (Just 1000))
  sequence (take 1000000 (repeat doIO))
  replicateM_ 1000000 doIO'
  sequence (take 1000000 (repeat doIOWithoutFlush))
  return ()

通过编译运行如下:

$ ghc -O2 -prof -fprof-auto Fuga.hs
$ ./Fuga +RTS -p -RTS > /dev/null

我得到了以下结果。

COST CENTRE      MODULE  %time %alloc

doIO             Main     74.7   35.8
doIO'            Main     21.4   35.8
doIOWithoutFlush Main      2.6   21.4
main             Main      1.3    6.9

执行相同任务的 doIO 和 doIO' 有什么区别?为什么 doIOWithoutFlush 即使按顺序重复运行也很快?有没有关于这种行为的参考?

谢谢。

4

3 回答 3

8

在每次写入时调用 hFlush 似乎是错误的。

这个简单的改变,使用严格的字节串,forM_或者replicateM_代替你的显式sequence和块缓冲,将运行时间从 16.2s 减少到 0.3s

{-# LANGUAGE OverloadedStrings #-}
module Main where

import qualified Data.ByteString.Char8 as S
import Control.Monad
import System.IO

str = S.pack "FugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFuga\n"

main = replicateM_ 1000000 $ S.putStr str

虽然更惯用的是使用惰性字节串的单次写入,但依靠字节串子系统来协调写入。

import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import Control.Monad
import System.IO

str :: S.ByteString
str = S.pack "FugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFugaFuga\n"

main = L.putStr $ L.fromChunks (replicate 1000000 str)

性能略有提高(0.27 秒)

于 2012-11-13T15:17:06.850 回答
6

我不确定 Python 代码(什么是doIO()?),但是改进 Haskell 的一个明显方法是使用sequence_而不是sequence,因此它不需要建立庞大的()s 列表。这个小小的改变使它在我的机器上快了 6-7 倍。

(表达该行的更简单方法是replicateM_ 1000000 (L.hPutStr stdout str >> hFlush stdout)。)

可能是系统调用的数量很大——GHC 的 RTS 确实进行了非阻塞 I/O,并且可能会进行不必要select的调用——但是根据你的数字,这个变化可能足以让它进入 Python 的范围它自己的。

于 2012-11-13T15:17:31.863 回答
5

最大的问题是

sequence (take 1000000 (repeat (L.hPutStr stdout str >> hFlush stdout)))

收集IO列表中执行的操作的结果。如果你丢弃结果,

sequence_ (take 1000000 (repeat (L.hPutStr stdout str >> hFlush stdout)))

它会更快,并且分配更少。

于 2012-11-13T15:21:11.513 回答