open hash tables with linear probing#20
Conversation
This benchmark comes from backtracking#19
This commit reimplements the weak hash tables of `ocaml-hashcons` using open-adressing hash tables with linear probing. The implementation is loosely inspired from François Pottier's `hachis` library ( https://github.com/fpottier/hachis/ ), from which the suggestion comes that open-addressing hash tables can be competitive with the array-of-buckets design commonly found in OCaml libraries. One interesting aspect of open-addressing hash tables is that we can use a single large weak array, instead of many smaller weak arrays of buckets. (This is joint work with @iuliadmtru, we worked on this during the [Rocq'n'share 2026](https://proofs.swiss/rocq-n-share/2026/) in Lausanne.) The 'hashcons' library comes with three benchmarks representing three very different workloads: 1. quicksort: 'test_qs.exe', which reduces a large lambda-calculus term implementing a quicksort, has a hit rate of 99.8%, that is, virtually all 'hashcons' call succeed by finding a value already in the hash set. Terms get collected very slowly. The new implementation struggles on this workload: on my machine it is around 1.1x slower than the previous implementation, with similar memory usage. (We had to implement specific logic to avoid a 2x slowdown on this benchmark.) 2. pigeons: 'bench_bdd.exe -pigeon 11' is more balanced, with a hit rate of 44.6%, but terms that are largely long-lived -- the number of distinct live elements grows over time so the table needs to be resized repeatedly. The new implementation is 1.21x faster on this benchmark, with similar memory usage. 3. indices; 'bench_bdd.exe -de-bruijn 400' has a hit rate of 1.2%: most terms in the table are new, and in fact most terms are relatively short-lived and get collected quickly. The new implementation uses 7x less memory and is 2.85x faster on this benchmark. These results suggest that the new implementation could be an improvement over the previous one for certain workloads. We wrote this with the intention of adopting the same approach in the hash-consing layer of Rocq (moving from an array-of-buckets design to an open-addressing design); but at this point we don't know to which of the three workloads Rocq is closer to. (We suspect that it is in-betwen (1) and (2), so it is really unclear what the gains would be for Rocq.) Co-authored-by: Iulia Dumitru <iuliadmtru@gmail.com>
|
cc @sim642 who may be interested in experiments with hash-consing implementations |
|
In typical @fpottier fashion the implementation comes with a Verbose output for the quicksort benchmark (excerpt)There are regular "compression" events to remove from the (weak) table values that have been deleted by the GC. Each compression does not reduce the occupation by a lot, but doing them regularly is key to having good performance on this benchmark. If we remove the logic in the PR to do regular compressions even in absence of the need to resize, we see the following instead: The compression kicks in when 13441 slots are used (or occupied by dead values), only three times, but this results in much longer probe travel on each access to the table, and the benchmark takes 1.7s rather than 1.3s. (The periodic compressions discussed above mostly do not fire in the other benchmarks discussed below, because their tables grow fast enough for the resizing logic to kick in.) PigeonsA "resize" events doubles the size of the backing array, and also removes dead values (so occupation also decreases temporarily). We can see that the table steadily grows with this test. IndicesThe number of live values in the table grows over time, but somewhat slowly, and the GC can collect existing values at almost the same speed. Whenever we would need to resize we first check if a compression suffices to free half the table, and here most resizing events are replaced by compressions. |
This commit, joint work with @iuliadmtru, changes the implementation of weak hashtables in clib/hashset.ml, from the array-of-buckets design inherited from the OCaml standard library, to open-adressing hash tables with linear probing. (Open-adressing requires storing the hashes of the values with the hashtable itself, so it is a good match for the implementation of hashset which already does this.) The implementation here is adapted from the implementation we proposed for the ocaml-hashcons librar, see backtracking/ocaml-hashcons#20 In the ocaml-hashcons PR I discuss the performance impact on ocaml-hashcons benchmarks, which can be roughly summarized as follows: 1. The new code is slower than before, by 10%-15%, on a benchmark with an extremely high hit rate (virtually all values hashconsed are already in the table), where terms are relatively long-lived. 2. The new code is faster than before by 20% on a benchmark with a 50/50 split between hits and misses, whose keys are long-lived (the GC collects them very slowly). 3. The new code is much faster than before (2.85x faster) on a benchmark with a low hit rate (1.2%: most values hashconsed were _not_ in the table) where most values are relatively short-lived. My impression from a distance is that the Rocq workload is closer to (2) than (1) or (3), I would expect that the hit rate is not so high, and that the hash-consed values (terms sent to the kernel at Qed time) are in general very long-lived. Co-authored-by: Iulia Dumitru <iuliadmtru@gmail.com>
This commit, joint work with @iuliadmtru, changes the implementation of weak hashtables in clib/hashset.ml, from the array-of-buckets design inherited from the OCaml standard library, to open-adressing hash tables with linear probing. (Open-adressing requires storing the hashes of the values with the hashtable itself, so it is a good match for the implementation of hashset which already does this.) The implementation here is adapted from the implementation we proposed for the ocaml-hashcons librar, see backtracking/ocaml-hashcons#20 In the ocaml-hashcons PR I discuss the performance impact on ocaml-hashcons benchmarks, which can be roughly summarized as follows: 1. The new code is slower than before, by 10%-15%, on a benchmark with an extremely high hit rate (virtually all values hashconsed are already in the table), where terms are relatively long-lived. 2. The new code is faster than before by 20% on a benchmark with a 50/50 split between hits and misses, whose keys are long-lived (the GC collects them very slowly). 3. The new code is much faster than before (2.85x faster) on a benchmark with a low hit rate (1.2%: most values hashconsed were _not_ in the table) where most values are relatively short-lived. My impression from a distance is that the Rocq workload is closer to (2) than (1) or (3), I would expect that the hit rate is not so high, and that the hash-consed values (terms sent to the kernel at Qed time) are in general very long-lived. Co-authored-by: Iulia Dumitru <iuliadmtru@gmail.com>
…ctions The need to do this was found the hard way when testing Rocq with these changes, a benchmark of universe levels became 3x slower due to a densely populated interval with thousands of filled spots without any gaps. Co-authored-by: Iulia Dumitru <iuliadmtru@gmail.com> Co-authored-by: Andrei Duma <andrei@andreiduma.ro>
This commit reimplements the weak hash tables of
ocaml-hashconsusing open-adressing hash tables with linear probing. The implementation is loosely inspired from François Pottier'shachislibrary ( https://github.com/fpottier/hachis/ ), from which the suggestion comes that open-addressing hash tables can be competitive with the array-of-buckets design commonly found in OCaml libraries.One interesting aspect of open-addressing hash tables is that we can use a single large weak array, instead of many smaller weak arrays of buckets. (At this point I don't know whether this difference should make things faster or slower.)
(This is joint work with @iuliadmtru, we worked on this during the Rocq'n'share 2026 in Lausanne.)
The 'hashcons' library comes with three benchmarks representing three very different workloads:
quicksort: 'test_qs.exe', which reduces a large lambda-calculus term implementing a quicksort, has a hit rate of 99.8%, that is, virtually all 'hashcons' call succeed by finding a value already in the hash set. Terms get collected very slowly.
The new implementation struggles on this workload: on my machine it is around 1.1x slower than the previous implementation, with similar memory usage. (We had to implement specific logic to avoid a 2x slowdown on this benchmark.)
pigeons: 'bench_bdd.exe -pigeon 11' is more balanced, with a hit rate of 44.6%, but terms that are largely long-lived -- the number of distinct live elements grows over time so the table needs to be resized repeatedly.
The new implementation is 1.21x faster on this benchmark, with similar memory usage.
indices; 'bench_bdd.exe -de-bruijn 400' has a hit rate of 1.2%: most terms in the table are new, and in fact most terms are relatively short-lived and get collected quickly.
The new implementation uses 7x less memory and is 2.85x faster on this benchmark.
These results suggest that the new implementation could be an improvement over the previous one for certain workloads.
We wrote this with the intention of adopting the same approach in the hash-consing layer of Rocq (moving from an array-of-buckets design to an open-addressing design); but at this point we don't know to which of the three workloads Rocq is closer to. (We suspect that it is in-betwen (1) and (2), so it is really unclear what the gains would be for Rocq.)