Skip to content

Commit 223c477

Browse files
committed
Add: Looking up associations
1 parent 341d60a commit 223c477

File tree

2 files changed

+1529
-1142
lines changed

2 files changed

+1529
-1142
lines changed

README.org

+110
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,115 @@ 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 pairs"
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)))
550+
(symbols-alist (cl-loop for string in strings
551+
collect (cons (intern string) string)))
552+
(strings-plist (map-into strings-alist 'plist))
553+
(symbols-plist (map-into symbols-alist 'plist))
554+
(strings-ht (map-into strings-alist '(hash-table :test equal)))
555+
(symbols-ht-equal (map-into symbols-alist '(hash-table :test equal)))
556+
(symbols-ht-eq (map-into symbols-alist '(hash-table :test eq)))))
557+
("with 52 pairs"
558+
((char-range (cons ?A ?z))
559+
(strings (cl-loop for char from (car char-range) to (cdr char-range)
560+
collect (concat "prefix-" (char-to-string char))))
561+
(strings-alist (cl-loop for string in strings
562+
collect (cons string string)))
563+
(symbols-alist (cl-loop for string in strings
564+
collect (cons (intern string) string)))
565+
(strings-plist (map-into strings-alist 'plist))
566+
(symbols-plist (map-into symbols-alist 'plist))
567+
(strings-ht (map-into strings-alist '(hash-table :test equal)))
568+
(symbols-ht-equal (map-into symbols-alist '(hash-table :test equal)))
569+
(symbols-ht-eq (map-into symbols-alist '(hash-table :test eq))))))
570+
:forms (("strings/alist-get/string=" (sort (cl-loop for string in strings
571+
collect (alist-get string strings-alist nil nil #'string=))
572+
#'string<))
573+
("strings/plist" (sort (cl-loop for string in strings
574+
collect (plist-get strings-plist string))
575+
#'string<))
576+
("symbols/concat/intern/plist" (sort (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+
collect (plist-get symbols-plist symbol))
580+
#'string<))
581+
("strings/alist-get/equal" (sort (cl-loop for string in strings
582+
collect (alist-get string strings-alist nil nil #'equal))
583+
#'string<))
584+
("strings/hash-table/equal" (sort (cl-loop for string in strings
585+
collect (gethash string strings-ht))
586+
#'string<))
587+
("symbols/concat/intern/hash-table/equal" (sort (cl-loop for char from (car char-range) to (cdr char-range)
588+
for string = (concat "prefix-" (char-to-string char))
589+
for symbol = (intern string)
590+
collect (gethash symbol symbols-ht-equal))
591+
#'string<))
592+
("symbols/concat/intern/hash-table/eq" (sort (cl-loop for char from (car char-range) to (cdr char-range)
593+
for string = (concat "prefix-" (char-to-string char))
594+
for symbol = (intern string)
595+
collect (gethash symbol symbols-ht-eq))
596+
#'string<))
597+
("symbols/concat/intern/alist-get" (sort (cl-loop for char from (car char-range) to (cdr char-range)
598+
for string = (concat "prefix-" (char-to-string char))
599+
for symbol = (intern string)
600+
collect (alist-get symbol symbols-alist))
601+
#'string<))
602+
("symbols/concat/intern/alist-get/equal" (sort (cl-loop for char from (car char-range) to (cdr char-range)
603+
for string = (concat "prefix-" (char-to-string char))
604+
for symbol = (intern string)
605+
collect (alist-get symbol symbols-alist nil nil #'equal))
606+
#'string<))))
607+
#+END_SRC
608+
609+
#+RESULTS[041dd7c6644612027379e3558fcf60e61eb4896a]:
610+
| Form | x faster than next | Total runtime | # of GCs | Total GC runtime |
611+
|-------------------------------------------------------+--------------------+---------------+----------+------------------|
612+
| with 26 pairs: strings/hash-table/equal | 1.06 | 0.040321 | 0 | 0 |
613+
| with 26 pairs: strings/plist | 2.26 | 0.042848 | 0 | 0 |
614+
| with 52 pairs: strings/hash-table/equal | 1.27 | 0.096877 | 0 | 0 |
615+
| with 26 pairs: strings/alist-get/equal | 1.04 | 0.123039 | 0 | 0 |
616+
| with 26 pairs: strings/alist-get/string= | 1.03 | 0.128221 | 0 | 0 |
617+
| with 52 pairs: strings/plist | 2.62 | 0.131451 | 0 | 0 |
618+
| with 26 pairs: symbols/concat/intern/hash-table/eq | 1.00 | 0.344524 | 1 | 0.266744 |
619+
| with 26 pairs: symbols/concat/intern/hash-table/equal | 1.01 | 0.344951 | 1 | 0.267860 |
620+
| with 26 pairs: symbols/concat/intern/plist | 1.02 | 0.349360 | 1 | 0.266529 |
621+
| with 26 pairs: symbols/concat/intern/alist-get | 1.19 | 0.358071 | 1 | 0.267457 |
622+
| with 26 pairs: symbols/concat/intern/alist-get/equal | 1.11 | 0.424895 | 1 | 0.271568 |
623+
| with 52 pairs: strings/alist-get/equal | 1.03 | 0.471979 | 0 | 0 |
624+
| with 52 pairs: strings/alist-get/string= | 1.50 | 0.485663 | 0 | 0 |
625+
| with 52 pairs: symbols/concat/intern/hash-table/equal | 1.00 | 0.730628 | 2 | 0.547082 |
626+
| with 52 pairs: symbols/concat/intern/hash-table/eq | 1.05 | 0.733726 | 2 | 0.548910 |
627+
| with 52 pairs: symbols/concat/intern/alist-get | 1.00 | 0.773320 | 2 | 0.545707 |
628+
| with 52 pairs: symbols/concat/intern/plist | 1.36 | 0.774225 | 2 | 0.549963 |
629+
| with 52 pairs: symbols/concat/intern/alist-get/equal | slowest | 1.056641 | 2 | 0.545522 |
630+
631+
We see that hash-tables are generally the fastest solution.
632+
633+
Comparing alists and plists, we see that, when using string keys, plists are significantly faster than alists, even with 52 pairs. When using symbol keys, plists are faster with 26 pairs; with 52, plists and alists (using ~alist-get~ with ~eq~ as the test function) are nearly the same in performance.
634+
635+
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).
636+
637+
***** TODO Compare looking up interned symbols in obarray instead of hash table
638+
:PROPERTIES:
639+
:TOC: :ignore (this)
640+
:END:
641+
642+
***** TODO Compare a larger number of pairs
643+
:PROPERTIES:
644+
:TOC: :ignore (this)
645+
:END:
646+
537647
*** Examples :examples:
538648

539649
**** Alists :alists:

0 commit comments

Comments
 (0)