Skip to content

Commit

Permalink
Improve map and map-ref.
Browse files Browse the repository at this point in the history
- Check as we go instead of using `seq-uniq` immediately, which tests have shown
  to always faster for the tested 10, 100, and 1000 entries.

- Allow `unique` to be evaluated at run time.  Optimize when we know
  when it is `nil` or `non-nil` at compile time.
  • Loading branch information
okamsn committed Sep 11, 2024
1 parent 264a382 commit 0f007e9
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 30 deletions.
11 changes: 6 additions & 5 deletions doc/loopy-doc.org
Original file line number Diff line number Diff line change
Expand Up @@ -1978,9 +1978,10 @@ source sequences.
order in which the key-value pairs are found. There is no guarantee that they
be in the same order each time.

These pairs are created before the loop begins. In other words, the map
=EXPR= is not processed progressively, but all at once. Therefore, this
command can have a noticeable start-up cost when working with very large maps.
These pairs are created before the loop begins via ~map-pairs~. In other
words, the map =EXPR= is not processed progressively, but all at once.
Therefore, this command can have a noticeable start-up cost when working with
very large maps.

#+begin_src emacs-lisp
;; => ((a . 1) (b . 2))
Expand Down Expand Up @@ -2426,8 +2427,8 @@ the accessed index during the loop.
of other commands. This is not the same as the =key= keyword parameter of the
accumulation commands.

Like in the command =map=, the keys of the map are generated before the
loop is run, which can be expensive for large maps.
Like in the command =map=, the keys of the map are generated via the function
~map-keys~ before the loop is run, which can be expensive for large maps.

Similar to =map=, any duplicate keys are ignored by default. This can be
disabled by setting the =unique= keyword argument to nil, though note that
Expand Down
88 changes: 67 additions & 21 deletions loopy-commands.el
Original file line number Diff line number Diff line change
Expand Up @@ -897,50 +897,96 @@ BY is the function to use to move through the list (default `cdr')."
(setq ,list-val (funcall ,list-func ,list-val)))))))

;;;;;; Map
;; TODO: Instead of using `seq-uniq' at the start,
;; check as we go.
(cl-defun loopy--parse-map-command ((name var val &key (unique t)))
"Parse the `map' loop command.
"Parse the `map' loop command as `(map VAR EXPR &key (unique t))'.
Iterates through an alist of (key . value) dotted pairs,
extracted from a hash-map, association list, property list, or
vector using the library `map.el'."
(when loopy--in-sub-level
(loopy--signal-bad-iter name 'map))
(let ((value-holder (gensym "map-")))
`((loopy--iteration-vars
(,value-holder ,(if unique
`(seq-uniq (map-pairs ,val) #'loopy--car-equal-car)
`(map-pairs ,val))))
,@(loopy--destructure-for-iteration-command var `(car ,value-holder))
;; NOTE: The benchmarks show that `consp' is faster than no `consp',
;; Not sure if this is needed, but want to be careful since we could have been
;; incorrectly using pre-macro-expansion forms in the comparison.
(setq unique (macroexpand-all unique macroexpand-all-environment))
(loopy--instr-let-var* ((value-holder `(map-pairs ,val)))
loopy--iteration-vars
`(;; NOTE: The benchmarks show that `consp' is faster than no `consp',
;; at least for some commands.
(loopy--pre-conditions (consp ,value-holder))
(loopy--latter-body (setq ,value-holder (cdr ,value-holder))))))
(loopy--latter-body (setq ,value-holder (cdr ,value-holder)))
,@(cond
((null unique)
(loopy--destructure-for-iteration-command var `(car ,value-holder)))
;; If UNIQUE is not evaluable code and is not `nil', then we know that
;; we can use `member' directly.
((and (macroexp-const-p unique)
unique)
(loopy--instr-let-var* ((key-list nil))
loopy--iteration-vars
(loopy--destructure-for-iteration-command
var `(progn
(while (member (caar ,value-holder) ,key-list)
(setq ,value-holder (cdr ,value-holder)))
(push (caar ,value-holder) ,key-list)
(car ,value-holder)))))
(t
(loopy--instr-let-var* ((key-list nil)
(test-fn `(if ,unique
#'member
#'ignore)))
loopy--iteration-vars
(loopy--destructure-for-iteration-command
var `(progn
(while (funcall ,test-fn (caar ,value-holder) ,key-list)
(setq ,value-holder (cdr ,value-holder)))
(push (caar ,value-holder) ,key-list)
(car ,value-holder)))))))))

;;;;;; Map-Ref
(cl-defun loopy--parse-map-ref-command ((name var val &key key (unique t)))
"Parse the `map-ref' command as (map-ref VAR VAL).
"Parse the `map-ref' command as (map-ref VAR VAL &key key (unique t)).
KEY is a variable name in which to store the current key.
Uses `map-elt' as a `setf'-able place, iterating through the
map's keys. Duplicate keys are ignored."
(when loopy--in-sub-level
(loopy--signal-bad-iter name 'map-ref))
(let ((key-list (gensym "map-ref-keys")))
`((loopy--iteration-vars (,key-list ,(if unique
`(seq-uniq (map-keys ,val))
`(map-keys ,val))))
;; Not sure if this is needed, but want to be careful since we could have been
;; incorrectly using pre-macro-expansion forms in the comparison.
(setq unique (macroexpand-all unique macroexpand-all-environment))
(loopy--instr-let-var* ((key-list `(map-keys ,val)))
loopy--iteration-vars
`(;; NOTE: The benchmarks show that `consp' is faster than no `consp',
;; at least for some commands.
(loopy--pre-conditions (consp ,key-list))
(loopy--latter-body (setq ,key-list (cdr ,key-list)))
,@(cond
;; We don't need to do anything if we don't care about uniqueness.
((null unique) nil)
;; If UNIQUE is not evaluable code and is not `nil', then we know that
;; we can use `member' directly.
((and (macroexp-const-p unique)
unique)
(loopy--instr-let-var* ((found-keys nil))
loopy--iteration-vars
`((loopy--main-body (while (member (car ,key-list) ,found-keys)
(setq ,key-list (cdr ,key-list))))
(loopy--main-body (push (car ,key-list) ,found-keys)))))
(t
(loopy--instr-let-var* ((found-keys nil)
(test-fn `(if ,unique
#'member
#'ignore)))
loopy--iteration-vars
`((loopy--main-body (while (funcall ,test-fn (car ,key-list) ,found-keys)
(setq ,key-list (cdr ,key-list))))
(loopy--main-body (push (car ,key-list) ,found-keys))))))
,@(when key
`((loopy--iteration-vars (,key nil))
(loopy--main-body (setq ,key (car ,key-list)))))
,@(loopy--destructure-for-generalized-command
var `(map-elt ,val ,(or key `(car ,key-list))))
;; NOTE: The benchmarks show that `consp' is faster than no `consp',
;; at least for some commands.
(loopy--pre-conditions (consp ,key-list))
(loopy--latter-body (setq ,key-list (cdr ,key-list))))))
var `(map-elt ,val ,(or key `(car ,key-list)))))))

;;;;;; Numbers

Expand Down
34 changes: 30 additions & 4 deletions tests/tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,18 @@ Using numbers directly will use less variables and more efficient code."
:iter-bare ((map-pairs . mapping-pairs)
(collect . collecting)))

(loopy-deftest map-:unique-var
:doc "`:unique' it `t' by default. Test when `nil'."
:result '((a . 1) (a . 27) (b . 2) (c . 3))
:body ((with (cat nil))
(map-pairs pair '((a . 1) (a . 27) (b . 2) (c . 3)) :unique cat)
(collect coll pair)
(finally-return coll))
:loopy t
:iter-keyword (map-pairs collect)
:iter-bare ((map-pairs . mapping-pairs)
(collect . collecting)))

(loopy-deftest map-destructuring
:doc "Check that `map' implements destructuring, not destructuring itself."
:result '((a b) (1 2))
Expand Down Expand Up @@ -1803,10 +1815,24 @@ Using numbers directly will use less variables and more efficient code."
(loopy-deftest map-ref-:unique-nil
:doc "Fist `:a' becomes 15 because it gets found twice by `setf'."
:result '(:a 15 :a 2 :b 10)
:body (loopy (with (map (list :a 1 :a 2 :b 3)))
(map-ref i map :unique nil)
(do (cl-incf i 7))
(finally-return map))
:body ((with (map (list :a 1 :a 2 :b 3)))
(map-ref i map :unique nil)
(do (cl-incf i 7))
(finally-return map))
:loopy t
:iter-keyword (map-ref do collect)
:iter-bare ((map-ref . mapping-ref)
(do . ignore)
(collect . collecting)))

(loopy-deftest map-ref-:unique-var
:doc "Fist `:a' becomes 15 because it gets found twice by `setf'."
:result '(:a 15 :a 2 :b 10)
:body ((with (map (list :a 1 :a 2 :b 3))
(cat nil))
(map-ref i map :unique cat)
(do (cl-incf i 7))
(finally-return map))
:loopy t
:iter-keyword (map-ref do collect)
:iter-bare ((map-ref . mapping-ref)
Expand Down

0 comments on commit 0f007e9

Please sign in to comment.