Need advice - better documentation and guidance in clj-fast

I have been working on clj-fast for around two years now. What started as an exploration congealed into a collection of tools and heuristics for Clojure code speedups under certain conditions.

A fair criticism I have received is that the use cases for the library and guidance around it could be clearer. I have made an attempt to rewrite significant portions of the README but frankly, I don’t like the result, so I thought I’d come to you for aid :slight_smile:

How should the new documentation look like? Thinking of criteria like structure, use cases, guidance, cautions. Are there any criteria I neglected?

Generally, the library has a few use cases I can think of:

  • reference material on PGO and benchmarking in Clojure
  • reference for specific optimizations possible in Clojure
  • for library authors: deliver good performance while hiding the “ick” implementation details
  • application developers: not a drop-in replacement, but sometimes performance has to be improved and they don’t have to sacrifice most Clojure idioms or drop down to Java.

It’s a pretty low level library and I don’t expect it to, or think it should, gain wide-range adoption, but I would like it to be accessible to those who need it, when they need it, and that the tradeoffs be clear.

Looking forward to your comments

4 Likes

My guess is that you’re getting that comment from people who don’t have an optimization problem, but instead who might be more junior or less knowledgeable about trade offs and performance in general, who have a bit of self doubt or OCD about having things be as fast as it can even if not necessarily because they’ve found it that their program is too slow.

So I’m guessing people who don’t already have a written piece of code, but instead people who are writing one might be mistaking your lib as more of: core functions are slow, use this lib instead.

And my guess is those people are looking for generally applicable “best practices”.

I say that because I think someone that finds your library and knows they need to make something faster wouldn’t really need guidance, beyond maybe seeing a benchmark to know if it’s worth their time.

4 Likes

That’s very kind of you, and you might be surprised, but this comment came from very experienced and knowledgeable individuals, whose opinion I appreciate. The thrust was that such a library could be misleading, in that it might present as a drop-in replacement when it is not suitable for all scenarios. Which is not something I considered, but should have.

1 Like

I think the README is great. I am not the kind of expert that likes to read JVM machine language, but I have enough sense to perform aggressive optimization only when it seems valuable, and with careful benchmarking. For me, the README is just what I would need.

My only thought is you might want to consider expanding the initial description (“Faster idiomatic Clojure”) and the first section below it, which might mislead the kind of person who could be misled in the ways that you’re worried about. Of course, if they read further, they’ll be enlightened, but maybe it’s worth worrying about a tl;dr effect where someone just scans for the project.clj include statements. Or maybe it’s not worth worrying about that.

From the README:

Some tools such as Leiningen suppress the JIT to enable faster start-up times.

Wait, what?? Oh noo.
(Thanks for the tips about how to manage that.)

Also thanks for the tips about current tools. (I wasn’t sure whether Criterium was still what was recommended–used it a lot in the past–and I have never used a profiler for Clojure, but will in the future when it seems useful.) Also thanks for pointing to other libraries that offer potential speed-ups.

3 Likes

Thank you. I’ll try to see how I can integrate your comments here. Just making sure, are you referring to the original README or the rewrite?

yeah, that was a fun exercise figuring out that particular performance disparity.

1 Like

The rewrite, I assume. The one that was linked in the original post today.

(And fwiw when I wrote “I am not the kind of expert that likes to read JVM machine language”, I meant that I am not an expert at all, and I don’t like to read JVM lang (even if I have suffered through little bits of it once or twice).

In general, I think it looks pretty good. If I was looking for things to complain about though, here’s my one critique:

“Providing faster implementations of Clojure’s core functions as macros.”

This seems like it’d be a foot-gun for new folks, if they don’t know what the trade offs are of replacing a function with a macro and expecting it to act the same way, semantically - like how you can’t pass macros around in higher-order ways like you can with functions.

And I’d worry that Clojure veterans might look at the lib and be reluctant to recommend it to new folks, without first educating them on the pros and cons of such a strategy.

If I want the power to replace a fn with a faster macro, though, I want the freedom to choose that option. And I think those macros serve a good purpose there, to help those that can and should use them.

A few things I’d recommend: explicitly call out all those fns that are getting turned into macros - label them, perhaps… Or, come up with a naming scheme, like all fns transformed into macros start with m-, like m-foo or whatever. Or partition all such macros, which change the semantic of a fn to a macro, into another namespace, where those changes in behavior are clearly demarcated.

If there are enough of the non-macro functions that provide performance boosts, I’d almost recommend presenting those separately, and advertising them as their own value proposition, separate from the macro’izing versions. If there are enough of those, I’d offer that in a “beginners” section, then set the macros apart in another “advanced” section, where you warn people that if they’re new, they’re about to have to learn something new to use these new macros safely.

That being said, it’s an advanced library, mostly for advanced users, so maybe it’s over qualifying and over conditionalizing for an audience that doesn’t exist… But at the very least, maybe veterans would feel more inclined to share it with curious newbies if it had more educational content for them. Just some thoughts…

1 Like

Side remark: CLJS has the concept of macro-fns where, in call position, the call goes through a macro and in higher order position, the call goes through a function. This can be used for compile time optimisations without having the restriction of macros for the non-function position case (hope I’ve correctly summarised this).

2 Likes

There’s still other caveats though, right?

All the params to the macro version still need to be given as the macro literally expects them, right?

Yes: the macro can only see the s-exprs you literally pass to it, as usual.

Mike Fikes wrote a blog about it: FikesFarm Blog: ClojureScript Macro-Functions

1 Like

Isn’t that what the inline meta is for in Clojure? Like some functions can use it to inline themselves?

1 Like

The second link is the rewrite

Yes, I had an issue with it in the past but I think I figured out how I can “install” the inline implementation correctly. But in some way it’s even worse, because then the user doesn’t know when they’re using the inlined version and when they’re using the core version, while using the macro directly from the library will throw an exception at compile time.

That’s what I’m trying to navigate around now. Even if you’re using inline/get-in it’s clear it’s not get-in. Just don’t :refer to it.

Ya, I think with :inline you’re supposed to keep the behavior identical between the two, so your optimization should only be inlining, you shouldn’t change any semantics otherwise.

You’re often specializing the function to a specific type aren’t you? Maybe you could either namespace that or have the name reflect it?

Like: clj-fast.associative/get-in. Might make it more clear that the function is restricted to some type or interface.

You see? These questions are evidence my documentation is not good enough :upside_down_face:
For example, :inline also works with :inline-arities, which while we usually know as a set, is actually a function. I already use that function as a :pre condition, I can use it there.
Generally, the main api of the library is the inline namespace aided by the core namespace.

Yes, that’s the one I read.

1 Like