An example of fast numerical computation using Haskell
mit.edu
mit.edu
So here is the code: http://lpaste.net/96397
Here are my measurements:
time ./original 1000000
(329, 837799)
real 0m0.183s
user 0m0.180s
sys 0m0.001s
time ./new 1000000
(329, 837799)
real 0m0.017s
user 0m0.015s
sys 0m0.001sI would suggest it's best to show (at least) 4 versions of the code to compare two language: the naive implementation in both languages, and the really optimised version in both languages (plus intermediate levels of optimisation if you feel dedicated). That gives a picture of how fast the typical code one would write will be, as well as the potential for optimisation if you are experiencing bottlenecks, and how much effort it takes to optimise the code.
[1] http://augustss.blogspot.co.uk/2007/08/quicksort-in-haskell-...
Haskell version is totally naïve. That's the point.
Once you start modifying code to make it run faster it's no longer a 'naive' implementation. No matter how nice or logical you think those transformations are.
I'm not saying you can't compare the stream-fusion version against other languages, just that it's highly disingenuous to optimise the Haskell code with some external libraries but then argue that optimisations of the C version are unfair.
The first version [1] is simple and slow. The second version [2] is complex and fast.
The third, fast version [3] is identical to the first, simple version, with list processing functions imported from the `stream-fusion` package. This is the entire point of this article.
The only transformation shown is purely mechanical (prepending symbol names with the qualified import prefix `S.`), and even this is not required (as `Prelude` could have been imported `hiding` the appropriate symbol names instead).
[1]: http://www.mit.edu/~mtikekar/posts/src/collatz.hs
There is also no benevolent dictator in charge of Haskell.
http://research.microsoft.com/en-us/um/people/simonpj/papers...
http://www.reddit.com/r/haskell/comments/1br0ls/haskell_beat...
Likewise, stream fusion isn't always a win! Stream fusion and its siblings are great for pointwise operations, but aren't always a good idea for computations with heavy reuse, like nested convolutions or performant matrix multipy
If you want to use fusion, the best place is the `vector` package.
What we can do, is measure complexity empirically and extrapolate something from there. I haven't done a lot of tests with this code but to me it seemed like it would scale the same as the original one but had a much lower constant.
There's a little work on that in Lispland as well. Some discussion, a prototype, and a link to the older SERIES package: http://pvk.ca/Blog/Lisp/Pipes/
:tmp $ time ./collatz.native 1000000
start: 837799 length 329
real 0m0.782s
user 0m0.776s
sys 0m0.004s
exception Error of string
let (@@) f x = f x
let error fmt = Printf.kprintf (fun msg -> raise (Error msg)) fmt
let max (a, x) (b, y) = if x > y then (a, x) else (b, y)
let length start =
let rec loop len a =
let next = if a mod 2 = 0 then a/2 else (3*a+1)/2 in
if a != 1 then
loop (len+1) next
else
len
in
loop 0 start
let collatz limit =
let rec loop start longest =
if start >= limit
then longest
else loop (start+1) (max longest (start, length start))
in
loop 1 (1,0)
let report (start, len) = Printf.printf "start: %d length %d\n" start len
let main () =
let argv = List.tl @@ Array.to_list Sys.argv in
match argv with
| [] -> report @@ collatz 10000
| [n] -> report @@ collatz @@ int_of_string n
| _ -> error "usage: collatz [n]"
let () = main () Cython 0.47s less
...ignoring the cdefs, it's just as readable or more, at least for someone used to reading imperative code as opposed to functional code or mathematical equations. Plus that it takes you 5 minutes to quickly get from a quick and dirty Python hackoff to Cython. And you can stay blissful and ignorant about what "stream fusion" is, focusing on the problem you try to solve and not on the computer science of the tools. Haskell really is far far away from the "don't make me think" philosophy (or "don't make me think of anything else besides the real problem that I'm trying to solve"). No wonder Python and Cython are so loved in the scientific computing field :)Haskell is great if you want to expand your CS knowledge, but not if your math/physics/engineering problem is already so complicated that it uses up all your available brain cycles and working memory and you just can't squeeze thinking about language-specific optimizations too. I guess this is why it's only used by companies like Galois and such that are already neck deep in advanced CS problems and used to thinking about them all the time.
I am quite taken by Haskell, and I have the whopping mathematical background of two introductory courses from my institute of mathematics.
> But I'm referring to the cognitive overload incurred when you try to optimize it, thinking about list creation costs, then stream fusion to avoid i
The author didn't need to know what stream fusion was; all they did was import a library... it might as well have been called import MagicallyOptimizingPrelude and it would have the same effect.
That's not true. This article is about a library that works as a drop-in replacement of the default list processing functions in the Prelude. Eventually, it may be possible that these functions will be put into the Prelude itself. This would allow naive users to get all the performance gains from stream fusion without ever knowing it's happening. All of the "cognitive overload" is taken care of by the researchers who developed this technique and the library authors who implement it.
Having written quite a bit of Cython, I refuse to touch it anymore given how wildly undefined it's behavior can be with intermingling Python and C semantics.
Here is the code, perhaps someone else would know how to optimize it.
CODE:
function main(max_a0)
longest = max_len = len = a = 0
for a0 in 1:max_a0
a = a0
len = 0
while a != 1
len += 1
a = ((a%2==0) ? a : 3*a+1)/2
end
if len > max_len
max_len = len
longest = a0
end
end
println("($max_len, $longest)")
end
@time main(1000000)
CMD: $ julia collatz.jl
(329, 837799)
elapsed time: 16.436623922 seconds (4206873264 bytes allocated)
$ julia --version
julia version 0.3.0-prerelease+143Here is the new code:
function main(max_a0)
longest = max_len = len = a = 0
for a0 in 1:max_a0
a = a0
len = 0
while a != 1
len += 1
a::Int64 = ((a%2==0) ? a : 3*a+1)/2
end
if len > max_len
max_len = len
longest = a0
end
end
println("($max_len, $longest)")
end
@time main(1000000)
CMD: $ julia collatz.jl
(329, 837799)
elapsed time: 0.882312881 seconds (1377276 bytes allocated) a::Int64 = ((a%2==0) ? a : 3*a+1)/2
to a = ((a%2 == 0) ? a : 3a+1) >> 1
or a = div((a%2 == 0) ? a : 3a+1, 2)
for me this gives (329, 837799)
elapsed time: 0.449473338 seconds (544 bytes allocated)
For reference the C version gives ./a.out 1000000 0.40s user 0.00s system 99% cpu 0.407 totalanyway, when you think about it, you can skip everything between 0 and max_a0/2 because for any value there there is one with a longer chain (its double). that's a factor 2 in all languages.
proc main(max_a0: int): int =
var a, longest, len, max_len : int
for a0 in countup(1, max_a0):
a = a0
len = 0
while a != 1:
len += 1
if (a mod 2 != 0): a = (3*a + 1)
a = a div 2
if len > max_len:
max_len = len
longest = a0
return longest
# Main program starts here
echo(main(1000000))
Takes 0.76s (the C program in TFA takes 0.58s) next a = Just . join (,) $ (if even a then a else 3*a+1) `div` 2
len = (+1) . V.length . V.takeWhile (/=1) . V.unfoldr next
result = V.maximum . V.map (\x -> (len x, x)) . V.enumFromTo 1
main = do [a0] <- getArgs
let max_a0 = read a0 :: Word32
print . result $ max_a0
Results: haskell 5.0s, C 4.8s. Good enough I guess?PS: and this is how you do a proper benchmarking:
import Criterion.Main
main = defaultMain [
bench "data.vector/1M" $ whnf result 10000000
]
with result like http://lelf.lu/tmp/criterion.html resultQ :: W -> (W,W)
resultQ n = foldAllS max (0,0) $ runIdentity $ R.computeUnboxedP vec
where
fun (Z :. i) = len &&& id $ fromIntegral i
vec = R.fromFunction (Z :. fromIntegral n) fun :: Array D DIM1 (W,W)
Not as straight forward, but http://lelf.lu/files/fast-num-haskell-bench-N2.html (now you now number of cores on my macbook I guess).Not bad for 3 lines of code?
Then, I implemented it in Lua and JS and compared their performance to C and Haskell. It's not meant to be a (good) benchmark. Here you go: https://gist.github.com/killercup/7718804
tl;dr
Implementation | Time
-------------- | -----
clang 3.3 | 0.40s
luajit 2.0.2 | 0.86s
GHC 7.6.3 | 0.49s
Node 0.10.22 | 2.43s 329 837799
naive 3.06 sec
329 837799
decompozed 3.03 sec
329 837799
memoized decompozed 0.52 sec
The program
https://gist.github.com/xdenser/7722083 function intArithm(){
var
max_a0 = +process.argv[2],
longest = 0,
max_len = 0, a0,len, a,z;
for(a0=1;a0<=max_a0;++a0){
a = a0;
len = 0;
while( a != 1){
len++;
z = a >>> 1;
a = (!(a&1))?z:(a+z+1);
}
if(len>max_len){
max_len = len;
longest = a0;
}
}
console.log(max_len,longest);
}
the problem was with division by 2 operation which is not integer in js. So replacing it with shift >>> makes the trick.
Formula optimization gives additional 200 ms.And I doubt memoized c or lua variant will be 6 times faster than non-memoized. As it is not that linear.
So to put simply: you have two functions. They are semantically equivalent, but one is faster than the other. Why not use the faster one?
real 0m21.891s
On machine in which the C Variant: real 0m0.570s
But - I learned about Cython today, so great article.Interesting that the Python Version takes so much longer.
import sys
check=int(sys.argv[1])
lnum=1
lcount=1
for x in range(1,check):
len=1
a=x
while (a>1):
len+=1
if a%2==0:
a=a/2
else:
a=(a*3+1)/2
if len > lcount:
lcount=len
lnum=x
print lcount,lnum4.5 -> 2.7 seconds on my machine when using 'quot' instead of '/'.
2.4 seconds when hinting ^long in argument of collatz-length.
1.6 when also hinting ^long in collatz-next. Interestingly, when I hint only in collatz-next it takes 1.5.
I was also unsure if loop/recur was a good loop paradigm for this kind of problem (maybe for with variable would be better?) but I'd say in 1/3 speed of C it's pretty good for Clojure :)
$time ./c_compiled_with_O2 1000000
real 0m0.362s
user 0m0.360s
$time ./c_compiled_with_O3 1000000
real 0m0.189s
user 0m0.184s
I also tried dropping all the cdefs from the Cython code and running the Python version in Parakeet (http://www.parakeetpython.com) and it took 0.57s (whereas CPython without the @jit decorator took 19.57s). package main
import (
"log"
)
func main() {
max := 1000000
var maxa int
var maxlength int
for a0 := 0; a0 < max; a0++ {
var length int
a := a0
for a > 1 {
if a%2 == 0 {
a = a/2
} else {
a = (3 * a + 1) / 2
}
length++
}
if length > maxlength {
maxa = a0
maxlength = length
}
}
log.Println(maxlength, maxa)
}
Compiled with go 1.1: > go version
go version go1.1.2 linux/amd64
Result: > time ./collatz
2013/12/01 13:51:31 329 837799
./collatz 0,34s user 0,00s system 99% cpu 0,344 total package main
import (
"log"
)
func main() {
max := 1000000
var maxa int
var maxlength int
for a0 := 0; a0 < max; a0++ {
var length int
a := uint64(a0)
for a > 1 {
if a%2 == 0 {
a = a/2
} else {
a = (3 * a + 1) / 2
}
length++
}
if length > maxlength {
maxa = a0
maxlength = length
}
}
log.Println(maxlength, maxa)
}
Even more interestingly, this is even faster: > time ./collatz
2013/12/01 13:59:06 329 837799
./collatz 0,27s user 0,00s system 99% cpu 0,276 totalWithout start-up overhead. C versions via FFI.
https://gist.github.com/llelf/7721211 (quick'n'shitty, sorry)
I think the last bit of the line is:
for a0 in range(1,max_a0):
collatzLen(a0,a0)
a0 is basically the loop variable.Maximum finds the largest component of a list and compares on the first element of a pair first, so the pair with the longest sequence length as its first element and the starting value that produced that sequence length as it's second element will be returned by `maximum $ map (\a0 -> (collatzLen a0, a0)) [1..max_a0]`.
This function could also be expressed as `collatzLen >>= (,)`, if you were into that sort of thing.
I think the main reason functional languages are currently slower, in general, than procedural languages is that they haven't been popular for long enough yet to get all of the necessary optimizations implemented in the compilers. Think about it -- millions of person-hours have been invested into implementing optimizations in C and Fortran compilers over the past few decades. I expect the performance gap will get shrink steadily as time goes on, and as you said, at some point a cost/benefit threshold is reached where it makes more sense to switch to a functional language even if it's a little slower.
This really is a myth, there's been plenty of work showing you can compile the lambda calculus to abstract machines which map nearly one-to-one on to assembly. High level functional languages make the same performance compromises that high level imperative languages make in performance ( garbage collectors, runtime dispatch, etc ), but there's nothing inherently about slower about compiling functional languages.