TE
TechEcho
Home24h TopNewestBestAskShowJobs
GitHubTwitter
Home

TechEcho

A tech news platform built with Next.js, providing global tech news and discussions.

GitHubTwitter

Home

HomeNewestBestAskShowJobs

Resources

HackerNews APIOriginal HackerNewsNext.js

© 2025 TechEcho. All rights reserved.

Idiomatic Clojure without sacrificing performance

150 pointsby ceronmanover 3 years ago

12 comments

jwrover 3 years ago
A datapoint from a self-funded entrepreneur, for whom an app written in Clojure and ClojureScript provides a living:<p>Before you start discussing whether a language is &quot;slow&quot;, check if perhaps it is &quot;fast enough&quot;.<p>Apart from trying not to write stupid code, I don&#x27;t even optimize anymore, because coupled with ridiculously fast servers (bare-metal hardware from Hetzner in my case), it&#x27;s a waste of time. Things run great, and I&#x27;d much rather invest in building complex app features that customers will pay for, and have flexible living code that I can easily rearchitect as needed.<p>The last time I had to optimize Clojure code was when I wrote a search engine that had to respond to incremental searches in a fairly large E-commerce product databases. But that was also about 12 years ago, so computers were quite a bit slower, and JVMs were not as good as they are today.<p>I do tend to use transducers quite a bit, because they provide great composability and structure the code nicely, while providing a good performance boost as well (you avoid creating lots of intermediate data structures, which not only saves CPU, but also reduces cache, memory and GC usage) — but I don&#x27;t worry about performance too much. It&#x27;s just not a problem that appears on the radar.
评论 #28976901 未加载
评论 #28982113 未加载
MillenialManover 3 years ago
The biggest performance issue Clojure has, which isn&#x27;t mentioned in the article and is fundamentally unsolvable, is that it misses the CPU cache - a lot.<p>The data structure that drives the immutable variables, the Hash-Array Mapped Trie, is efficient in terms of time- and space- complexity, but it&#x27;s incredibly CPU cache-unfriendly because by nature it&#x27;s fragmented - it&#x27;s not contiguous in RAM. Any operation on a HAMT will be steeped in cache misses, which slows you down by a factor of hundreds to thousands. The longer the trie has been around, the more fragmented it is likely to become. Looping over a HAMT is slower than looping over an array by two or three orders of magnitude.<p>I don&#x27;t think there is really a solution to that. It&#x27;s inherent. Clojure will always be slow, because it&#x27;s not cache friendly. The architecture of your computer means you physically can&#x27;t speed it up without sacrificing immutability.
评论 #28976816 未加载
评论 #28976767 未加载
评论 #28976214 未加载
评论 #28976367 未加载
评论 #28978653 未加载
评论 #28976393 未加载
评论 #28984726 未加载
评论 #28977088 未加载
roenxiover 3 years ago
This does underline the weaknesses of Clojure and performance. For a typical language, the advice is (1) don&#x27;t call expensive functions and (2) loop less.<p>For Clojure, we get the wonderful quote &quot;There is a close transducer, partition-all, which has no step arity.&quot;.<p>One of Clojure&#x27;s biggest weaknesses in practice is that breaking in to those functional structures to figure out where the time is being spent or to debug them is harder than in other languages. This is a natural trade-off of developing a terse and powerful language.<p>It is notable how much trouble Clojure has had over the years linking a bug back to a line number and a reason. Even with spec.
评论 #28975350 未加载
Blackthornover 3 years ago
The performance difference between (peek n) and (last n) for vector typed n strikes me as something that really ought to be handled by the implementation, not the programmer since the outputs are equivalent.<p>I really like clojure, like, a lot, but I might have taken an opposite than intended result from this blog post. It made me feel like the language has a number of performance footguns that don&#x27;t necessarily need to exist.
评论 #28975327 未加载
评论 #28975201 未加载
thomover 3 years ago
I personally feel that people for whom all the underlying JVM&#x2F;Java stuff is deeply icky and unidiomatic are missing out on a lot. Clojure has some deeply held principles but also a whole heap of pragmatism and that’s what’s always kept it useful to me.
评论 #28975665 未加载
medo-bearover 3 years ago
i like in depth optimisation posts like this post. however it starts by expressing disagreement with the statement that idiomatic common lisp is much faster than idiomatic clojure, but in the end doesnt do much to dispell this. all it does is it shows how much in depth knowledge of clojure and jvm you need in order to still be slower than a very simple implementation in common lisp. i myself am quite new to common lisp, and comming from the machine learning and scientific computing background, i am VERY impressed by its speed, interactiveness, and just general neatness of programming. i don&#x27;t think there exists a language that comes close to this. i wish there were more members from ml and scientific community involved in the common lisp community. in fact there is one person in the clojure community who i would love to try common lisp: <a href="https:&#x2F;&#x2F;dragan.rocks&#x2F;" rel="nofollow">https:&#x2F;&#x2F;dragan.rocks&#x2F;</a>
评论 #28977364 未加载
评论 #28976245 未加载
drcodeover 3 years ago
I am someone who uses Clojure a lot and understands transducers, but I would pretty much never consider transducers &quot;idiomatic code&quot;.<p>In my opinion they are a straight-up an optimization and using them definitely reduces code readability for the vast majority of people who would ever read your code (yes, it makes your code less readable even if it sometimes doesn&#x27;t increase the token count of your code)
评论 #28978920 未加载
delegateover 3 years ago
Clojure&#x27;s power is not in its ability to be fast.<p>Immutability has a price and it&#x27;s a price we&#x27;re consciously paying. JVM also has a performance cost, but it&#x27;s well worth paying due to the wins of running on a virtual machine.<p>For most boiler-plate tasks, especially where I&#x2F;O is involved, Clojure is fast enough (eg. handling HTTP requests or talking to a database).<p>The true power of Clojure, in my opinion, is in how it allows you to think about problems; that machines produce side effects by interpreting a data structure, that the program is one transformation of the input data structure to the target data structure for the desired side effect. You don&#x27;t think in terms of imaginary objects &#x27;doing&#x27; things, you think about the input data structure and how the output data structure should look like, such that if fed to a library function, the desired side effect will occur (eg. the text will be printed or the robot arm will rise).<p>This mode of thinking about problems is closer to how machines actually work on a higher level and amusingly, &#x27;close to the metal&#x27; C++, offers abstractions which take the writer into a fairy tale of class hierarchies, methods that &#x27;do&#x27; things to other things, inserting, deleting, mutating everything all over the place. Because that&#x27;s efficient and fast (and dangerous). But it&#x27;s also focused a lot on telling the machine &#x27;how&#x27; to run; while coincidentally making it do &#x27;what&#x27; you want it to do. As a wizard who can put that orchestra with swords and knifes in motion without too much blood spilling on the floors, you can&#x27;t but feel proud to have achieved it.<p>Data in -&gt; data out. That&#x27;s Clojure&#x27;s way of thinking.<p>It&#x27;s a lot simpler way of looking at the problem and maybe makes the `-&gt;` feel a bit insignificant and maybe not that important in most cases, as long as it produces the correct output.<p>The biggest performance hit will be choosing the wrong data structure for solving the problem in a functional way. If that&#x27;s done correctly, Clojure does offer some tools for squeezing a bit more out of performance critical pieces of code (the articles covers some). Those should be a couple of functions in all your code base; for the boilerplate and domain logic or code involving non realtime I&#x2F;O, it doesn&#x27;t matter that much.<p>If performance is a big concern and you end up writing imperative code in Clojure, I think you&#x27;ll be better off writing the bits in Java or choose a different language, which is better designed for that purpose.
creamytacoover 3 years ago
Pretty sure that this isn&#x27;t the post&#x27;s purpose but it makes me want to never touch Clojure. I&#x27;m obviously not used to it however I can&#x27;t help but think that it takes serious mental gymnastics to consider the &quot;optimized&quot; code the author came up with as even remotely good, from readability &#x2F; comprehensibility &#x2F; extensibility perspective.
评论 #28984316 未加载
评论 #28976791 未加载
eigenhombreover 3 years ago
For the record, I&#x27;m the author of the original post on optimizing Common Lisp that the OP is addressing. The OP is a wonderful deep dive into optimizing Clojure. However, aside from possibly from avoiding the &quot;last&quot; function, and maybe using &quot;keep&quot; rather than &quot;map&quot; + &quot;filter&quot;, I personally would avoid some of the strategies he outlines unless I truly needed the performance.<p>Transducers, for example, have a couple of counter-intuitive features that make them harder (for me, at least) to read... the order of &quot;comp&quot; arguments, for example, is reversed from normal usage. We tend to avoid them at my work unless they are truly needed for performance. In a complicated business domain, we struggle much more with building understanding than with slow code, so readability matters quite a bit.<p>(I leave aside the question of what is &quot;idiomatic,&quot; since it seems pretty subjective, and probably depends somewhat on the crowd you run with.)<p>As several people have written here, Clojure is fast enough without optimization most of the time. (I leave aside the question of startup time -- a very different discussion.) Though I&#x27;m <i>curious</i> how to get good performance, most of the time it matters more to me for the code to be easier to <i>understand</i> than for it to run optimally fast, unless the code creates a bottleneck that has some clear impact on usability.<p>My goal in writing the original article was to play with optimizing Common Lisp as a learning exercise, rather than to throw down the gauntlet and assert that &quot;Clojure is slow.&quot; The author&#x27;s response was to show how to optimize the Clojure, which was very interesting! Ultimately, both languages have their strengths and weaknesses, and I consider myself lucky to be able to write Clojure for pay.
评论 #28984219 未加载
eggyover 3 years ago
So in the end, a vanilla CL implementation is still slower than all the work that went into the Clojure version? I love SBCL, and I am rediscovering Lisp after playing with a lot of other PLs. This is reassuring to me in my choice to go with SBCL and CL.
didibusover 3 years ago
On Slack I gave it a go as well, my naïve idiomatic solution turned out to be 50x faster from the get go.<p>Times are all from my laptop using the same test input and running on JDK 11.<p>Baseline smt-8: Execution time mean : 2.386019 sec<p>My first attempt: Execution time mean : 48.977240 ms<p><pre><code> (defn smt-8&#x27;&#x27; [times-vec] (loop [res (transient []) pointer-1 0 pointer-2 7] (if-let [end-element (get times-vec pointer-2)] (let [start-element (get times-vec pointer-1) time-diff (- end-element start-element)] (recur (if (&lt; time-diff 1000) (conj! res [(subvec times-vec pointer-1 (inc pointer-2)) time-diff]) res) (inc pointer-1) (inc pointer-2))) (persistent! res)))) </code></pre> With performance tweaks: Execution time mean : 23.567174 ms<p><pre><code> (defn smt-8&#x27;&#x27;&#x27; [times-vec] (binding [*unchecked-math* true] (loop [res (transient []) pointer-1 (int 0) pointer-2 (int 7)] (if-let [end-element (get times-vec pointer-2)] (let [start-element (get times-vec pointer-1) time-diff (- end-element start-element)] (recur (if (&lt; time-diff 1000) (conj! res [(subvec times-vec pointer-1 (inc pointer-2)) time-diff]) res) (inc pointer-1) (inc pointer-2))) (persistent! res))))) </code></pre> Less idiomatic as I&#x27;m using an array: Execution time mean : 1.678226 ms<p><pre><code> (defn smt-8&#x27;&#x27; [^&quot;[J&quot; times-arr] (binding [*unchecked-math* true] (loop [res (transient []) pointer-1 (int 0) pointer-2 (int 7)] (if (&lt; pointer-2 (alength times-arr)) (let [start-element (aget times-arr pointer-1) end-element (aget times-arr pointer-2) time-diff (- end-element start-element)] (recur (if (&lt; time-diff 1000) (conj! res [(mapv #(aget times-arr (+ pointer-1 %)) (range 8)) time-diff]) res) (inc pointer-1) (inc pointer-2))) (persistent! res))))) </code></pre> All my solutions produce exactly the same result where it includes both the list of elements and their time difference.
评论 #28983789 未加载