@@ -375,6 +375,7 @@ Relint scans elisp files for mistakes in regexps, including deprecated syntax an
375
375
- [[#collecting-items-into-a-list][Collecting items into a list]]
376
376
- [[#diffing-two-lists][Diffing two lists]]
377
377
- [[#filtering-a-list][Filtering a list]]
378
+ - [[#looking-up-associations][Looking up associations]]
378
379
:END:
379
380
380
381
**** Collecting items into a list :lists:
@@ -534,6 +535,115 @@ Using ~-select~ from =dash.el= seems to be the fastest way:
534
535
| cl-remove-if-not | 1.18 | 0.02459478 | 0 | 0.0 |
535
536
| (-non-nil (--map (when ... | slowest | 0.02903999 | 0 | 0.0 |
536
537
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
+
537
647
*** Examples :examples:
538
648
539
649
**** Alists :alists:
0 commit comments