13

My first real programming experience was with Haskell. For my ad-hoc needs I required a tool that was easy to learn, quick to code and simple to maintain and I can say it did the job nicely.

However, at one point the scale of my tasks became much bigger and I thought that C might suit them better and it did. Maybe I was not skilled enough in terms of [any] programming, but I was not able to make Haskell as speed-efficient as C, even though I heard that proper Haskell is capable of similar performance.

Recently, I thought I would try some Haskell once more, and while it's still great for generic simple (in terms of computation) tasks, it doesn't seem to be able to match C's speed with problems like Collatz conjecture. I have read:

Speed comparison with Project Euler: C vs Python vs Erlang vs Haskell

GHC Optimization: Collatz conjecture

collatz-list implementation using haskell

But from what I see, simple optimization methods, including:

  • choosing "tighter" types, like Int64 instead of Integer
  • turning GHC optimizations on
  • using simple optimization techniques like avoiding unneccessary computation or simpler functions

still don't make Haskell code even close to almost identical (in terms of methodology) C code for really big numbers. The only thing that seems to make its performance comparable to C's [for big-scale problems] is using optimization methods that make the code a long, horrendous monadic hell, which goes against the principles that Haskell (and I) value so much.

Here's the C version:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int32_t col(int64_t n);

int main(int argc, char **argv)
{
    int64_t n = atoi(argv[1]), i;
    int32_t s, max;

    for(i = 2, max = 0; i <= n; ++i)
    {
        s = col(i);
        if(s > max) max = s;
    }
    printf("%d\n", max);

    return 0;
}

int32_t col(int64_t n)
{
    int32_t s;

    for(s = 0; ; ++s)
    {
        if(n == 1) break;
        n = n % 2 ? 3 * n + 1 : n / 2;
    }

    return s;
}

and the Haskell version:

module Main where

import System.Environment (getArgs)
import Data.Int (Int32, Int64)

main :: IO ()
main = do
    arg <- getArgs
    print $ maxCol 0 (read (head arg) :: Int64)

col :: Int64 -> Int32
col x = col' x 0

col' :: Int64 -> Int32 -> Int32
col' 1 n            = n
col' x n
    | rem x 2 == 0  = col' (quot x 2) (n + 1)
    | otherwise     = col' (3 * x + 1) (n + 1)

maxCol :: Int32 -> Int64 -> Int32
maxCol maxS 2   = maxS
maxCol maxS n
    | s > maxS  = maxCol s (n - 1)
    | otherwise = maxCol maxS (n - 1)
    where s = col n

TL;DR: Is Haskell code quick to write and simple to maintain only for computationally simple tasks and loses this characteristic when performance is crucial?

Community
  • 1
  • 1
ljedrz
  • 20,316
  • 4
  • 69
  • 97

1 Answers1

20

The big problem with your Haskell code is that you are dividing, which you don't in the C version.

Yes, you wrote n % 2 and n / 2, but the compiler replaces that with shifts and bitwise and. GHC has unfortunately not yet been taught to do that.

If you do the substitution yourself

module Main where

import System.Environment (getArgs)
import Data.Int (Int32, Int64)
import Data.Bits

main :: IO ()
main = do
    arg <- getArgs
    print $ maxCol 0 (read (head arg) :: Int64)

col :: Int64 -> Int32
col x = col' x 0

col' :: Int64 -> Int32 -> Int32
col' 1 n            = n
col' x n
    | x .&. 1 == 0  = col' (x `shiftR` 1) (n + 1)
    | otherwise     = col' (3 * x + 1) (n + 1)

maxCol :: Int32 -> Int64 -> Int32
maxCol maxS 2   = maxS
maxCol maxS n
    | s > maxS  = maxCol s (n - 1)
    | otherwise = maxCol maxS (n - 1)
    where s = col n

with a 64-bit GHC you get comparable speed (0.35s vs C's 0.32s on my box for a limit of 1000000). If you compile using the LLVM backend, you don't even need to replace the % 2 and / 2 with bitwise operations, LLVM does that for you (but it produces slower code, 0.4s, for your original Haskell source, surprisingly - normally, LLVM is not worse than the native code generator at loop optimisation).

With a 32-bit GHC, you won't get comparable speed, since with those, the primitive operations on 64-bit integers are implemented through C calls - there never was enough demand for fast 64-bit operations on 32-bit systems for them to be implemented as primops; the few people working on GHC spent their time doing other, more important, things.

TL;DR: Is Haskell code quick to write and simple to maintain only for computationally simple tasks and loses this characteristic when performance is crucial?

That depends. You must have some idea of what sort of code GHC generates from what sort of input, and you must avoid some performance traps. With a bit of practice, it is quite easy to get within say 2× the speed of gcc -O3 for such tasks.

Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
  • I would have never suspected that GHC doesn't optimise division! And I use Windows GHC 7.4.2, so I guess this is the second problem (it's 32bit). Could you point me to some sources on such GHC flaws (i guess it should be considered a flaw)? – ljedrz Dec 02 '12 at 12:43
  • 7
    @yzb3 Upgrade to 7.6.1, that has a 64-bit version for Windows. Upgrade as fast as possible. Yeah, GHC is **Awe-so-me** as far as high-level optimisations go, but somewhat lacking in low-level optimisations. Not enough people working on it, and the demand for language extensions is higher than for low-level optimisations. I know of no collection of such weaknesses, you can search [trac](http://hackage.haskell.org/trac/ghc) for various performance related tickets, apart from that, the best advice I can give is to read the core (and asm, if you can). – Daniel Fischer Dec 02 '12 at 12:50
  • 1
    Let's archive this question to show that non-constructive questions can be topics teaching users something very interesting. – David Dec 02 '12 at 21:17
  • I can confirm that I was able to get much better results using 64bit GHC on linux. However, with the aforementioned optimizations I get results still only about 2x slower than those of simply written C. I think Haskell is awesome and I prefer to use it for most of my generic needs, but I remain unconvinced in terms of its performance with comptutationally heavy tasks. – ljedrz Dec 03 '12 at 05:43
  • Which GHC version and which C compiler, if I may ask? I get slightly slower times with ghc-6.12.3, 7.0.4, 7.2.2 and 7.4.2 than with 7.6.1, but all take less than 1.3× the time gcc-4.6.2 with -O3 takes (and all beat clang-3.0) on my box. With regard to the performance, one advantage of C that isn't to be underestimated is that there are fewer gotchas. In Haskell, with today's compilers, it's easier to do something bad for performance if you don't know rather well what you're doing (and sometimes even then). So for performance-critical computationally heavy tasks, C is my choice too. – Daniel Fischer Dec 03 '12 at 05:59
  • But it's not so far out that it's absurd to ponder doing it in Haskell. – Daniel Fischer Dec 03 '12 at 06:00
  • I used gcc 4.7.2 with -O3 and ghc 7.6.1 with -O2. I agree that the difference was not big in terms of single execution - it is more of a problem for really heavy duty (which for me was the reason for learning C). It's good to know about Haskell's focus on high-level optimization; judging by some other posts I thought that it wanted to compete in terms of low-level performance as well - and it sometimes looked like some magic, which I didn't find very Haskellish. I guess you can't have it all :). – ljedrz Dec 03 '12 at 06:42
  • Hmm, gcc-4.7 seems to be quite a bit better than 4.6, I've heard before that it produces significantly faster code, should upgrade. GHC is a bit Janus-headed. On the one hand, it's heavily involved in language research, implementing an ever-growing family of language extensions. On the other, it aims to produce code that can compete in terms of performance. And while everybody involved would be happy to beat gcc, they're also satisfied with "close enough". In particular because they all are originally language researchers and find that more enjoyable as long as the performance doesn't stink. – Daniel Fischer Dec 03 '12 at 06:56