Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed-optimize '--each-while'. #153

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open

Speed-optimize '--each-while'. #153

wants to merge 8 commits into from

Conversation

doublep
Copy link
Contributor

@doublep doublep commented Aug 23, 2015

I want to optimize several functions in the library. For a start, here is the first optimization. Tell me if it's OK to continue or if I shouldn't waste my time.

Reasoning: dash is low-level library used in many other places. It contains very generic functions that a purpose-agnostic and may or may not be used in performance-critical places. Implementation of the functions is also pretty simple, usually 1--10 lines. Therefore, I think, performance here is more important than code clarity.

This patch optimizes '--each-while':

  • don't bind 'it' in a loop, it's slow: move it to encompassing 'let' instead;
  • get rid of 'continue': we can just set 'list' to nil to break the loop early;
  • rearrange loop body a bit so that there are fewer gotos in compiled code.

Benchmark:

(defvar --large-list-- (--map-indexed it-index (-repeat 1000 nil)))
(benchmark-run-compiled 10000
  (--each-while --large-list-- (< it 500)))
(benchmark-run-compiled 10000
  (--each-while-optimized --large-list-- (< it 500)))

Before: 0.33+ seconds here, after: 0.28+ s here (runs differ a lot, I just repeat them several times and pick the best.

@Fuco1
Copy link
Collaborator

Fuco1 commented Aug 23, 2015

I have no problems with this.

@magnars
Copy link
Owner

magnars commented Aug 23, 2015

Hi! This is a great initiative. One issue: Do you have your FSF paperwork signed?

@doublep
Copy link
Contributor Author

doublep commented Aug 23, 2015

Yes, I signed them many years ago. You can find a few my changes in Emacs' ChangeLog and in AUTHORS.

@Fuco1
Copy link
Collaborator

Fuco1 commented Aug 23, 2015

Speaking about benchmarks, if we're going to be doing such an optimization project, it would make sense to prepare some suite we could run automatically, ideally covering all the functions.

Obviously you don't need to write a benchmark for every function, but we could at least discuss and come up with some framework, ideally as automatic as possible (something alike to how tests work now)

@doublep
Copy link
Contributor Author

doublep commented Aug 23, 2015

The problem is that benchmarking seems to give really wide result distribution. So, to get a good estimate you'd need to run each benchmark at least 10 times, if not 100.

@Fuco1
Copy link
Collaborator

Fuco1 commented Aug 23, 2015

I guess it has lot to do with GC and other internals too... there might be some ways to prepare "good states", or alternatively always run in a clean emacs instance (loading up one with -nw -q doesn't take long). Plus, if the whole benchmark runs less than 5 minutes I think it is acceptable (if we provide some way to run single benchmarks separately).

Though this is maybe not a task for dash but for some generic framework to be developed (a la ert). Maybe there even already is something like that, dunno.

@doublep
Copy link
Contributor Author

doublep commented Aug 23, 2015

Here is a simple try. I explicitly run GC before every benchmark-run-compiled. 10 is meant to be configurable later.

(defmacro --benchmark (&optional repetitions &rest forms)
  `(-let ((all-results nil))
     (--dotimes 10
       (garbage-collect)
       (!cons (benchmark-run-compiled ,repetitions ,@forms) all-results))
     (--benchmark--result 10 all-results)))

(defun --benchmark--result (num-tries all-results)
  (-let ((best-result (--min-by (- (nth 0 it) (nth 2 it)) all-results)))
    (if (= (nth 1 best-result) 0)
        (format "Best of %d tries: %.3f s" num-tries (nth 0 best-result))
      (format "Best of %d tries: %.3f s (%d GC runs)"
              num-tries (- (nth 0 best-result) (nth 2 best-result)) (nth 1 best-result)))))

(defvar --large-list-- (--map-indexed it-index (-repeat 1000 nil)))
(--benchmark 10000
  (--each-while --large-list-- (< it 500)))
(--benchmark 10000
  (--each-while-optimized --large-list-- (< it 500)))

(error "Unhandled selector '%s'" selector))))

(defun select-benchmarks (selector)
(nreverse (--filter (-let (((name details _ &as benchmark) it))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, does this let binding work? &as should be in the beginning, foo &as <destruct-here>.

I guess we could theoretically make it work both ways... but it is different from clojure at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. It "sort of works" only because I never use the benchmark variable, but it is wrong. I will commit a fix.

How often GC is called and how much time it takes also indicates
function's efficiency.  We already call 'garbage-collect' before
benchmarking.
As a side effect, (-cons*) evaluates to nil rather than fail with an
error.  And with a silly number of arguments it no longer exceeds
recursion depth limit.
@doublep
Copy link
Contributor Author

doublep commented Aug 27, 2015

I squashed benchmark framework commits, also with a fix for that wrong &as you noticed.

@Fuco1
Copy link
Collaborator

Fuco1 commented Sep 14, 2016

This PR has got quite big. Would you be so kind and split it into two (ideally one commit per function), one for the optimization thing and another for the benchmarks?

I'm very much looking forward to getting this merged... we've neglected the PRs here for quite some time :)

@Fuco1 Fuco1 modified the milestone: 2.14.0 Nov 9, 2016
@basil-conto
Copy link
Collaborator

@doublep Are you still interested in rebasing this work on top of latest master?

@basil-conto basil-conto added the enhancement Suggestion to improve or extend existing behavior label Feb 15, 2021
@basil-conto basil-conto removed this from the 2.16.0 milestone Feb 15, 2021
@basil-conto
Copy link
Collaborator

@doublep Are you still interested in rebasing this work on top of latest master?

Ping.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Suggestion to improve or extend existing behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants