【问题标题】:Haskell program involving `read` is much slower than an equivalent Python one涉及“读取”的 Haskell 程序比等效的 Python 程序慢得多
【发布时间】:2015-05-25 01:02:47
【问题描述】:

作为编程挑战的一部分,我需要从标准输入读取一系列以空格分隔的整数(在一行),并将这些整数的总和打印到标准输出。有问题的序列可以包含多达 10,000,000 个整数。

我有两种解决方案:一种是用 Haskell (foo.hs) 编写的,另一种是等效的,用 Python 2 (foo.py) 编写的。不幸的是,(编译的)Haskell 程序一直比 Python 程序慢,我无法解释这两个程序之间的性能差异。请参阅下面的基准部分。如果有的话,我本以为 Haskell 会占上风……

我做错了什么?我该如何解释这种差异?有没有一种简单的方法可以加速我的 Haskell 代码?

(有关信息,我使用的是 2010 年中期配备 8Gb RAM、GHC 7.8.4 和 Python 2.7.9 的 Macbook Pro。)

foo.hs

main = print . sum =<< getIntList

getIntList :: IO [Int]
getIntList = fmap (map read . words) getLine

(用ghc -O2 foo.hs编译)

foo.py

ns = map(int, raw_input().split())
print sum(ns)

基准测试

在下文中,test.txt 由一行 1000 万个以空格分隔的整数组成。

# Haskell
$ time ./foo < test.txt 
1679257

real    0m36.704s
user    0m35.932s
sys     0m0.632s

# Python
$ time python foo.py < test.txt
1679257 

real    0m7.916s
user    0m7.756s
sys     0m0.151s

【问题讨论】:

  • 您的测试文件几乎全为零。这真的是一个很好的测试吗?
  • @dfeuer 我没有生成那个文件。它恰好是比赛期间给出的测试用例之一。这可能很糟糕或无趣,但我对此没有发言权。
  • 我知道read :: String -&gt; Integer 至少在最近的补丁之前是二次的。不确定Int是否使用了同一种算法...
  • 注意 read 当您的格式只是 ints 时,它是适合该工作的错误工具。 read 旨在解析有效的 haskell 表达式,所以像 "((( 0x8485) ))" 这样的东西可以很好地解析,但需要付出相当多的额外费用。

标签: python performance haskell io


【解决方案1】:

read 很慢。对于批量解析,使用bytestringtext 原语或attoparsec

我做了一些基准测试。您的原始版本在我的计算机上运行了 23,9 秒。以下版本在 0.35 秒内运行:

import qualified Data.ByteString.Char8 as B
import Control.Applicative
import Data.Maybe
import Data.List
import Data.Char

main = print . sum =<< getIntList

getIntList :: IO [Int]
getIntList =
    map (fst . fromJust . B.readInt) . B.words <$> B.readFile "test.txt"

通过将解析器专门用于您的 test.txt 文件,我可以将运行时间缩短到 0.26 秒:

getIntList :: IO [Int]          
getIntList =
    unfoldr (B.readInt . B.dropWhile (==' ')) <$> B.readFile "test.txt"

【讨论】:

  • 您应该使用B.dropWhile (not . isDigit) 做得更好。 isDigitisSpace 便宜得多,虽然不像假冒版本 (==' ') 那样相当便宜。
【解决方案2】:

读取速度很慢

快速阅读,from this answer,将把您缩短到 5.5 秒。

import Numeric
fastRead :: String -> Int
fastRead s = case readDec s of [(n, "")] -> n

字符串是链表

在 Haskell 中,String 类型是一个链表。使用打包表示(bytestring 如果你真的只想要 ascii 但Text 也非常快并且支持 unicode)。如this answer所示,那么性能应该并驾齐驱。

【讨论】:

    【解决方案3】:

    我冒昧地猜测您的问题的很大一部分实际上是words。当你map read . words时,你实际上在做的是:

    1. 扫描输入以查找空格,并在执行过程中构建非空格列表。有很多不同类型的空格,检查任何不是常见空格类型的字符还涉及对 C 函数的外部调用(慢)。我计划在某个时候解决这个问题,但我还没有解决这个问题,即便如此,你仍然会无缘无故地构建和丢弃列表,并在你真的只想检查空间时检查空间数字。
    2. 通读累积字符列表,尝试从中找出一个数字。产生号码。累积的列表现在变成垃圾了。
    3. 返回步骤 1。

    这是一种相当荒谬的方式。我相信你甚至可以使用像 reads 这样可怕的东西做得更好,但使用像 ReadP 这样的东西会更有意义。你也可以尝试一些更高级的东西,比如基于流的解析;我不知道这是否会有很大帮助。

    【讨论】:

    • word 从经验上看似乎不是大问题。使用map (const 0) . words &lt;$&gt; readFile "test.txt",代码运行时间为 1.3 秒,而如果我使用 read 而不是 const 0,则运行时间约为 23 秒。
    • @AndrásKovács,很有趣。如果您同时使用read 的快速版本和words,会发生什么情况?如果read 可以做得这么快,为什么不呢?
    • 如果我使用相当丑陋的map (fst . fromJust . B.readInt . B.pack) . words &lt;$&gt; readFile "test.txt",我仍然可以获得 2 秒的运行时间。最好看看read...
    猜你喜欢
    • 2014-10-05
    • 2016-02-08
    • 1970-01-01
    • 2011-03-03
    • 2011-10-24
    • 2011-04-16
    相关资源
    最近更新 更多