Hacker News new | ask | show | jobs
by gengstrand 1583 days ago
I started playing around with Clojure back in 2014 by implementing a rudimentary polyglot persistent news feed microservice. That microservice used Ring which sits on top of Jetty. I blogged about that implementation at https://glennengstrand.info/software/architecture/oss/clojur... which is my personal blog. Last year, I re-evaluated Clojure with a feature identical microservice. This time, I integrated with Donkey which sits on top of Vert.x and https://glennengstrand.info/software/architecture/microservi... is where I blogged about that implementation.

I put each of these implementations in a load test lab where I collect then analyze the performance data for comparison purposes. Those two blogs include the performance results. It is hard for Clojure to compete with other tech stacks, primarily because each method call in Clojure goes through Java reflection which is pretty slow. There is a way to prevent the use of Java reflection in Clojure but then the Clojure code doesn't end up looking very Lisp like. The usual advise is to use type hints very sparingly like in hot spots in the code.

I still like Clojure, though.

4 comments

Clojure absolutely does not reflect on every call, and no it does require type hinting every argument to every function to get that speed. Especially after Clojure 1.8 came out, huge performance gains were had by inlining far more direct function calls. You can achieve near Java performance on most code, especially if you are okay sacrificing the immutable data structures and lazy sequences. I think this is what makes Clojure shine: you get a beautiful, functional, immutable, easy to understand world, and mostly it is extremely performant, and if it's not, you can have a few functions wrapping uglier but more performant bits, and Java interop is also extremely easy if you want to drop another level down. All the Java tooling for analyzing stack traces and bottlenecks works pretty well if you need to chase performance issues too. And my god it's going to be a lot faster than a lot of Ruby and Python etc apps. I say that with love as long term Rails programmer.
You do still need to use type hints to achieve best performance, especially in numerical code. That said, even if I’m doing primitive math and using arrays in Clojure for performance sensitive code, it’s still a nicer experience than other JVM languages for me.
> It is hard for Clojure to compete with other tech stacks, primarily because each method call in Clojure goes through Java reflection which is pretty slow.

That's not true. Clojure functions calling each other do not use reflection. It is only when interoping with Java, and trying to call a Java member method of an object that reflection might be used if there are multiple concrete implementation of the method and the compiler can't find the right one at compile time. In which case, you can provide a type hint to tell it which one you want.

Additionally, you can use Leiningen's `check` to quickly identify all the places that need hints. It's not something you need to keep in mind while you're working.
You can take a look at this blog post to learn more about reflection calls in Clojure. There are other posts in that blog that goes deep into Clojure high performance.

https://cuddly-octo-palm-tree.com/posts/2022-02-20-opt-clj-6...

Like others said, Clojure does not reflect on every call.

A good resource if you really want to push the perf limit http://clojure-goes-fast.com

Most data focused microservices for business applications devote most of their code to calling databases. The PoC microservices that I implemented in order to evaluate Clojure use Java libraries to communicate with Cassandra, MySql, Redis, and Elastic Search. Clojure is not currently at a point in adoption where it makes a lot of sense for these database vendors to support native Clojure libraries.

While it is true that not every method calls goes through Java reflection, it is also safe to say that every API call to this kind of service will end up making a lot of calls via Java reflection unless you use types hints all over the place in the DAOs.

The reflection lookup takes about 0.04 msecs extra on my laptop.

When you're doing things like I/O, an extra 0.04 milliseconds won't matter much, since the operation is already a lot slower then that. So hinting in those case generally isn't really needed, as it won't cause visible performance gains in your application.

Where hinting is worth it, is in tight loops where 0.04 ms will add up. Numeric code that uses arrays especially benefits, since the benefits of the array are lost to the reflection overhead otherwise.

Your claim, that the latency cost of reflection is small in comparison to the latency of performing I/O, seems reasonable assuming that the service is processing only one request at a time. That is not the case for typical microservice architectures and certainly not what is happening in the load test by which each of these service implementations are compared.

There is an upper limit of requests to which any service can handle concurrently. Usually, that limit is based on memory but there could be other factors too. After that upper limit is reached, additional requests wait in a queue or get rejected. From Little's Law, we know that the number of requests in any system is the product of the rate of ingress and the average time it takes to service a request. Since that number of requests is bounded, even small increases in the average process time can have a significant negative impact on the ingress rate that the service can sustainable handle. If you are not familiar with queuing theory, then perhaps you have heard of onlooker delay being the major cause of traffic jams.

Is running Clojure services with Java reflection the end of the world? Of course not. Just be aware that it does make performance for those kind of services look not quite as efficient when compared with services that do not use Java reflection.

For sure. YourKit is an excellent piece of software to discover the hot spots in the code that are the bottleneck in such cases. It takes just a few minutes to discover and add the type hints in the proper places.