fastconstmap 0.8.0__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/MANIFEST.in +1 -1
  2. fastconstmap-0.10.0/PKG-INFO +441 -0
  3. fastconstmap-0.10.0/README.md +423 -0
  4. fastconstmap-0.10.0/benchmarks/bench_batch.c +269 -0
  5. fastconstmap-0.10.0/benchmarks/benchmark.py +202 -0
  6. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/examples/example.py +10 -1
  7. fastconstmap-0.10.0/examples/shared_memory_example.py +57 -0
  8. fastconstmap-0.10.0/fastconstmap/__init__.py +38 -0
  9. fastconstmap-0.10.0/fastconstmap/_fastconstmap.pyi +111 -0
  10. fastconstmap-0.10.0/fastconstmap.egg-info/PKG-INFO +441 -0
  11. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/fastconstmap.egg-info/SOURCES.txt +5 -1
  12. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/pyproject.toml +1 -1
  13. fastconstmap-0.10.0/src/constmap.c +1300 -0
  14. fastconstmap-0.10.0/src/constmap.h +210 -0
  15. fastconstmap-0.10.0/src/fastconstmap_module.c +1365 -0
  16. fastconstmap-0.10.0/tests/test_constmap.py +405 -0
  17. fastconstmap-0.10.0/tests/test_interop.py +114 -0
  18. fastconstmap-0.10.0/tests/test_shared_memory.py +281 -0
  19. fastconstmap-0.8.0/PKG-INFO +0 -249
  20. fastconstmap-0.8.0/README.md +0 -231
  21. fastconstmap-0.8.0/benchmarks/benchmark.py +0 -155
  22. fastconstmap-0.8.0/fastconstmap/__init__.py +0 -24
  23. fastconstmap-0.8.0/fastconstmap/_fastconstmap.pyi +0 -62
  24. fastconstmap-0.8.0/fastconstmap.egg-info/PKG-INFO +0 -249
  25. fastconstmap-0.8.0/src/constmap.c +0 -833
  26. fastconstmap-0.8.0/src/constmap.h +0 -113
  27. fastconstmap-0.8.0/src/fastconstmap_module.c +0 -856
  28. fastconstmap-0.8.0/tests/test_constmap.py +0 -240
  29. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/LICENSE +0 -0
  30. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/fastconstmap/py.typed +0 -0
  31. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/fastconstmap.egg-info/dependency_links.txt +0 -0
  32. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/fastconstmap.egg-info/top_level.txt +0 -0
  33. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/setup.cfg +0 -0
  34. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/setup.py +0 -0
  35. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/src/third_party/xxhash/LICENSE +0 -0
  36. {fastconstmap-0.8.0 → fastconstmap-0.10.0}/src/third_party/xxhash/xxhash.h +0 -0
@@ -7,4 +7,4 @@ recursive-include src/third_party *
7
7
  recursive-include fastconstmap *.py *.pyi py.typed
8
8
  recursive-include tests *.py
9
9
  recursive-include examples *.py
10
- recursive-include benchmarks *.py
10
+ recursive-include benchmarks *.py *.c
@@ -0,0 +1,441 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastconstmap
3
+ Version: 0.10.0
4
+ Summary: Fast, immutable, compact map from strings to uint64 (binary fuse filter).
5
+ Author-email: Daniel Lemire <daniel@lemire.me>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/lemire/fastconstmap
8
+ Project-URL: Source, https://github.com/lemire/fastconstmap
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: C
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # fastconstmap
20
+
21
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
22
+ [![Test](https://github.com/lemire/fastconstmap/actions/workflows/test.yml/badge.svg)](https://github.com/lemire/fastconstmap/actions/workflows/test.yml)
23
+
24
+ Fast, immutable, compact map from strings to 64-bit integers — for Python.
25
+
26
+ `fastconstmap` is a C implementation of the [binary fuse filter][bff]
27
+ construction (a static perfect-hash-like structure), exposed to Python.
28
+ Given a `dict[str, int]` at build time, you get back a lookup object that:
29
+
30
+ - uses **~9 bytes per key** (or ~18 with missing-key detection),
31
+ - answers a lookup in **one xxhash call plus three array reads**,
32
+ - is **immutable** and **serializable** to bytes / a file, in a format the
33
+ Go and Rust implementations read too,
34
+ - exposes both a single-key API (`m[key]`) and a **batched** API
35
+ (`m.get_many([...])`, `m.get_many_into([...], buf)`) that amortises Python-C
36
+ call overhead and overlaps the memory accesses of a whole block of keys.
37
+
38
+ This package is a C port of the Go library
39
+ [`github.com/lemire/constmap`][constmap]. It vendors
40
+ [xxHash](https://github.com/Cyan4973/xxHash) (BSD-2) for string hashing.
41
+
42
+ ## Installation
43
+
44
+ ```
45
+ pip install fastconstmap
46
+ ```
47
+
48
+ To build from source you need a C compiler. There are no Python runtime
49
+ dependencies.
50
+
51
+ ## Usage
52
+
53
+ ```python
54
+ from fastconstmap import ConstMap, VerifiedConstMap, PairedVerifiedConstMap
55
+
56
+ d = {"apple": 100, "banana": 200, "cherry": 300}
57
+
58
+ # Variant 1: minimal memory, no missing-key detection.
59
+ m = ConstMap(d)
60
+ m["apple"] # -> 100
61
+ m.get_many(["banana", "cherry"]) # -> [200, 300]
62
+ m["grape"] # undefined value! use VerifiedConstMap if you care.
63
+
64
+ # Variant 2: dict-like, detects keys not in the original mapping.
65
+ vm = VerifiedConstMap(d)
66
+ vm["apple"] # -> 100
67
+ vm.get("grape") # -> None
68
+ vm.get("grape", -1) # -> -1
69
+ "grape" in vm # -> False
70
+ vm["grape"] # raises KeyError
71
+ vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
72
+
73
+ # Variant 3: same as VerifiedConstMap, laid out for fewer cache misses when
74
+ # the keys you look up are usually present.
75
+ pm = PairedVerifiedConstMap(d)
76
+ pm["apple"] # -> 100
77
+ pm.get("grape") # -> None
78
+
79
+ # Batches can also be written straight into a buffer you own — no list of
80
+ # Python ints is built.
81
+ import array
82
+ out = array.array("Q", [0]) * 3
83
+ m.get_many_into(["apple", "banana", "cherry"], out) # -> 3; out == [100, 200, 300]
84
+
85
+ # Any kind can be saved and loaded.
86
+ m.save("mymap.cmap")
87
+ m2 = ConstMap.load("mymap.cmap")
88
+
89
+ # ... or as raw bytes.
90
+ blob = m.to_bytes()
91
+ m3 = ConstMap.from_bytes(blob)
92
+ ```
93
+
94
+ ### Choosing between `ConstMap`, `VerifiedConstMap` and `PairedVerifiedConstMap`
95
+
96
+ | | `ConstMap` | `VerifiedConstMap` | `PairedVerifiedConstMap` |
97
+ |--|--|--|--|
98
+ | Bytes per key | ~9 | ~18 | ~18 |
99
+ | Lookup of present key | value | value | value |
100
+ | Lookup of missing key | **undefined garbage** | `KeyError` / `default` / `None` | same |
101
+ | Cache lines touched per lookup | 3 | 6 (3 on a miss) | 3 |
102
+ | Best if | you always look up known keys | you need dict-like semantics and most lookups miss | you need dict-like semantics and most lookups hit |
103
+
104
+ The false-positive rate of the verified maps (a missing key wrongly
105
+ reported as present) is roughly 2⁻⁶⁴, which is negligible in practice.
106
+
107
+ `VerifiedConstMap` keeps its values and its 64-bit check words in two
108
+ separate arrays. A lookup reads three check words, and if they match, three
109
+ value words: six cache lines, in two different arrays. `PairedVerifiedConstMap`
110
+ is the same map with each value stored next to its check word, so the three
111
+ reads bring in both at once (three cache lines), and on x86-64 and AArch64
112
+ each slot is loaded and XORed as one 128-bit word (SSE2 / NEON).
113
+
114
+ The trade-off is what happens on a **miss**. With the split layout, a
115
+ one-at-a-time lookup of an absent key reads only the check array, which is
116
+ half the size of the whole map and so far more likely to be sitting in cache.
117
+ The paired layout always pulls in the value alongside the check, so its
118
+ working set for misses is twice as large. In batched lookups
119
+ (`get_many`, `get_many_into`) the split layout gathers both arrays
120
+ unconditionally, so there the paired layout wins whether keys hit or miss.
121
+
122
+ Measured at the C level (`fcm_*_lookup`, one key at a time, random order
123
+ over the whole key set, best of five), ns per lookup, *hit* = key present,
124
+ *miss* = key absent:
125
+
126
+ | | keys | `VerifiedConstMap` hit | `PairedVerifiedConstMap` hit | `VerifiedConstMap` miss | `PairedVerifiedConstMap` miss |
127
+ |--|--|--|--|--|--|
128
+ | Apple M4 Max | 1M | 9.9 | 9.5 | **6.4** | 8.8 |
129
+ | | 10M | 20.9 | **17.1** | **15.5** | 17.1 |
130
+ | Xeon Gold 6548N | 1M | 17.3 | **12.8** | 13.8 | 13.0 |
131
+ | | 10M | 44.4 | **37.3** | **31.8** | 37.4 |
132
+
133
+ The two serialised formats are the same size but not interchangeable:
134
+ loading one class's bytes with the other raises `ValueError`.
135
+
136
+ ## Batched lookups
137
+
138
+ `get_many` takes an iterable of keys and returns a list of values, in the same
139
+ order:
140
+
141
+ ```python
142
+ cm.get_many(["apple", "banana", "cherry"]) # -> [100, 200, 300]
143
+ vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
144
+ ```
145
+
146
+ `get_many_into` is the same lookup writing 64-bit words into a buffer you own,
147
+ so a repeated batch allocates nothing at all — no list, and no Python `int` per
148
+ key. The buffer can be an `array("Q")`, a numpy `uint64` array, a
149
+ `SharedMemory` block, or any other writable buffer of at least `8 * len(keys)`
150
+ bytes. It returns the number of values written:
151
+
152
+ ```python
153
+ import array
154
+ out = array.array("Q", [0]) * 4096
155
+ for batch in batches:
156
+ n = cm.get_many_into(batch, out)
157
+ use(out[:n])
158
+ ```
159
+
160
+ `VerifiedConstMap.get_many_into` writes `fastconstmap.NOT_FOUND` (`2**64 - 1`)
161
+ for a key that was not in the original mapping — a buffer of raw words has no
162
+ room for a Python default.
163
+
164
+ A batch is faster than a loop over single lookups because it hashes a block of
165
+ eight keys before gathering any values. That lets the three array reads of all
166
+ eight keys be in flight at once, instead of each key's loads waiting behind the
167
+ hashing of the key before it. A lookup is memory-latency bound as soon as the
168
+ map outgrows the last-level cache, which is where the gain comes from.
169
+
170
+ ### Measured
171
+
172
+ At the C level, 2000-key batches against a 1,000,000-key map, ns/key, medians
173
+ of three runs (`benchmarks/bench_batch.c`). *Cold* rotates through 64 distinct
174
+ random batches so the cache lines the lookups touch are not already resident;
175
+ *hot* replays one batch, so the touched region stays cache-resident and hashing
176
+ dominates instead.
177
+
178
+ | | | loop over lookup | `lookup_many` | |
179
+ |---|---|---|---|---|
180
+ | **Apple M4 Max** | `ConstMap` cold | 7.1 | **5.5** | 23% |
181
+ | | `ConstMap` hot | 6.1 | **4.6** | 24% |
182
+ | | `VerifiedConstMap` cold | 11.9 | **8.0** | 33% |
183
+ | | `VerifiedConstMap` hot | 7.4 | **5.8** | 21% |
184
+ | | `PairedVerifiedConstMap` cold | 10.8 | **7.3** | 32% |
185
+ | | `PairedVerifiedConstMap` hot | 6.6 | **4.8** | 27% |
186
+ | **Xeon Gold 6548N** | `ConstMap` cold | 19.1 | **13.6** | 29% |
187
+ | | `ConstMap` hot | 10.7 | **8.5** | 21% |
188
+ | | `VerifiedConstMap` cold | 21.8 | 21.9 | ~0% |
189
+ | | `VerifiedConstMap` hot | 12.6 | **10.5** | 17% |
190
+ | | `PairedVerifiedConstMap` cold | 21.0 | **15.0** | 29% |
191
+ | | `PairedVerifiedConstMap` hot | 12.5 | **9.9** | 21% |
192
+
193
+ These are with the default key hash, XXH64, which is what makes the files
194
+ interchangeable with the Go and Rust implementations (see
195
+ [Interoperability](#interoperability-with-the-go-and-rust-implementations)).
196
+ XXH3, the hash of 0.9 and earlier, is about 1.3 ns faster per 20-byte key on
197
+ the M4 Max and 2 ns on the Xeon; `ConstMap(d, hash="xxh3")` keeps it, at the
198
+ price of files only fastconstmap can read.
199
+
200
+ The one case where batching does not pay is `VerifiedConstMap` in the cold
201
+ regime on the Xeon. That map probes two arrays per lookup, so a fresh batch is
202
+ bound by a memory latency the overlapping cannot hide. The Go original reports
203
+ the same result on the same processor.
204
+
205
+ Through the Python API, ns/key on the same batches — 0.8.0 is the previous
206
+ per-key loop, 0.9.0 the batched path (both measured with XXH3; with the
207
+ default XXH64 of 0.10, add roughly 1-4 ns/key to the batched columns, and
208
+ 1-5 ns to a single `m[k]` that costs 50-120 ns):
209
+
210
+ | | | 0.8.0 | 0.9.0 | `get_many_into` |
211
+ |---|---|---|---|---|
212
+ | **Apple M4 Max** | `ConstMap` cold | 30.7 | **20.6** | **14.2** |
213
+ | | `ConstMap` hot | 16.4 | **14.1** | **6.1** |
214
+ | | `VerifiedConstMap` cold | 35.5 | 35.3 | **23.9** |
215
+ | **Xeon Gold 6548N** | `ConstMap` cold | 34.9 | **25.5** | **16.5** |
216
+ | | `ConstMap` hot | 18.7 | **17.2** | **8.8** |
217
+ | | `VerifiedConstMap` cold | 37.6 | **31.9** | **23.3** |
218
+
219
+ What is left in the `get_many` column is the cost of boxing each value in a
220
+ Python `int` and building the list, which is why `get_many_into` — which does
221
+ neither — is another 30-60% faster again.
222
+
223
+ To reproduce:
224
+
225
+ ```
226
+ cc -O3 -std=c11 -Isrc benchmarks/bench_batch.c src/constmap.c -lm -o bench_batch
227
+ ./bench_batch 1000000
228
+ ```
229
+
230
+ ## Keys and values
231
+
232
+ - **Keys** may be `str` or `bytes`. `str` is encoded as UTF-8 internally;
233
+ lookups must use the same encoding to match.
234
+ - **Values** are 64-bit integers. We accept anything in
235
+ `[-2**63, 2**64 - 1]`; negatives are stored via two's complement, so
236
+ `m[k]` returns `2**64 - 1` for a value of `-1`. (To recover the signed
237
+ reading, reinterpret bits yourself.)
238
+
239
+ Keys must be unique (Python dict semantics already guarantee this).
240
+ Construction raises `ValueError` in the extremely unlikely event of an
241
+ xxhash collision (~2⁻⁶⁴ per key pair).
242
+
243
+ ## Sharing a map across processes (zero-copy)
244
+
245
+ A map's serialized form *is* its in-memory lookup array (plus a small
246
+ header). That means it can live in a
247
+ [`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
248
+ block and be opened by any number of processes **without copying** — every
249
+ process reads the same physical pages.
250
+
251
+ Three methods make this work:
252
+
253
+ | Method | Purpose |
254
+ |--|--|
255
+ | `m.serialized_size()` | bytes needed to hold the serialized map |
256
+ | `m.write_into(buffer)` | serialize straight into a writable buffer (no intermediate `bytes`) |
257
+ | `ConstMap.from_buffer(buffer)` | open a **zero-copy** map that reads directly from `buffer` |
258
+
259
+ Producer — build once, publish into a **named** shared-memory block:
260
+
261
+ ```python
262
+ from multiprocessing.shared_memory import SharedMemory
263
+ from fastconstmap import ConstMap
264
+
265
+ SHM_NAME = "fastconstmap_demo"
266
+
267
+ cm = ConstMap({f"key-{i}": i for i in range(1_000_000)})
268
+
269
+ shm = SharedMemory(create=True, size=cm.serialized_size(), name=SHM_NAME)
270
+ cm.write_into(shm.buf)
271
+ # keep `shm` alive (do not close/unlink) while consumers are running
272
+ ```
273
+
274
+ Consumer — attach to the same name with no copy:
275
+
276
+ ```python
277
+ from multiprocessing.shared_memory import SharedMemory
278
+ from fastconstmap import ConstMap
279
+
280
+ SHM_NAME = "fastconstmap_demo"
281
+
282
+ shm = SharedMemory(name=SHM_NAME) # attach by name, no `create=`
283
+ cm = ConstMap.from_buffer(shm.buf) # zero-copy: no per-process copy
284
+ cm["key-42"] # reads straight from shared memory
285
+ ```
286
+
287
+ Choosing the name yourself (rather than letting `SharedMemory` generate
288
+ one) means consumers can hard-code it or read it from config — no need to
289
+ pass the auto-generated name around. Pick a unique name; creating a block
290
+ whose name already exists raises `FileExistsError`.
291
+
292
+ Notes and constraints:
293
+
294
+ - **`from_buffer` does not copy.** The returned map holds a reference to the
295
+ buffer; the buffer (and, for shared memory, the `SharedMemory` object)
296
+ must stay alive and **must not be closed or mutated** while the map is in
297
+ use. Drop the map (`del cm`) before calling `shm.close()`.
298
+ - The map is **immutable** — the intended pattern is *write once in the
299
+ producer, then only read in every process*. Concurrent readers need no
300
+ locking.
301
+ - `from_buffer` verifies the magic bytes and the FNV-1a checksum, so a
302
+ truncated or corrupt block raises `ValueError` rather than returning
303
+ garbage.
304
+ - Requirements: a **little-endian** host (x86-64, ARM64, …) and an
305
+ **8-byte-aligned** buffer. `SharedMemory.buf`, `bytes`, and `bytearray`
306
+ all satisfy the alignment requirement; an offset slice of a buffer may
307
+ not, in which case `from_buffer` raises `ValueError` — use
308
+ `from_bytes()` (which copies) instead.
309
+ - `VerifiedConstMap` and `PairedVerifiedConstMap` support the same three
310
+ methods.
311
+
312
+ `from_bytes()` remains available when you *want* an owned copy (or need to
313
+ load on a big-endian host): it copies the data and the resulting map owns
314
+ its memory independently of the source buffer.
315
+
316
+ ## Interoperability with the Go and Rust implementations
317
+
318
+ A map saved by any of [`constmap`][constmap] (Go), [`rsconstmap`][rsconstmap]
319
+ (Rust) and fastconstmap loads in the other two, on a little-endian host (which
320
+ is every mainstream one: x86-64, ARM64, RISC-V, Apple silicon). The three share
321
+ the key hash (XXH64), the mixing, the seed sequence and the layout, so for the
322
+ same input Go and Rust write byte-identical files, and fastconstmap writes the
323
+ same bytes apart from one header word:
324
+
325
+ | class | magic | notes |
326
+ |---|---|---|
327
+ | `ConstMap` | `CMAP0003` | Go and Rust write `CMAP0001`, the same format minus a 4-byte key count; all three read both |
328
+ | `VerifiedConstMap` | `VMAP0001` | identical; the header word Go and Rust leave zero holds the key count here |
329
+ | `PairedVerifiedConstMap` | `PMAP0001` | likewise |
330
+
331
+ ```python
332
+ m = ConstMap.load("built-by-go.cmap") # from constmap.SaveToFile / save_to_file
333
+ vm = VerifiedConstMap.load("built-by-rust.vmap")
334
+ ```
335
+
336
+ Two things to know about a map that came from Go or Rust:
337
+
338
+ - **`len(m)` is 0**, because those writers do not record the key count. Every
339
+ lookup is unaffected.
340
+ - **A Go or Rust `ConstMap` file (`CMAP0001`) cannot be opened with
341
+ `from_buffer`**: its 28-byte header leaves the array 4 bytes off an 8-byte
342
+ boundary in any aligned buffer, so `from_buffer` raises `ValueError`. `load`
343
+ and `from_bytes`, which copy, work as usual. The other two formats have a
344
+ 32-byte header and view fine.
345
+
346
+ **Files from fastconstmap 0.9 and earlier** (`CMAP0002` / `VCMP0002`) still
347
+ load and answer correctly: they were built with XXH3, and a map keeps the hash
348
+ its file was built with (`m.hash` tells you which). Re-saving such a map keeps
349
+ its legacy magic, since its table only answers to XXH3; Go and Rust name these
350
+ files in their error message. To get a file the other implementations read,
351
+ rebuild the map from its keys.
352
+
353
+ **Keeping XXH3.** XXH3 is faster than XXH64 on short keys, by about 1-2 ns per
354
+ lookup at the C level. If you do not need the shared format, build with
355
+ `ConstMap(d, hash="xxh3")` (likewise for the other two classes); such a map
356
+ serializes with the legacy magic, readable by fastconstmap only.
357
+
358
+ The interoperability is tested: `tests/interop/` holds files written by the Go
359
+ package from a fixed input, which the test suite loads and checks, and the Go
360
+ and Rust repositories hold files written by this package.
361
+
362
+ [rsconstmap]: https://github.com/lemire/rsconstmap
363
+
364
+ ## Benchmark
365
+
366
+ On an Apple M4 Max, with 1,000,000 string keys (`key-{i}-{hex}` shaped
367
+ strings). Lookups query every key exactly once, in random order, from a list
368
+ built in that order:
369
+
370
+ ```
371
+ === fastconstmap benchmark — n = 1,000,000 keys, python 3.14.5 ===
372
+
373
+ Construction:
374
+ ConstMap.__init__ 0.145 s
375
+ VerifiedConstMap.__init__ 0.142 s
376
+ dict(d) 0.004 s
377
+
378
+ Memory:
379
+ ConstMap.nbytes 9,043,968 bytes (9.04 bytes/key)
380
+ VerifiedConstMap.nbytes 18,087,936 bytes (18.09 bytes/key)
381
+ dict (table+keys+values) 118,380,958 bytes (118.38 bytes/key)
382
+ ratio dict / ConstMap 13.1x
383
+
384
+ Single lookup, every key once in random order (1,000,000 ops):
385
+ dict[k] 336.1 ns/op (0.336 s total)
386
+ ConstMap[k] 95.2 ns/op (0.095 s total)
387
+ VerifiedConstMap[k] 112.3 ns/op (0.112 s total)
388
+
389
+ Batched lookup, 2000 × 2000:
390
+ dict comprehension (cold) 148.7 ns/op (0.30 ms/batch of 2000)
391
+ ConstMap.get_many (cold) 24.2 ns/op (0.05 ms/batch of 2000)
392
+ ConstMap.get_many (hot) 15.6 ns/op (0.03 ms/batch of 2000)
393
+ ConstMap.get_many_into (cold) 14.2 ns/op (0.03 ms/batch of 2000)
394
+ ConstMap.get_many_into (hot) 6.1 ns/op (0.01 ms/batch of 2000)
395
+ VerifiedConstMap.get_many (cold) 33.7 ns/op (0.07 ms/batch of 2000)
396
+ VerifiedConstMap.get_many_into (cold) 23.9 ns/op (0.05 ms/batch of 2000)
397
+
398
+ Serialization:
399
+ ConstMap.to_bytes 0.010 s (9,044,008 bytes)
400
+ ConstMap.from_bytes 0.010 s
401
+ ```
402
+
403
+ The single-lookup numbers are dominated by Python call overhead, not by the
404
+ map: use `get_many` when you have an array of keys to look up at once, and
405
+ `get_many_into` when you have somewhere to put the results.
406
+
407
+ To reproduce:
408
+
409
+ ```
410
+ python benchmarks/benchmark.py 1000000
411
+ ```
412
+
413
+ ## How it works
414
+
415
+ Given *n* (key, value) pairs the algorithm:
416
+
417
+ 1. Hashes each key with XXH64 (or XXH3 if asked) to a 64-bit value.
418
+ 2. Maps each hashed key to three positions `h0, h1, h2` in an array of
419
+ size ~1.125·*n*, using overlapping segments.
420
+ 3. Finds, via [peeling][bff], an ordering in which each key has an
421
+ exclusive cell among its three; walks that ordering in reverse,
422
+ setting each cell so `array[h0] ^ array[h1] ^ array[h2] == value`.
423
+
424
+ Lookup is one xxhash, three array reads, and two XORs.
425
+
426
+ References:
427
+
428
+ > Thomas Mueller Graf and Daniel Lemire,
429
+ > [*Binary Fuse Filters: Fast and Smaller Than Xor Filters*][bff],
430
+ > ACM Journal of Experimental Algorithmics, Vol. 27, 2022.
431
+ > DOI: [10.1145/3510449](https://doi.org/10.1145/3510449)
432
+
433
+ ## License
434
+
435
+ Apache License 2.0. See [LICENSE](LICENSE).
436
+
437
+ `fastconstmap` vendors xxHash, which is licensed under the BSD-2-clause
438
+ license; see `src/third_party/xxhash/LICENSE`.
439
+
440
+ [bff]: https://arxiv.org/abs/2201.01174
441
+ [constmap]: https://github.com/lemire/constmap