Haskell中hFlush的高CPU使用率

时间:2012-11-13 15:05:13

标签: performance haskell bytestring

我发现以下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使用率。但是这种解决方案并不令人满意,因为它没有冲洗。

感谢。

EDITED

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

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

为什么只有序列和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之间的区别是什么?谁做同样的任务?为什么IOWithoutFlush甚至按顺序快速运行并重复?有没有关于这种行为的参考?

感谢。

3 个答案:

答案 0 :(得分: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

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

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.27s)

答案 1 :(得分:6)

我不确定Python代码(什么是doIO()?),但改进Has​​kell的一个显而易见的方法是使用sequence_代替sequence,所以它没有'需要建立庞大的()列表。这个微小的变化使我的机器上的速度提高了6-7倍。

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

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

答案 2 :(得分:5)

最大的问题是

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

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

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

它会更快,分配更少。