Skip to content

Commit 4fff0a4

Browse files
committed
Add: Looking up associations
1 parent 341d60a commit 4fff0a4

File tree

2 files changed

+1455
-1142
lines changed

2 files changed

+1455
-1142
lines changed

README.org

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ Relint scans elisp files for mistakes in regexps, including deprecated syntax an
375375
- [[#collecting-items-into-a-list][Collecting items into a list]]
376376
- [[#diffing-two-lists][Diffing two lists]]
377377
- [[#filtering-a-list][Filtering a list]]
378+
- [[#looking-up-associations][Looking up associations]]
378379
:END:
379380

380381
**** Collecting items into a list :lists:
@@ -534,6 +535,92 @@ Using ~-select~ from =dash.el= seems to be the fastest way:
534535
| cl-remove-if-not | 1.18 | 0.02459478 | 0 | 0.0 |
535536
| (-non-nil (--map (when ... | slowest | 0.02903999 | 0 | 0.0 |
536537

538+
**** Looking up associations
539+
540+
There are a few options in Emacs Lisp for looking up values in associative data structures: association lists (alists), property lists (plists), and hash tables. Which one performs best in a situation may depend on several factors. This benchmark shows what may be a common case: looking up values using a string as the key. We compare several combinations, including the case of prepending a prefix to the string, interning it, and looking up the resulting symbol (which might be done, e.g. when looking up a function to call based on the value of a string).
541+
542+
#+BEGIN_SRC elisp :exports both :cache yes
543+
(bench-multi-lets :times 10000 :ensure-equal t
544+
:lets (("with 26 keys"
545+
((char-range (cons ?A ?Z))
546+
(strings (cl-loop for char from (car char-range) to (cdr char-range)
547+
collect (concat "prefix-" (char-to-string char))))
548+
(strings-alist (cl-loop for string in strings
549+
collect (cons string (string-to-char string))))
550+
(symbols-alist (cl-loop for string in strings
551+
collect (cons (intern string) (string-to-char string))))
552+
(strings-ht (map-into strings-alist '(hash-table :test equal)))
553+
(symbols-ht-equal (map-into symbols-alist '(hash-table :test equal)))
554+
(symbols-ht-eq (map-into symbols-alist '(hash-table :test eq)))))
555+
("with 52 keys"
556+
((char-range (cons ?A ?z))
557+
(strings (cl-loop for char from (car char-range) to (cdr char-range)
558+
collect (concat "prefix-" (char-to-string char))))
559+
(strings-alist (cl-loop for string in strings
560+
collect (cons string (string-to-char string))))
561+
(symbols-alist (cl-loop for string in strings
562+
collect (cons (intern string) (string-to-char string))))
563+
(strings-ht (map-into strings-alist '(hash-table :test equal)))
564+
(symbols-ht-equal (map-into symbols-alist '(hash-table :test equal)))
565+
(symbols-ht-eq (map-into symbols-alist '(hash-table :test eq))))))
566+
:forms (("strings/alist-get/string=" (cl-loop for string in strings
567+
do (alist-get string strings-alist nil nil #'string=)))
568+
("strings/alist-get/equal" (cl-loop for string in strings
569+
do (alist-get string strings-alist nil nil #'equal)))
570+
("strings/hash-table/equal" (cl-loop for string in strings
571+
do (gethash string strings-ht)))
572+
("symbols/concat/intern/hash-table/equal" (cl-loop for char from (car char-range) to (cdr char-range)
573+
for string = (concat "prefix-" (char-to-string char))
574+
for symbol = (intern string)
575+
do (gethash symbol symbols-ht-equal)))
576+
("symbols/concat/intern/hash-table/eq" (cl-loop for char from (car char-range) to (cdr char-range)
577+
for string = (concat "prefix-" (char-to-string char))
578+
for symbol = (intern string)
579+
do (gethash symbol symbols-ht-eq)))
580+
("symbols/concat/intern/alist-get" (cl-loop for char from (car char-range) to (cdr char-range)
581+
for string = (concat "prefix-" (char-to-string char))
582+
for symbol = (intern string)
583+
do (alist-get symbol strings-alist)))
584+
("symbols/concat/intern/alist-get/equal" (cl-loop for char from (car char-range) to (cdr char-range)
585+
for string = (concat "prefix-" (char-to-string char))
586+
for symbol = (intern string)
587+
do (alist-get symbol strings-alist nil nil #'equal)))))
588+
#+END_SRC
589+
590+
#+RESULTS[ce54186c1427ed462049d4f2ae0ae141a5a4bb6c]:
591+
| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
592+
|------------------------------------------------------+--------------------+---------------+----------+------------------|
593+
| with 26 keys: strings/hash-table/equal | 2.30 | 0.013158 | 0 | 0 |
594+
| with 52 keys: strings/hash-table/equal | 3.14 | 0.030205 | 0 | 0 |
595+
| with 26 keys: strings/alist-get/equal | 1.03 | 0.094957 | 0 | 0 |
596+
| with 26 keys: strings/alist-get/string= | 3.23 | 0.097786 | 0 | 0 |
597+
| with 26 keys: symbols/concat/intern/hash-table/equal | 1.00 | 0.316200 | 1 | 0.266073 |
598+
| with 26 keys: symbols/concat/intern/hash-table/eq | 1.04 | 0.317554 | 1 | 0.267119 |
599+
| with 26 keys: symbols/concat/intern/alist-get | 1.19 | 0.330465 | 1 | 0.265569 |
600+
| with 52 keys: strings/alist-get/equal | 1.03 | 0.393080 | 0 | 0 |
601+
| with 52 keys: strings/alist-get/string= | 1.11 | 0.404900 | 0 | 0 |
602+
| with 26 keys: symbols/concat/intern/alist-get/equal | 1.45 | 0.449617 | 1 | 0.267943 |
603+
| with 52 keys: symbols/concat/intern/hash-table/equal | 1.03 | 0.650723 | 2 | 0.534464 |
604+
| with 52 keys: symbols/concat/intern/hash-table/eq | 1.07 | 0.667369 | 2 | 0.551349 |
605+
| with 52 keys: symbols/concat/intern/alist-get | 1.73 | 0.714040 | 2 | 0.533654 |
606+
| with 52 keys: symbols/concat/intern/alist-get/equal | slowest | 1.233442 | 2 | 0.535236 |
607+
608+
We see that hash-tables are generally the fastest solution.
609+
610+
When using an alist with 26 keys, it's faster to concat and intern a string and then look up the resulting symbol in the alist with ~eq~ as the test function (the =symbols/concat/intern/alist-get= test) than to lookup the string directly with ~equal~ (the =strings/alist-get/equal= test).
611+
612+
Also, perhaps surprisingly, when looking up a string in an alist, using ~equal~ as the test function may be faster than using the type-specific ~string=~ function (possibly indicating an optimization to be made in Emacs's C code).
613+
614+
***** TODO Compare looking up interned symbols in obarray instead of hash table
615+
:PROPERTIES:
616+
:TOC: :ignore (this)
617+
:END:
618+
619+
***** TODO Compare plists
620+
:PROPERTIES:
621+
:TOC: :ignore (this)
622+
:END:
623+
537624
*** Examples :examples:
538625

539626
**** Alists :alists:

0 commit comments

Comments
 (0)