fn-in library performance summary

Discussion
Details
Results

Version 4 to version 5

Version 5 introduces type dispatch with Clojure protocols, bringing improved performance compared to version 4's multimethod type dispatch. Across a wide span of collection types and collection sizes, all eight of the public functions are significantly faster in at least some cases, while none regress. Upgrading to version 5 therefore provides improved performance for general uses.

Details

To ensure that any implementation change gives general performance improvement and not merely for one special case, we use Fastester to test a broad span of arguments. That involves running Criterium benchmarks for various sizes of vectors, maps, sequences, and lists for each of fn-in's eight functions.

For example, to test the performance of associating a new value into a flat (i.e., un-nested) vector, we do a lookup in vec-of-n-rand-ints, whose ten-element entry looks like this.

(vec-of-n-rand-ints 10) ;; => [68 71 2 89 4 19 35 4 84 37]

This provides the benchmark with a pre-made, one-dimensional series of random integers. We pre-compute the collections so that the benchmark does not measure the time it takes to construct the argument collection.

In our example, a single benchmark run takes multiple measurements of the time to associate a new value to the final element. Like this.

(assoc* (vec-of-n-rand-ints 10) (dec 10) :benchmark-sentinel)
;; => [68 71 2 89 4 19 35 4 84 :benchmark-sentinel]

assoc* replaces the 37 at the end of the test vector with :benchmark-sentinel in about 151 nanoseconds.

The benchmarks for the 'plain' functions (i.e., get*, assoc*, update*, and dissoc*) cover hashmaps, lists, sequences, and vectors, each containing up to one million elements. In total, over fifty benchmarks per function.

The -in function variants are benchmarked against nested versions of four collection types. For example, to test updating a value buried in a nested vector, we do a lookup in nested-vec, whose third entry looks like this.

(nested-vec 3)
;; => [[[0 1 2]
;;      [3 4 5]
;;      [6 7 8]]
;;     [[9 10 11]
;;      [12 13 14]
;;      [15 16 17]]
;;     [[18 19 20]
;;      [21 22 23]
;;      [24 25 26]]]

This supplies the benchmark with a pre-made, triply-nested series of vectors containing a range of integers.

A single benchmark run takes multiple measurements of the time to increment the last element of the last nested vector. Like this.

(update-in* (nested-vec 3) [2 2 2] inc)
;; => [[[0 1 2]
;;      [3 4 5]
;;      [6 7 8]]
;;     [[9 10 11]
;;      [12 13 14]
;;      [15 16 17]]
;;     [[18 19 20]
;;      [21 22 23]
;;      [24 25 27]]]

update-in* dives into the nested collection and increments the final 26 to a 27 in about 576 nanoseconds.

For each of the -in function variants (i.e., get-in*, assoc-in*, update-in*, and dissoc-in*), we run over fifty benchmarks, involving deeply-nested (up to six levels) hashmaps, lists, sequences, and vectors, whose sizes span up to one-hundred thousand elements.

Benchmark results

get*
get-in*
assoc*
assoc-in*
update*
update-in*
dissoc*
dissoc-in*