Skip to content

Commit 64a95bc

Browse files
authored
Improve map and map-ref. (#209)
- 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. - Clarify Org docs. See also issue #179. Closes #179.
1 parent a374774 commit 64a95bc

File tree

6 files changed

+159
-39
lines changed

6 files changed

+159
-39
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,24 @@ This document describes the user-facing changes to Loopy.
4242

4343
- Make `sequence-index` the default name and `seq-index` an alias ([#126, #206]).
4444

45+
- Allow the `unique` keyword argument of the commands `map` and `map-ref` to be
46+
evaluable at run time, instead of just checked at compile time ([#209]).
47+
48+
### Improvements
49+
50+
- The `map` and `map-ref` commands now check for duplicate keys step by step,
51+
instead of all at once at the start of the loop ([#209], [#179]). Testing
52+
showed that this is consistently faster than the old method.
4553

4654
[#126]: https://github.com/okamsn/loopy/issues/126
4755
[#168]: https://github.com/okamsn/loopy/issues/168
4856
[#169]: https://github.com/okamsn/loopy/issues/169
57+
[#179]: https://github.com/okamsn/loopy/issues/179
4958
[#203]: https://github.com/okamsn/loopy/pull/203
5059
[#205]: https://github.com/okamsn/loopy/pull/205
5160
[#206]: https://github.com/okamsn/loopy/pull/206
5261
[#207]: https://github.com/okamsn/loopy/pull/207
62+
[#209]: https://github.com/okamsn/loopy/pull/209
5363

5464

5565
## 0.13.0

README.org

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ please let me know.
3939
list of built-in aliases in the future. They can still be added to the
4040
list of known aliases using ~loopy-defalias~. See the changelog for more
4141
information.
42+
- The =:unique= keyword argument of the =map= and =map-ref= commands can now
43+
be evaluable at run time, similar to most other keyword arguments.
4244
- Version 0.13.0:
4345
- The deprecated =:init= keyword argument has been removed. Use the =with=
4446
special macro argument instead.

doc/loopy-doc.org

+6-5
Original file line numberDiff line numberDiff line change
@@ -1978,9 +1978,10 @@ source sequences.
19781978
order in which the key-value pairs are found. There is no guarantee that they
19791979
be in the same order each time.
19801980

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

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

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

24322433
Similar to =map=, any duplicate keys are ignored by default. This can be
24332434
disabled by setting the =unique= keyword argument to nil, though note that

doc/loopy.texi

+10-9
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ You should keep in mind that commands are evaluated in order. This means that
705705
attempting something like the below example might not do what you expect, as @samp{i}
706706
is assigned a value from the list after collecting @samp{i} into @samp{coll}.
707707

708-
@float Listing,org5119497
708+
@float Listing,orgfa9540f
709709
@lisp
710710
;; => (nil 1 2)
711711
(loopy (collect coll i)
@@ -887,7 +887,7 @@ the flag @samp{dash} provided by the package @samp{loopy-dash}.
887887

888888
Below are two examples of destructuring in @code{cl-loop} and @code{loopy}.
889889

890-
@float Listing,orgf3a9f7c
890+
@float Listing,orgb706e77
891891
@lisp
892892
;; => (1 2 3 4)
893893
(cl-loop for (i . j) in '((1 . 2) (3 . 4))
@@ -902,7 +902,7 @@ Below are two examples of destructuring in @code{cl-loop} and @code{loopy}.
902902
@caption{Destructuring values in a list.}
903903
@end float
904904

905-
@float Listing,org1bf5dc0
905+
@float Listing,orgcc60e47
906906
@lisp
907907
;; => (1 2 3 4)
908908
(cl-loop for elem in '((1 . 2) (3 . 4))
@@ -2150,9 +2150,10 @@ In general, as a map in not necessarily a sequence, you should not rely on the
21502150
order in which the key-value pairs are found. There is no guarantee that they
21512151
be in the same order each time.
21522152

2153-
These pairs are created before the loop begins. In other words, the map
2154-
@samp{EXPR} is not processed progressively, but all at once. Therefore, this
2155-
command can have a noticeable start-up cost when working with very large maps.
2153+
These pairs are created before the loop begins via @code{map-pairs}. In other
2154+
words, the map @samp{EXPR} is not processed progressively, but all at once.
2155+
Therefore, this command can have a noticeable start-up cost when working with
2156+
very large maps.
21562157

21572158
@lisp
21582159
;; => ((a . 1) (b . 2))
@@ -2629,8 +2630,8 @@ place referred to by @samp{VAR}. This is similar to the @samp{index} keyword pa
26292630
of other commands. This is not the same as the @samp{key} keyword parameter of the
26302631
accumulation commands.
26312632

2632-
Like in the command @samp{map}, the keys of the map are generated before the
2633-
loop is run, which can be expensive for large maps.
2633+
Like in the command @samp{map}, the keys of the map are generated via the function
2634+
@code{map-keys} before the loop is run, which can be expensive for large maps.
26342635

26352636
Similar to @samp{map}, any duplicate keys are ignored by default. This can be
26362637
disabled by setting the @samp{unique} keyword argument to nil, though note that
@@ -4653,7 +4654,7 @@ using the @code{let*} special form.
46534654
This method recognizes all commands and their aliases in the user option
46544655
@code{loopy-aliases}.
46554656

4656-
@float Listing,orgb546acb
4657+
@float Listing,orgab882ee
46574658
@lisp
46584659
;; => ((1 2 3) (-3 -2 -1) (0))
46594660
(loopy-iter (arg accum-opt positives negatives other)

loopy-commands.el

+57-21
Original file line numberDiff line numberDiff line change
@@ -897,50 +897,86 @@ BY is the function to use to move through the list (default `cdr')."
897897
(setq ,list-val (funcall ,list-func ,list-val)))))))
898898

899899
;;;;;; Map
900-
;; TODO: Instead of using `seq-uniq' at the start,
901-
;; check as we go.
902900
(cl-defun loopy--parse-map-command ((name var val &key (unique t)))
903-
"Parse the `map' loop command.
901+
"Parse the `map' loop command as `(map VAR EXPR &key (unique t))'.
904902
905903
Iterates through an alist of (key . value) dotted pairs,
906904
extracted from a hash-map, association list, property list, or
907905
vector using the library `map.el'."
908906
(when loopy--in-sub-level
909907
(loopy--signal-bad-iter name 'map))
910-
(let ((value-holder (gensym "map-")))
911-
`((loopy--iteration-vars
912-
(,value-holder ,(if unique
913-
`(seq-uniq (map-pairs ,val) #'loopy--car-equal-car)
914-
`(map-pairs ,val))))
915-
,@(loopy--destructure-for-iteration-command var `(car ,value-holder))
916-
;; NOTE: The benchmarks show that `consp' is faster than no `consp',
908+
(loopy--instr-let-var* ((value-holder `(map-pairs ,val)))
909+
loopy--iteration-vars
910+
`(;; NOTE: The benchmarks show that `consp' is faster than no `consp',
917911
;; at least for some commands.
918912
(loopy--pre-conditions (consp ,value-holder))
919-
(loopy--latter-body (setq ,value-holder (cdr ,value-holder))))))
913+
(loopy--latter-body (setq ,value-holder (cdr ,value-holder)))
914+
,@(pcase unique
915+
('nil
916+
(loopy--destructure-for-iteration-command var `(car ,value-holder)))
917+
('t
918+
(loopy--instr-let-var* ((key-list nil))
919+
loopy--iteration-vars
920+
(loopy--destructure-for-iteration-command
921+
var `(progn
922+
(while (member (caar ,value-holder) ,key-list)
923+
(setq ,value-holder (cdr ,value-holder)))
924+
(push (caar ,value-holder) ,key-list)
925+
(car ,value-holder)))))
926+
(_
927+
(loopy--instr-let-var* ((key-list nil)
928+
(test-fn `(if ,unique
929+
#'member
930+
#'ignore)))
931+
loopy--iteration-vars
932+
(loopy--destructure-for-iteration-command
933+
var `(progn
934+
(while (funcall ,test-fn (caar ,value-holder) ,key-list)
935+
(setq ,value-holder (cdr ,value-holder)))
936+
(push (caar ,value-holder) ,key-list)
937+
(car ,value-holder)))))))))
920938

921939
;;;;;; Map-Ref
922940
(cl-defun loopy--parse-map-ref-command ((name var val &key key (unique t)))
923-
"Parse the `map-ref' command as (map-ref VAR VAL).
941+
"Parse the `map-ref' command as (map-ref VAR VAL &key key (unique t)).
924942
925943
KEY is a variable name in which to store the current key.
926944
927945
Uses `map-elt' as a `setf'-able place, iterating through the
928946
map's keys. Duplicate keys are ignored."
929947
(when loopy--in-sub-level
930948
(loopy--signal-bad-iter name 'map-ref))
931-
(let ((key-list (gensym "map-ref-keys")))
932-
`((loopy--iteration-vars (,key-list ,(if unique
933-
`(seq-uniq (map-keys ,val))
934-
`(map-keys ,val))))
949+
(loopy--instr-let-var* ((key-list `(map-keys ,val)))
950+
loopy--iteration-vars
951+
`(;; NOTE: The benchmarks show that `consp' is faster than no `consp',
952+
;; at least for some commands.
953+
(loopy--pre-conditions (consp ,key-list))
954+
(loopy--latter-body (setq ,key-list (cdr ,key-list)))
955+
,@(pcase unique
956+
;; We don't need to do anything if we don't care about uniqueness.
957+
('nil nil)
958+
;; If UNIQUE is not evaluable code and is not `nil', then we know that
959+
;; we can use `member' directly.
960+
('t
961+
(loopy--instr-let-var* ((found-keys nil))
962+
loopy--iteration-vars
963+
`((loopy--main-body (while (member (car ,key-list) ,found-keys)
964+
(setq ,key-list (cdr ,key-list))))
965+
(loopy--main-body (push (car ,key-list) ,found-keys)))))
966+
(_
967+
(loopy--instr-let-var* ((found-keys nil)
968+
(test-fn `(if ,unique
969+
#'member
970+
#'ignore)))
971+
loopy--iteration-vars
972+
`((loopy--main-body (while (funcall ,test-fn (car ,key-list) ,found-keys)
973+
(setq ,key-list (cdr ,key-list))))
974+
(loopy--main-body (push (car ,key-list) ,found-keys))))))
935975
,@(when key
936976
`((loopy--iteration-vars (,key nil))
937977
(loopy--main-body (setq ,key (car ,key-list)))))
938978
,@(loopy--destructure-for-generalized-command
939-
var `(map-elt ,val ,(or key `(car ,key-list))))
940-
;; NOTE: The benchmarks show that `consp' is faster than no `consp',
941-
;; at least for some commands.
942-
(loopy--pre-conditions (consp ,key-list))
943-
(loopy--latter-body (setq ,key-list (cdr ,key-list))))))
979+
var `(map-elt ,val ,(or key `(car ,key-list)))))))
944980

945981
;;;;;; Numbers
946982

tests/tests.el

+74-4
Original file line numberDiff line numberDiff line change
@@ -1717,7 +1717,7 @@ Using numbers directly will use less variables and more efficient code."
17171717
:iter-bare ((_map . (mapping mapping-pairs))
17181718
(collect . collecting)))
17191719

1720-
(loopy-deftest map-:unique-t
1720+
(loopy-deftest map-:unique-t-1
17211721
:doc "`:unique' it `t' by default."
17221722
:result '((a . 1) (b . 2) (c . 3))
17231723
:multi-body t
@@ -1732,7 +1732,18 @@ Using numbers directly will use less variables and more efficient code."
17321732
:iter-bare ((map-pairs . mapping-pairs)
17331733
(collect . collecting)))
17341734

1735-
(loopy-deftest map-:unique-nil
1735+
(loopy-deftest map-:unique-t-2
1736+
:doc "Check that optimization to avoid `funcall' works."
1737+
:result t
1738+
:wrap ((x . `(not (string-match-p
1739+
"funcall"
1740+
(format "%S" (macroexpand-all (quote ,x) nil))))))
1741+
:body ((map-pairs i map :unique t))
1742+
:loopy t
1743+
:iter-keyword (map-pairs)
1744+
:iter-bare ((map-pairs . mapping-pairs)))
1745+
1746+
(loopy-deftest map-:unique-nil-1
17361747
:doc "`:unique' it `t' by default. Test when `nil'."
17371748
:result '((a . 1) (a . 27) (b . 2) (c . 3))
17381749
:body ((map-pairs pair '((a . 1) (a . 27) (b . 2) (c . 3)) :unique nil)
@@ -1743,6 +1754,29 @@ Using numbers directly will use less variables and more efficient code."
17431754
:iter-bare ((map-pairs . mapping-pairs)
17441755
(collect . collecting)))
17451756

1757+
(loopy-deftest map-:unique-nil-2
1758+
:doc "Check that optimization to avoid `funcall' works."
1759+
:result t
1760+
:wrap ((x . `(not (string-match-p
1761+
"funcall"
1762+
(format "%S" (macroexpand-all (quote ,x) nil))))))
1763+
:body ((map-pairs i map :unique nil))
1764+
:loopy t
1765+
:iter-keyword (map-pairs)
1766+
:iter-bare ((map-pairs . mapping-pairs)))
1767+
1768+
(loopy-deftest map-:unique-var
1769+
:doc "`:unique' it `t' by default. Test when `nil'."
1770+
:result '((a . 1) (a . 27) (b . 2) (c . 3))
1771+
:body ((with (cat nil))
1772+
(map-pairs pair '((a . 1) (a . 27) (b . 2) (c . 3)) :unique cat)
1773+
(collect coll pair)
1774+
(finally-return coll))
1775+
:loopy t
1776+
:iter-keyword (map-pairs collect)
1777+
:iter-bare ((map-pairs . mapping-pairs)
1778+
(collect . collecting)))
1779+
17461780
(loopy-deftest map-destructuring
17471781
:doc "Check that `map' implements destructuring, not destructuring itself."
17481782
:result '((a b) (1 2))
@@ -1782,7 +1816,7 @@ Using numbers directly will use less variables and more efficient code."
17821816
(do . ignore)
17831817
(collect . collecting)))
17841818

1785-
(loopy-deftest map-ref-:unique-t
1819+
(loopy-deftest map-ref-:unique-t-1
17861820
:doc "`:unique' is `t' by default."
17871821
:result '(:a 8 :a 2 :b 10)
17881822
:multi-body t
@@ -1800,7 +1834,18 @@ Using numbers directly will use less variables and more efficient code."
18001834
(do . ignore)
18011835
(collect . collecting)))
18021836

1803-
(loopy-deftest map-ref-:unique-nil
1837+
(loopy-deftest map-ref-:unique-t-2
1838+
:doc "Check that optimization to avoid `funcall' works."
1839+
:result t
1840+
:wrap ((x . `(not (string-match-p
1841+
"funcall"
1842+
(format "%S" (macroexpand-all (quote ,x) nil))))))
1843+
:body ((map-ref i map :unique t))
1844+
:loopy t
1845+
:iter-keyword (map-ref)
1846+
:iter-bare ((map-ref . mapping-ref)))
1847+
1848+
(loopy-deftest map-ref-:unique-nil-1
18041849
:doc "Fist `:a' becomes 15 because it gets found twice by `setf'."
18051850
:result '(:a 15 :a 2 :b 10)
18061851
:body ((with (map (list :a 1 :a 2 :b 3)))
@@ -1813,6 +1858,31 @@ Using numbers directly will use less variables and more efficient code."
18131858
(do . ignore)
18141859
(collect . collecting)))
18151860

1861+
(loopy-deftest map-ref-:unique-nil-2
1862+
:doc "Check that optimization to avoid `funcall' works."
1863+
:result t
1864+
:wrap ((x . `(not (string-match-p
1865+
"funcall"
1866+
(format "%S" (macroexpand-all (quote ,x) nil))))))
1867+
:body ((map-ref i map :unique nil))
1868+
:loopy t
1869+
:iter-keyword (map-ref)
1870+
:iter-bare ((map-ref . mapping-ref)))
1871+
1872+
(loopy-deftest map-ref-:unique-var
1873+
:doc "Fist `:a' becomes 15 because it gets found twice by `setf'."
1874+
:result '(:a 15 :a 2 :b 10)
1875+
:body ((with (map (list :a 1 :a 2 :b 3))
1876+
(cat nil))
1877+
(map-ref i map :unique cat)
1878+
(do (cl-incf i 7))
1879+
(finally-return map))
1880+
:loopy t
1881+
:iter-keyword (map-ref do collect)
1882+
:iter-bare ((map-ref . mapping-ref)
1883+
(do . ignore)
1884+
(collect . collecting)))
1885+
18161886
(loopy-deftest map-ref-destr
18171887
:doc "Check that `map-ref' implements destructuring, not the destructuring itself."
18181888
:result [[7 8] [7 8]]

0 commit comments

Comments
 (0)