Make Lisp 15x faster than Python or 4x faster than Java
blog.postabon.com
blog.postabon.com
The finished program went down in runtime from 533.7s on my machine, to 24.42 seconds. If we adjust for machine speed (since the author's Python ran the python code in 474s), the comparative optimized result would be 21.7 seconds compared to author's hand-optimized lisp code at 30.2.
Note that this is probably an ideal case for Shedskin (which is a relatively new product: a 1-man job that's just a few years old compared to, I guess, hundreds if not thousands of man-years of research that have gone in LISP implementations).
Still, an interesting result.
And the project: http://code.google.com/p/shedskin/
SBCL:
* (test)
Evaluation took:
45.268 seconds of real time
Sun JDK Runtime was: 51086 Milliseconds
That's less than 15%, a far, far cry from '4 times' local radius = 6371
local function distance(latA, lngA, latB, lngB)
local latAr = math.rad(latA)
local lngAr = math.rad(lngA)
local latBr = math.rad(latB)
local lngBr = math.rad(lngB)
local deltaLat = latBr - latAr
local deltaLng = lngBr - lngAr
return radius * 2 * math.asin(math.sqrt(
math.sin(deltaLat/2)^2 + math.cos(latAr)
* math.cos(latBr) * (math.sin(deltaLng/2)^2)))
end
local start = os.clock()
for latA=-90,90,2.5 do
for lngA=-90,90,2.5 do
for latB=-90,90,2.5 do
for lngB=-90,90,2.5 do
distance(latA, lngA, latB, lngB)
end
end
end
end
local stop = os.clock()
print(stop-start)Java does actually keep a table for trig functions on small values -- they just haven't widened the table definition, which is filed as a bug at http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5005861
"I just performed some simple benchmarks of my own, and noticed that summing e.g. sqrt, sin and tan for small values are just as fast as their C equivalents. This was a great relief to me.
However, when computing sin in the range of pi/2 ... pi and 10 ... 10+pi/6 Java became 3-5 times slower, while the C program was just as fast. This is explained above by lack of precision, but for example sin(10.0) yields bit-to-bit exactly the same result in Java and C! This comparison was performed on a rather old 1GHz Mobile Intel Pentium, so I would assume that the precision has been fixed in virtually all modern processors, and therefore these operations could be intrinsified for a much larger value range (possibly the entire double range)."
Java and Python are actually doing all of the work to compute their
trigonometric functions
Not true in the case of CPython. It uses the underlying C implementation. See line 270 onwards.http://www.google.com/codesearch/p?hl=en#2T6lfGELm_A/trunk/M...
So always use small values?
http://shootout.alioth.debian.org/gp4/program.php?test=parti...
I thought you would only really be finding deals at distances small enough to just assume the earth is flat. Or at least, aren't those the only ones people would actually be interested in? If it's beyond my physical reach, I don't care how far it is. I'll get it shipped.
Or am I missing something?
Like I mentioned in the post, the distance function presented is a bit of a simplification I invented for pedagogical purposes (my goal was to examine languages, not geometry).
I don't mean to discourage you or be cynical. I love the fact that you are writing interesting articles on the practicalities of using Lisp with actual data behind it. So, keep up the good work. I look forward to more posts.
I'd like to compare it to C.
I just iterate through each latitude from -90 to 90 and each longitude from -180 to 180 in increments of 2.5 (for both the start and end). Check out the source code for any of the 5 examples (included in the post).
If you'd like, post the code up somewhere and I'll be happy to run it on the same machine and post the results along with the rest.
A charitable response would help catch23 understand what's different about this one problem from what we can sensibly presume as common experience - not Sun Java, JVM trig functions.
http://shootout.alioth.debian.org/u32/benchmark.php?test=all...
One of the guys behind R is also looking at moving to a LISP base after considering python:
http://www.springerlink.com/content/v38u176xp7j562m3/
That said, I will stick with python as I am not a programmer by profession and can't deal with a plethora of languages when I have a good swiss army knife as it is.
I think most people played with Scheme/Lisp in one class in college in the 90's and have been soured on it ever since.
I'm wary of analogies, because they can be tortured into "proving" anything you like. But what's faster, a car or a bike? In a big city during rush hour, a bike could very well be faster than driving. Clearly on the freeway, the car wins out.
For example, take a look at the solutions to PRIME 1; the task is to generate a list of prime numbers: http://www.spoj.pl/ranks/PRIME1/
The top 20 C solutions all take less than 0.05 seconds. The top 20 Java solutions all take less than 0.5 second. Python has several solutions that took 0.55 seconds. But for Lisp, only one user was able to get it to less than 1 second, and then only barely.
There's not enough data to draw any real conclusions, but it does make me skeptical of claims that Lisp is near in speed to Java or C (any Lisp gurus who want to prove me wrong, feel free to code up a fast solution and submit it)
It looks as if you only looked at the former as the later has an entry at 0.52 (as mentioned by another commenter.)
A note about Common Lisp implementations is that there are many of them, all with different performance specs ideal use cases. Some are considered very fast, such as SBCL and CCL, others, not so much. Most of the data I've seen benchmarking Lisp against other languages uses one of the open source implementations. I'd like to see how a commercial Lisp, such as Allegro or Lispworks, performed, especially since those are the ones that have actually been worked on since the 80s/
That said I think these kinds of benchmarks (and article titles) are misleading, because one who has deep knowledge in one language does not necessarily write good code in other languages. Even the article notes that "they are all just literal line-by-line translations of each other".
Now, I wonder whether I could use Lisp for what I do. One basic necessity would be to have a way to define a structure like this
struct s {
int32_t x;
int32_t y;
};
and then create an array of that type as struct s a[size], which uses exactly size * sizeof(s) bytes in memory. A further requirement would be to have a library of data structures (balanced trees, lists, hash tables) that support storing such structs directly without using pointers everywhere.Do you have any idea whether that could work with SBCL?
I usually don't code at that low level, but you might want to check out www.lrde.epita.fr/~didier/research/verna.06.imecs.pdf
$Weekend--;
Let me know if you're on Win32 and I will package for you my setup, with a double-clickable installer, and your choice of Emacs or Win32 friendly IDE :-)
I'm on an EC2 Ubuntu instance.
for latA, lngA, latB, lngB in itertools.product(*[xrange(-90,91), xrange(-180,181)]*2):
distance(latA, lngA, latB, lngB)
Not sure it's faster, but certainly more idiomatic.Also, the 2nd python example doesn't work; you need to drop all the 'math.' and use radians, sqrt, etc directly.
Python without Psyco : 522
Python with Psyco : 190
However, if you'd like to modify my code to do so I'd be happy to rerun the test and publish the results.
Deleted comment
If you care to do so, I'll be happy to include the results.
"I thought I'd demonstrate this functionality, with a bit of real world code. Postabon recommends deals to users based on a variety of factors such as the age of the deal, other people's votes, your preferences, and (relevant for this example) your distance from the deal. Most of the other factors can be computed asyncronously and just cached, but your distance from deals is computed a lot, and can't really be pre-computed since I have no way of knowing your location a priori (well, that's not strictly true, and we do do some memoization, but it has a relatively low hit rate)."
$ java -version
java version "1.6.0_0"
OpenJDK Runtime Environment (build 1.6.0_0-b11)
OpenJDK 64-Bit Server VM (build 1.6.0_0-b11, mixed mode)Get the sun version, it's probably faster.
Although it may not be much faster, found this unfixed bug report: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5005861
On the Hadoop project, checksumming was a major CPU constraint, because you have to assume every file everywhere is possibly on faulty hardware, and checksum everything. They found that it was actually faster to implement CRC32 in pure Java and let the JIT compilation do it's magic than it was to make native calls to zlibc.
But that may not apply to floating point if the Java implementation is bad enough.
Plain vanilla use of Math.sin and Math.cos = 9.15s Constrain to the range +45 to -45 degrees = 4.88s
As demonstrated 20 months ago http://shootout.alioth.debian.org/gp4/program.php?test=parti...
I coded it in haskell, and it is faster, if it's a correct translation. Without parallelizing, it clocks at 1.8 seconds, compiled with $ ghc --make. If anyone plans to compile this you'll need to $ cabal install geodetic, before $ ghc --make filename.hs
1.8 Seconds seems a little too fast though...
This was my theory, and I tested it by importing Debug.Trace, and then changing the line in question to:
let result = [traceShow [j,k,l,m] (distance j k l m) | j <- rangeLat, k <- rangeLng, l <- rangeLat, m <- rangeLng]
This will print "show [j,k,l,m]" when the actual distance is required. When I run the program, it prints what you'd expect: [[-90.0,-180.0,-90.0,-180.0]
0.0,[-90.0,-180.0,-90.0,-177.5]
2.2737639212328448e-32,[-90.0,-180.0,-90.0,-175.0]
9.095050533892612e-32,[-90.0,-180.0,-90.0,-172.5]
2.046381347867479e-31]
[90.0,180.0,90.0,180.0]
0.0
The stuff in brackets is the traceShow output, and it's mixed in with the printing of each result. Lazy evaluation is; it calculates only what is necessary. In this case, it's every (j,k,l,m) quad (so it knows which one is last), and then the 5 values you request.FWIW, your program runs in 14.28 seconds on my (old) machine, and the strict version runs in 48.94. (This includes a foldl' over the list that ensures every value is evaluated. It may add a bit of overhead.)
http://t-b-o-g.blogspot.com/2009/12/brians-brain-on-common-l...
It highlights the importance of profiling code first.
2. C translation takes 11 seconds (compare to 25.6 for SBCL)
http://gist.github.com/288936#file_silly distance loop
The speed difference is negligible since all but the inner multiplication is hoisted out, and each inner loop takes over 250 cycles.
http://shootout.alioth.debian.org/u32q/benchmark.php?test=al...
Roughly speaking, anything time-critical needs to be done in c.
As to "normal" people, I do a lot of numerical computing using python+C. I end up with probably 90% of the code in python, and 10% inner loops in C. That's typically enough that the C code is taking, say, 50% of the time, at which it isn't worth it (for me) to move more python into C, since I could never buy myself more than a factor of 2 speedup.
1.) A simple square macro will be faster than the expt function, because expt does special calculations for dealing with decimal exponents. All you really need to do is multiply it by itself. I think something like this:
(defmacro square (expression) (let ((symb (gensym))) `(let ((,symb ,expression)) (* ,symb ,symb))))
would work fine.
2.) you can turn degree to radian into a macro like so:
(defmacro degree-to-radian (deg) `(the single-float (* ,deg ,(/ (coerce pi 'single-float) 180))))
And avoid a couple of function calls and some multiplication. This way you also don't need to duplicate 'the single-float'...
On my machine, running clozure cl, this made it about 1/8th faster than the original optimized version.
3.) I was kind of wondering, could you do this sort of computation ahead of time for varying longitudes and latitudes, then store them in a table and do a simple look up and interpolation, instead of doing a lot of trigonometry (is this technically trigonometry?) at run time?
I'm not clear on the math exactly (whether it would work in this situation), but that sort of thing is done using pre-computed tables with fairly complex polynomial equations all of the time. In the case of a web app, it might be better to do this sort of thing (ram is cheap anyway).
I refer the honourable gentlman to Zed Shaw's "Programmers Need To Learn Statistics Or I Will Kill Them All"
I ran all tests 5 times each, and averaged the results. Each of the results for each of the tests were within 2% of each other, and the machine wasn't used for anything else while the tests were running.
I know it's not perfect, but I don't see any obvious sources of error ... I did provide all the source, so you're welcome to verify my results.
PS: I would probably inline the C function, but that's not a huge deal. My point was each language has its own optimizations; the only way to compare them is to write reasonably optimized programs in each of them and compare that.
Edit: I think Java's sin functions are slower than the HW implementation but more accurate, which is irrelevant because you don't care but it's something to look into. I dislike Java for other reasons, but it's easy to make code slow which code that looks very similar runs a lot faster.
That's one valid method. Another is to compare idiomatic programs from each language. Both are valid and both measure different things.
2. Inlining in an actual C implementation doesn't matter because there is no register pressure and each trig function generates a real function call. Function calls to known addresses are really fast (~2 cycles), even indirect calls are fast compared to transcendental functions (~ 6 cycles). For comparison, the C implementation takes ~260 cycles each distance computation. This can't be improved much without vectorizing the trig functions.
What do you call public static float distance(float latA, float lngA, float latB, float lngB) {
As I said but that's not a huge deal. Yes it's not a major slowdown, but a tight inner loop with a function call is begging for an inline.
might be your first clue...
> but a tight inner loop with a function call is begging for an inline.
The loop body makes several other function calls, you would be very hard pressed to measure the difference. And if it was a C function in a larger project, you probably wouldn't want to inline it because that would slow your compile times and cause you to lose modularity (i.e. you'd have to rebuild all client code rather than just update the DSO when you optimize the distance function). This function does enough work that it doesn't deserve to be inlined.