Slashdot Mirror


OCaml vs. C++ for Dynamic Programming

jcr13 writes "OCaml is nearly as fast (or sometimes even faster) than C, right? At least according to the Computer Language Shootout [alternate] (OCaml supporters often point to these shootout results). My results on a real-world programming problem (optimizing a garden layout using dynamic programming) disagree. On one particular problem instance (a garden of size 7x3), my C++ implementation finished in 1 second, while the OCaml implementation was still running after 16 minutes. Bear in mind that my OCaml implementation was dramatically faster than my equivalent Haskell code. It seems that if you program using a functional style in OCaml (which I did, using map, filter, and other recursive structures in place of loops), it is quite slow. However, most of the shootout OCaml programs rely heavily on OCaml's imperative features (unlike Haskell, OCaml doesn't force you to be a functional purist). If you write OCaml code that is isomorphic to C code, it will be fast---what about if you use OCaml the way it was meant to be used?"

17 of 161 comments (clear)

  1. Hmmm ... by crmartin · · Score: 4, Interesting

    That difference is so dramatic that I wonder if you made a mistake in your functional implementation? Or is there something specific about your dynamic program that makes trouble?

    Dynamic programming depends basically on memoization (not "memorization", before someone complains about my typo) which inherently means preserving some state. If you don't preserve state, it becomes a good old, likely exponential time, recursive program. Any chance your implementation is not memoizing?

    1. Re:Hmmm ... by bhurt · · Score: 5, Insightful

      Having read the article and looked at your Ocaml code, you do have at least one problem in your implementation. You're using lists. Lists are not for random access and modification. To change the nth element of a list, you need to modify (and reallocate) n elements.

      Try using a map instead. I'll send you example Ocaml code in a day or two (I'm moving, so I don't have that much free time to fix other people's bugs that I'm not getting paid to fix). Note that this is true in Haskell as well as Ocaml. Haskell may be just a little bit better at hiding the problem with laziness- but it's still a problem!

      Now, for the brutal part of the response: that big of a difference in performance almost certain does mean you've messed something up in your implementation. But, instead of saying "Gee- I wonder what I screwed up?" you said "Gee- Ocaml and functional programming must just suck!" That I still fault you for.

  2. apples and oranges by e+aubin · · Score: 3, Insightful

    Usings lists in all circumstances because its functional is not appropriate. Show the ocaml implementation using arrays or an c++ implementation using linked lists for a valid comparision.

    The strength of Ocaml is the flexibility it provides to a developer. If your solution is more elegantly coded using imperative constructs, then use them!
  3. Re:My realworld results differ by snorklewacker · · Score: 5, Insightful

    Ocaml doesn't support any ad-hoc polymorphism (overloading) whatsoever in functions. Methods on the other hand can be overloaded, but not generic. This sort of thing makes it weaker than even C++ for generic programming, let alone Haskell, though I must admit not having to use template syntax makes me want to claw my eyes out a good deal less when reading it (or my hair when writing). Modules simply don't do it for me. Having to differentiate between HashTable.insert and SkipList.insert sort of defeats the purpose of abstract types, because no one thought to make module signatures themselves first-class (except Alice).

    Haskell type families are just elegance and beauty itself, but doing state in Haskell is an exercise in raw tedium. Very localized state (in one function) is easy enough, but anything more pervasive and you soon become more familiar with monads than you ever wanted to be. If you want a haskell program that doesn't suck up more memory than emacs, you have to stay away from many modern features so your program will compile with nhc98.

    Ocaml isn't seeing a lot of new work going into it -- the language definition seems to have become cast in stone. Haskell is always evolving, though typically in ways that are really impenetrable to those of us without PhD's in category theory and denotational semantics.

    I guess I could search the world over for my holy grail FP language, and always be dissatisfied...

    --
    I am no longer wasting my time with slashdot
  4. Re:My realworld results differ by angst_ridden_hipster · · Score: 3, Insightful
    Stone is carved or chiseled.

    Not always, not always.

    For example, I have seen two different kinds of tree castings made of stone: one, a negative casting made by molten lava that built up as an accretion on a tree (which obviously burned out), and two, a positive casting made through a slow fossilization ("petrification") process.

    I would happily come up with a false etymology originating in the parlance of lime-slakers, medieval wall builders, sarcophagus fillers, or even potters discussing cone-10 firing, but you'd probably call me on it.

    That being said, it is a weird phrase, that probably belongs with "mute points" and exclaimations like "here here!"

    --
    Eloi, Eloi, lema sabachtani?
    www.fogbound.net
  5. Re:lookup table by PylonHead · · Score: 5, Insightful

    Yea, you're not kidding.

    I just looked at the code, and he's memoizing the function results in a associative list:

    (* Set up an associative list for memoization *)
    let lookup key table = List.assoc key !table;;
    let insert key value table = table := (key, value) :: !table;;

    Insertion is cheap, but the lookup is a linear table scan! Doh! What was he thinking?

    I suspect that a Hashtable or a Map datastructure might be much better suited to the task.

    In any case, it would have been very easy for him to post this code to the OCaml newsgroup and ask, "Am I writing good functional code?"

    He would of gotten a lot of advice on how he could have sped up his program while still maintaining a functional style.

    Lastly, in response to his question, "I could write an OCaml implementation that is isomorphic to the C++ code (using loops and side effects), but what would be the point?" The point is that you can easily mix and match styles in OCaml.
    You can write 90% of your code in a functional style and fall back to imperative style if there is an inner loop that would benefit from that.

    For this problem though, I suspect that a well written functional version would be pretty close in speed to his C++ version, cleaner, and easier to maintain.

    --
    # (/.);;
    - : float -> float -> float =
  6. Haskell code? by Pseudonym · · Score: 4, Informative

    Can we see your Haskell code?

    Haskell is not known for raw speed, but dynamic programming is probably the one thing it does well, thanks to lazy evaluation. You fill a CAF with unevaluated function calls, and the language engine does the rest. It won't be as fast as the hand-crafted C++ version, most likely, but if your O'Caml code is anything to go by, it might be able to be improved.

    --
    sub f{($f)=@_;print"$f(q{$f});";}f(q{sub f{($f)=@_;print"$f(q{$f});";}f});
  7. It's always possible to tune your inner loops by Tom7 · · Score: 4, Informative

    One of the joys of programming in ML is that you can write most of your code in a really nice, functional way, and (if necessary) put in the effort to write a tight inner loop, perhaps in an imperative style for speed. I don't see this as a disadvantage, and ML compilers often do a better job of optimizing such loops than C compilers, in part because of more information being available in the type system. (And if they don't, it's trivial to invoke C subroutines.) Also, if performance is really an issue, you might try mlton (which is for SML, very similar to Caml); its whole-program approach often produces significantly better code than O'Caml.

    However, as an every-day ML user I find it very unlikely that your program would be a thousand times slower if you're using it "the way it's meant to be used." I am guessing that your implementation is asymptotically worse, since using map and fold correctly should really only be a constant factor slower than C, at worst. (mlton can often inline and optimize these into essentially the same code you'd write in C!) How about posting your code?

    1. Re:It's always possible to tune your inner loops by CaptainPinko · · Score: 3, Insightful

      I for one would be interested by how much you could --as a regular ML user-- optimise the code and see what kind of performance you could get. Really there are no slow languages, only slow implementations.

      --
      Your CPU is not doing anything else, at least do something.
  8. Re:My realworld results differ by ijones · · Score: 5, Informative

    It's not actually the case that Haskell "forces" functional purity, at least not in the way the submitter seems to think. You can do things that are a LOT like non-pure functions, you just have to use Monads. You have the so-called "unsafe" functions, which perform side-effects in otherwise "pure" functions.

    So you might ask, "If you're going to write code like that in Haskell, why not just use C++." The answer is because even when using Haskell in a non-idiomatic way, Haskell is still more beautiful :)

    Monads are a means of threading "stateful" code in a very clean and predictable way through your programs. The parent's comment, "Very localized state (in one function) is easy enough, but anything more pervasive and you soon become more familiar with monads than you ever wanted to be," is sorta like saying, "You can write high-level code in C++, but you will soon become more familiar with objects than you ever wanted to be."

    They are indeed a part of the language, and definitely a new concept, but monads aren't nearly as confusing as people seem to think, certainly not more confusing than objects, it's just a reputation issue that makes people think monads are confusing. Take it from a random-joe hacker like me. You don't need a PhD to perform IO in Haskell.

    For instance, here's a basic implementation of 'cat' in Haskell:

    import System.Environment(getArgs)

    main = do
    {a <- getArgs;
    lines <- mapM readFile a;
    putStr (concat lines);}

    The code:

    a <- b
    is similar to assignment.

    getArgs just reads in the command-line arguments as a list, so 'a' represents a list of the filenames.

    readFile takes a file name, reads the contents, and returns it as a list of lines.

    mapM means 'perform this computation once for each item in this list'

    putStr is obvious, concat just takes a list of lists and turns it into a single list.

    There's a paper, Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell about how to do these kinds of "real-world" things in Haskell.

    There's also a very cool version control system called darcs that's written in Haskell, and recently an implementation of Perl 6 called Pugs in Haskell.

    peace,

    isaac

  9. Bad string manipulation by vanicat · · Score: 4, Informative
    I've not read all the ocaml code, but I've seen at the very begining this:
    let rec printCells cs =
    match cs with
    | [] -> ""
    | (c::rest) -> (printCell c) ^ (printCells rest);;
    And I know that this program will be slow. The ocaml string concatenation operator copy both string each time it is called, and this concatenation work will take O(n^2) step.

    You should use the Buffer module, or String.concat:
    let rec printCells cs =
    String.concat "" (list.map printCell cs)
    If there is a lot of those mistake, no wonder it is so slow...
  10. More efficient ocaml version by Anonymous Coward · · Score: 3, Interesting

    I see nothing wrong in your C++ version, while your ocaml version clearly sucks: you are memoizing using a complex key, and an association list, meaning that accessing memoized information costs a lot.

    If you are concerned by performance, you should use a complete cache, like in your C version.
    FYI, I uploaded an ocaml translation of your C code. It doesn't use mutable state except for memoizing, and uses pattern-matching on lists, and recursion rather than for loops, but otherwise it follows closely your code. Performance should be very similar.

    http://wwwfun.kurims.kyoto-u.ac.jp/~garrigue/garde n2.ml

  11. Email response by jcr13 · · Score: 5, Interesting

    > Here's a laundry list of why your O'Caml program in inefficient:
    >
    > 1. You use lists. Lists aren't designed to be fast (computationally)
    > to use. They're designed to be fast (programmatically) to use. You'll
    > be hard pressed to find a production, speed-sensitive Lisp or O'Caml
    > program that uses lists.

    Okay... but here's my point: Every single example that shows how elegant Haskell and OCaml are uses lists. The 4-line Quicksort example for Haskell uses lists. All of the code that demonstrates easy reuse of functions and functions taken as arguments uses lists (like how easy it is to implement quite complicated algorithms using only map and filter, for example).

    So, proponents say "Everyone should use functional languages because they can express complicated problems in elegant ways and result in cleaner, more reusable code."

    But what you're saying in #1 above is that in "production," speed-sensitive code, no one is using lists... this would mean that no one is using map, filter, or any other pieces of reusable primitive code. So, they are instead all using mutable data structures... I.e., they are programming with side-effects and loops (random access instead of recursion, even when ever element of an array/list needs to be accessed/processed).

    That was my point exactly. If you write elegant OCaml code using all of the lovely (and I mean lovely, really) tricks that they present when they demonstrate why OCaml is cool, you end up with code that is too slow to use in the real world.

    I would say that my C++ (or most would call it C) implementation is elegant enough... easy to understand... no messy optimization tricks. Sure, I'm not using objects and templates everywhere, but these structures are hardly needed to solve this simple problem.

    > 2. Practically none of your functions are written tail-recursively.

    Good point.

    > 2.5. You use a list append (@) inside a loop (generateStates).
    > List.append is O(m), where m is the length of its first argument. If
    > you write an implementation, you'll see why. It probably doesn't make
    > much of a difference here (generateStates is only called once) but it's
    > something to watch out for.

    Of course, as you point out, generateStates has almost no effect on the running time. However, I wonder how you might implement that in an elegant way in OCaml without @. In C, I just looped over all numbers between 0 and 2^stateLength and converted the bit representations for the numbers to cell on/off states.

    > 3. For Pete's sake, man, you're using an association list for your
    > memos! Surely you know that lookup in an association list is O(n) in
    > the size of the list.

    I simply Googled for "memoization Ocaml" and found that code:
    http://www.emeraldtiger.net/modules.php?op= modload &name=News&file=article&sid=9

    The author pointed out how "sweet" polymorphism is... one block of code that can be used to memoize any function. Sweet indeed, and it certainly sped up my OCaml code a lot (without memoization, it was so slow as to be intractable for anything larger than about 4x4).

    So... maybe you can re-write higher-order memoization code using more efficient data structures? I would love to see that code, and I'm sure the OCaml community would benefit from having that in their toolbox.

    I agree that the memoization code is probably the problem in the OCaml version. However, this code came directly from the OCaml community and was the *only* example of memoization in OCaml that I could find.

    For Haskell, I used an infinite list of results that was filled in lazily as the results were needed. This also sped up the algorithm dramatically. However, I cannot get a Haskell compiler to compile itself on my platform, so I was testing all code in the Hugs interpreter, which made it too slow to be practical. Isomorphic compiled OCaml code was hundreds of times fast

    1. Re:Email response by Fahrenheit+450 · · Score: 4, Informative

      Okay... but here's my point: Every single example that shows how elegant Haskell and OCaml are uses lists. The 4-line Quicksort example for Haskell uses lists. All of the code that demonstrates easy reuse of functions and functions taken as arguments uses lists (like how easy it is to implement quite complicated algorithms using only map and filter, for example).

      Or perhaps more correctly, "every single example that you've seen". For a real quick one, look at Jason Hickey's Intro to OCaml (pdf) and have a quick peek at his Red/Black tree implementation. Or even cooler (if you're into that sort of thing) is the ever famous One Day Compilers talk.

      But what you're saying in #1 above is that in "production," speed-sensitive code, no one is using lists... this would mean that no one is using map, filter, or any other pieces of reusable primitive code. So, they are instead all using mutable data structures... I.e., they are programming with side-effects and loops (random access instead of recursion, even when ever element of an array/list needs to be accessed/processed).

      No. What he's saying in that you should use the best data structure for the job. Your best bet would have been to use the Hashtbl module from the standard library, or if you wanted to stay in the purely applicative, the Map module (also in the standard library) would have been loads faster...

      You are aware that there are more purely functional data structures (pdf) (OCaml implementations) than the list, don't you?

      So... maybe you can re-write higher-order memoization code using more efficient data structures? I would love to see that code, and I'm sure the OCaml community would benefit from having that in their toolbox.

      Here's a pretty neat example that uses arrays in a naive way, but you could certainly use, say, a map instead... And I'm pretty sure the OCaml community (by which I mean the people who would have helped you improve your code had you asked them) know about things like this.

      I don't think I spread any falsehoods. I mean, my experiment was real, and the results are real, and the code is there for people to inspect and try on their own. I also talked in my /. post about OCaml code that is isomorphic to C being fast, but functional code perhaps not being fast.

      Yes. And we inspected it, found it to be poorly written, and told you so. The "falsehood" here is that you claim that code written in a functional style is slow, when you really should have said "my code written in a naively functional style is slow". If I fill my gas tank with water, my car sure is slower than walking, therefore all cars are slower than walking... right?

      Trust me... I am *dying* to use OCaml or Haskell for real-world programming. I have spent the past month or so exploring these languages and trying to apply them to real programming problems. Especially when shootout results showed that OCaml was sometimes faster than C, and when I discovered that OCaml was much faster than Hasell, I was really starting to think that OCaml was a possibility.

      I put a link to tho OCaml mailing lists above. Use it. Ask questions of the list (you may want to start with the beginner's list). They can help you learn the language faster and better than google will.

      However, the ONLY reason why I would want to use OCaml is to take advantage of the expressiveness of pure functional programmin

      --
      -30-
    2. Re:Email response by Fourier · · Score: 3, Interesting
      So... maybe you can re-write higher-order memoization code using more efficient data structures? I would love to see that code, and I'm sure the OCaml community would benefit from having that in their toolbox.

      You get a significant boost just by dumping the list memoization in favor of a hashtable implementation. I'm not necessarily saying that's the optimal choice, but it's an easy drop-in replacement that is much better suited to the task. Here's a patch:
      --- Garden.ml 2005-03-14 13:22:04.000000000 -0500
      +++ Garden2.ml 2005-03-15 14:38:34.000000000 -0500
      @@ -135,8 +135,8 @@ let costList = map cost allStates;;

      (* Set up an associative list for memoization *)
      -let lookup key table = List.assoc key !table;;
      -let insert key value table = table := (key, value) :: !table;;
      +let lookup key table = Hashtbl.find table key;;
      +let insert key value table = Hashtbl.add table key value;;

      (* memoize any 3-parameter function *)
      @@ -150,7 +150,7 @@ let memoize3 table f x y z =
      result;;

      (* table for memoizing optLayout *)
      -let isCovered_table = ref [];;
      +let isCovered_table = Hashtbl.create 100;;

      (* checks if each cell in center colum is covered by an empty cell *)
      let rec isCovered c1 c2 c3 =
      @@ -266,7 +266,7 @@ and memo_fib n = memoize fib n;;
      *)

      (* table for memoizing optLayout *)
      -let optLayout_table = ref [];;
      +let optLayout_table = Hashtbl.create 100;;

      (*
      Also: learn to use the profiler! It takes about five seconds to see that camlList__assoc is killing you.
  12. Quick Haskell Rebuttal by Anonymous Coward · · Score: 3, Interesting
    This is a 10 minute proof-of-concept that Haskell shouldn't lag as much as claimed. It's hardwired for n*3 grids, doesn't use memoization or arrays, and it solves 7*3 in 15 seconds on my ancient hardware. 15 lines of code, not astoundingly elegant, but no optimization tricks at all. If anyone cares I will write a generalized version to kick C++'s arse later.
    import Word; import Bits; import List

    collength = 7
    full = 2^collength-1

    selfs c = (shiftR c 1) .|. c .|. ((shiftL c 1).&.full)

    invert c = foldl (.) id [if(testBit c i)then(flip setBit (collength-1-i))else id|i<-[0..collength-1]]
    0

    empties c = length [()|i<-[0..collength-1],testBit c i]

    valids = [((c1,c2,c3),e1+e2+e3)
    |(c1,s1,i1,e1)<-c's, (c2,s2,i2,e2)<-c's, (c3,s3,i3,e3)<-c's,
    c1==minimum[c1,i1,c3,i3],
    s1.|.c2==full, c1.|.s2.|.c3==full, c2.|.s3==full]
    where c's = zip4 cs (map selfs cs) (map invert cs) (map empties cs)
    cs = [(0::Word32)..full]

    bests = (best,[cs|(cs,score)<-valids,score==best])
    where (_,scores) = unzip valids
    best = minimum scores
  13. OCaml Evalation Is Strict, Not Lazy by j+h+woodyatt · · Score: 3, Informative

    If you want to program in a functional style, and you need lazy evaluation, you're going to find the standard library that comes with the compiler somewhat limited.

    I wrote some extensions for programming in OCaml in the functional style. Check out the OCaml NAE project, and look for the Core Foundation (Cf) package.

    --
    jhw