45

As part of a programming challenge, I need to read, from stdin, a sequence of space-separated integers (on a single line), and print the sum of those integers to stdout. The sequence in question can contain as many as 10,000,000 integers.

I have two solutions for this: one written in Haskell (foo.hs), and another, equivalent one, written in Python 2 (foo.py). Unfortunately, the (compiled) Haskell program is consistently slower than the Python program, and I'm at a loss for explaining the discrepancy in performance between the two programs; see the Benchmark section below. If anything, I would have expected Haskell to have the upper hand...

What am I doing wrong? How can I account for this discrepancy? Is there an easy way of speeding up my Haskell code?

(For information, I'm using a mid-2010 Macbook Pro with 8Gb RAM, GHC 7.8.4, and Python 2.7.9.)

foo.hs

main = print . sum =<< getIntList

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

(compiled with ghc -O2 foo.hs)

foo.py

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

Benchmark

In the following, test.txt consists of a single line of 10 million space-separated integers.

# 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
Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
jub0bs
  • 60,866
  • 25
  • 183
  • 186
  • Your test file is virtually all zeros. Is this really a good test? – dfeuer Mar 21 '15 at 21:30
  • 3
    @dfeuer I didn't generate that file. It just happens to be one of the test cases given during the contest. It may be bad or uninteresting, but I didn't have a say in this. – jub0bs Mar 22 '15 at 10:01
  • I know that `read :: String -> Integer` was *quadratic* at least until a more or less recent patch. Not sure whether the same kind of algorithm was used for `Int`... – Bakuriu Mar 22 '15 at 12:07
  • 8
    Notice `read` is just the wrong tool for the job when your format is simply ints. `read` is intended to parse valid haskell expressions, so things like `"((( 0x8485) ))"` will parse fine at quite some extra cost. – Thomas M. DuBuisson Mar 22 '15 at 17:27

3 Answers3

66

read is slow. For bulk parsing, use bytestring or text primitives, or attoparsec.

I did some benchmarking. Your original version ran in 23,9 secs on my computer. The version below ran in 0.35 secs:

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"

By specializing the parser to your test.txt file, I could get the runtime down to 0.26 sec:

getIntList :: IO [Int]          
getIntList =
    unfoldr (B.readInt . B.dropWhile (==' ')) <$> B.readFile "test.txt"
András Kovács
  • 29,931
  • 3
  • 53
  • 99
  • 4
    You should do better using `B.dropWhile (not . isDigit)`. `isDigit` is much cheaper than `isSpace`, although not *quite* as cheap as the fake version, `(==' ')`. – dfeuer Mar 22 '15 at 05:48
27

Read is slow

Fast read, from this answer, will bring you down to 5.5 seconds.

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

Strings are Linked Lists

In Haskell the String type is a linked list. Using a packed representation (bytestring if you really only want ascii but Text is also very fast and supports unicode). As shown in this answer, the performance should then be neck and neck.

Community
  • 1
  • 1
Thomas M. DuBuisson
  • 64,245
  • 7
  • 109
  • 166
5

I would venture to guess that a big part of your problem is actually words. When you map read . words, what you're actually doing is this:

  1. Scan the input looking for a space, building a list of non-spaces as you go. There are a lot of different kinds of spaces, and checking any character that's not a common type of space additionally involves a foreign call to a C function (slow). I'm planning to fix this sometime, but I haven't gotten around to it yet, and even then you'll still be building and throwing away lists for no good reason, and checking for spaces when you really just want to check for digits.
  2. Read through the list of accumulated characters to try to make a number out of them. Produce the number. The accumulated list now becomes garbage.
  3. Go back to step 1.

This is a fairly ridiculous way to proceed. I believe you can even do better using something horrible like reads, but it would make more sense to use something like ReadP. You can also try fancier sorts of things like stream-based parsing; I don't know if that will help much or not.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • `word` doesn't seem to be the big issue, empirically. With `map (const 0) . words <$> readFile "test.txt"` the code runs in 1,3 sec, as opposed to ~23 sec if I do the `read` instead of `const 0`. – András Kovács Mar 21 '15 at 21:03
  • @AndrásKovács, interesting. What happens if you use a fast version of `read` along with `words`? If `read` can be made so much faster, why isn't it? – dfeuer Mar 21 '15 at 21:08
  • 4
    if I use a rather ugly `map (fst . fromJust . B.readInt . B.pack) . words <$> readFile "test.txt"`, I still get 2 sec runtime. It'd be a good idea to look at `read`... – András Kovács Mar 21 '15 at 21:10