Dash.el is a lovely library, and one of the most popular on MELPA. If we can squeeze every last drop of performance out of it, everyone benefits.
Let’s take a look at the black art of making elisp faster.
Measure First!
Chris Wellons has a
great optimisation blog post that
discusses the performance overhead of creating lambdas with mapcar
.
If we look at --map
, it does indeed create anonymous functions:
Creating anonymous functions instantiates a closure, which isn’t free. Let’s write an iterative equivalent:
List Length | mapcar (seconds) | dolist (seconds) |
---|---|---|
1 | 0.000010 | 0.000028 |
1,000 | 0.0027 | 0.0079 |
100,000 | 0.74 | 1.24 |
Surprisingly, mapcar
is consistently faster in this particular
benchmark! Other Emacsers have
observed dolist
outperforming mapcar
for
short lists.
mapcar
is primitive, and primitives tend to be fast. dolist
clearly isn’t a speedup in all situations. Let’s try something else.
Matching Primitive Performance
Some dash.el functions are equivalent to primitive functions. For
example, -first-item
is equivalent to car
, -drop
is equivalent
to nthcdr
.
We could write -first-item
like this:
However, this adds the overhead of an extra function call compared
with calling car
directly. Instead, dash.el does this:
Let’s do a small benchmark, to ensure that defalias
giving us the
peformance we want:
Approach | time (seconds) |
---|---|
wrapper function | 0.1399 |
alias | 0.0055 |
use car directly | 0.0050 |
For shame! Our alias still isn’t as fast as using the primitive. Let’s
compare the disassembly using M-x disassemble
.
Intriguingly, these are not the same. There’s a car
bytecode that’s
being used with use-car-directly
.
With a little
help from the Emacs Stack Exchange,
we can see that byte-opt.el looks for 'byte-opcode
properties on
functions. If a function symbol has this property, the byte-compiler
will replace the function with custom bytecode.
This makes performance of -first-item
indistinguishable from car
!
We do lose the ability to advise -first-item
, but that’s not
possible with car
either.
Leveraging the Byte-Compiler
What about functions that aren’t just aliases? Can the byte-compiler help us here?
It turns out that the byte-compiler can actually calculate values at compile time!
Suppose we define a pure function that drops the first two items of a list:
If we annotate our function as pure, the byte-compiler helpfully runs it at compile time:
This works because we’re calling drop-2-pure
on a literal, and we
know the value of literals at compile time.
We can even annotate our functions as having no side effects. In this situation, the byte-compiler removes the call entirely:
The byte-compiler helpfully reports a warning here too:
value returned from (drop-2-pure x) is unused
Open Source FTW
The latest version of dash.el includes all these improvements, so you can simply upgrade to take advantage. If you find yourself needing to squeeze every last drop of performance from your elisp, you can follow what we’ve done here:
- benchmark your code (with
benchmark-run
orprofiler-start
) - disassemble your functions (with
disassemble
) - ask some friendly Emacsers (e.g. the Emacs Stack Exchange)
Good luck! May your editing experience never be laggy!