1

Reading code in the wild, I seem to see less of the "many small functions doing one thing each" pattern in emacs lisp. Might this be due to calling functions being expensive (like in Python), so that you rather inline as much code as possible?

Is calling elisp functions expensive?

closed as primarily opinion-based by lunaryorn, Drew, Constantine, user2699, Dan Jan 23 '16 at 18:27

Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. If this question can be reworded to fit the rules in the help center, please edit the question.

  • 2
    I don't think that there's a general answer to this question, hence I voted to close. In Flycheck however we don't try to inline explicitly, and try to write small isolated testable functions, and haven't had any issues. Emacs Lisp is probably not fast, but it's fast enough. – lunaryorn Jan 22 '16 at 21:00
  • 1
    Define expensive. The question should be closed as primarily opinion-based. – Drew Jan 22 '16 at 21:54
  • 2
    Expensive is defined in terms of percent overhead for a call to some given function versus inline, which is easy to test. Would vote to keep open, as it's good to know about. – Brian Burns Jan 22 '16 at 23:51
  • Your question would seem to be, why does most elisp tend to not break functions up into small parts. – user2699 Jan 23 '16 at 5:20
  • Thanks. Feel free to close, but wouldn't it be good to keep this q around? Lots of good answers and even interesting comments from maintainers of much used packages. And I could find no answer anywhere else... – The Unfun Cat Jan 23 '16 at 16:33
6

To quote the Emacs Lisp Manual, appendix D.4 Tips for Making Compiled Code Fast:

Use iteration rather than recursion whenever possible. Function calls are slow in Emacs Lisp even when a compiled function is calling another compiled function.

(Emphasis is mine.)

Whether the function call overhead is something one should worry about is a different question -- and I don't really want to go there. :-)

2

I don't have much to say about Emacs Lisp efficiency, except that I don't think it is the reason for this phenomenon.

First and foremost, the language's ecosystem shapes its culture and practice. Emacs' lack of support for namespaces, modules (not the same thing as packages), and standard libraries has resulted in a culture where each contributor tries to make their code "self-contained". This inevitably results in lots of duplicate work or "God functions". Older code, which was written without niceties like package repositories and dependency management, is particularly prone to this.

Also keep in mind that Emacs Lisp (or Lisp in general) is not strongly associated with the functional programming paradigm, where each function is designed to take inputs and produce outputs, avoiding (or at least minimizing) side effects.

Finally, Emacs (and GNU software in general) has never really bought into the Unix philosophy of "small sharp tools". In fact, even Unix doesn't follow the Unix philosophy.

1

I've noticed this a lot also - functions that are pages long that could be decomposed into smaller (and reusable) functions. They're harder to read that way - pulling out some semantically meaningful functions would make them easier to follow, test, debug, and modify.

It might be leftover from days when computers were slower and had less memory, or due to the extra overhead of refactoring the code to make it more readable.

But there is a performance hit - here's a test of inline code vs a function call for both a simple and (simulated) complex function -

(defun simple-fn () (+ 1 1))
(benchmark-run 10000000 (+ 1 1))
(benchmark-run 10000000 (simple-fn))

(defun complex-fn () (dotimes (n 100000) (+ 1 1)))
(benchmark-run 100 (dotimes (n 100000) (+ 1 1)))
(benchmark-run 100 (complex-fn))

All times in seconds

| test          | simple fn | complex fn |
|---------------+-----------+------------|
| inline        |      0.65 |       2.23 |
|               |      0.74 |       2.27 |
|               |      0.67 |       2.22 |
| avg           |      0.69 |       2.24 |
|---------------+-----------+------------|
| function call |      1.24 |       2.21 |
|               |      1.17 |       2.26 |
|               |      1.21 |       2.23 |
| avg           |      1.21 |       2.23 |
|---------------+-----------+------------|
| difference    |       75% |         0% |

The complex function has less of a performance hit because more time is spent in the function than the function call.

So there would definitely be a cost for having lots of smaller functions.

But as the old saying goes, premature optimization is the root of all evil - it's best to code for readability and then optimize where needed.


I also tested with benchmark-run-compiled - the results were basically the same for the complex function, but for the simple function I got negative numbers, despite a wait of a few seconds, so I'm not sure what was going on there: (benchmark-run-compiled 10000000 (+ 1 1)) => -0.13.

  • Hmm. benchmark-run is a macro, so you should not quote forms passed to it. Compare (benchmark-run 10 (sit-for 1)) and (benchmark-run 10 '(sit-for 1))). Also, what about the call overhead in compiled elisp code? – Constantine Jan 23 '16 at 0:04
  • @Constantine Thanks - that did make quite a difference - went from 11% to 75% for the simple function. But I had problem with running benchmark-run-compiled - kept getting negative numbers. – Brian Burns Jan 23 '16 at 0:26
  • As you can see in its doc. string, benchmark-run-compiled wraps forms passed to it in a (lambda () ...) and compiles that function, and then tries to take the cost of function calls into account by timing (lambda ()). For some reason calling an empty function (i.e. returning nil) turned out a bit more expensive than calling what amounts to (lambda () 2), so you got negative numbers. In other words, it seems that benchmark-run-compiled is not accurate enough in your case. – Constantine Jan 23 '16 at 0:37
  • @Constantine Okay, that's interesting - I imagine the performance hit would be more for compiled code, due to less time spent in the function - I'll revisit it later when I get a chance. – Brian Burns Jan 23 '16 at 0:48

Not the answer you're looking for? Browse other questions tagged or ask your own question.