fastconstmap 0.9.0__tar.gz → 0.11.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.
- {fastconstmap-0.9.0/fastconstmap.egg-info → fastconstmap-0.11.0}/PKG-INFO +145 -24
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/README.md +143 -23
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/benchmarks/bench_batch.c +46 -2
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/benchmarks/benchmark.py +11 -1
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/fastconstmap/__init__.py +15 -6
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/fastconstmap/_fastconstmap.pyi +46 -4
- {fastconstmap-0.9.0 → fastconstmap-0.11.0/fastconstmap.egg-info}/PKG-INFO +145 -24
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/fastconstmap.egg-info/SOURCES.txt +3 -1
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/pyproject.toml +4 -1
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/src/constmap.c +557 -170
- fastconstmap-0.11.0/src/constmap.h +210 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/src/fastconstmap_module.c +556 -18
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/tests/test_constmap.py +85 -30
- fastconstmap-0.11.0/tests/test_interop.py +114 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/tests/test_shared_memory.py +63 -8
- fastconstmap-0.11.0/tests/test_threading.py +149 -0
- fastconstmap-0.9.0/src/constmap.h +0 -131
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/LICENSE +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/MANIFEST.in +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/examples/example.py +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/examples/shared_memory_example.py +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/fastconstmap/py.typed +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/fastconstmap.egg-info/dependency_links.txt +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/fastconstmap.egg-info/top_level.txt +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/setup.cfg +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/setup.py +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/src/third_party/xxhash/LICENSE +0 -0
- {fastconstmap-0.9.0 → fastconstmap-0.11.0}/src/third_party/xxhash/xxhash.h +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastconstmap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Fast, immutable, compact map from strings to uint64 (binary fuse filter).
|
|
5
5
|
Author-email: Daniel Lemire <daniel@lemire.me>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -8,6 +8,7 @@ Project-URL: Homepage, https://github.com/lemire/fastconstmap
|
|
|
8
8
|
Project-URL: Source, https://github.com/lemire/fastconstmap
|
|
9
9
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
|
11
12
|
Classifier: Programming Language :: C
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
13
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
@@ -29,7 +30,8 @@ Given a `dict[str, int]` at build time, you get back a lookup object that:
|
|
|
29
30
|
|
|
30
31
|
- uses **~9 bytes per key** (or ~18 with missing-key detection),
|
|
31
32
|
- answers a lookup in **one xxhash call plus three array reads**,
|
|
32
|
-
- is **immutable** and **serializable** to bytes / a file,
|
|
33
|
+
- is **immutable** and **serializable** to bytes / a file, in a format the
|
|
34
|
+
Go and Rust implementations read too,
|
|
33
35
|
- exposes both a single-key API (`m[key]`) and a **batched** API
|
|
34
36
|
(`m.get_many([...])`, `m.get_many_into([...], buf)`) that amortises Python-C
|
|
35
37
|
call overhead and overlaps the memory accesses of a whole block of keys.
|
|
@@ -50,7 +52,7 @@ dependencies.
|
|
|
50
52
|
## Usage
|
|
51
53
|
|
|
52
54
|
```python
|
|
53
|
-
from fastconstmap import ConstMap, VerifiedConstMap
|
|
55
|
+
from fastconstmap import ConstMap, VerifiedConstMap, PairedVerifiedConstMap
|
|
54
56
|
|
|
55
57
|
d = {"apple": 100, "banana": 200, "cherry": 300}
|
|
56
58
|
|
|
@@ -69,13 +71,19 @@ vm.get("grape", -1) # -> -1
|
|
|
69
71
|
vm["grape"] # raises KeyError
|
|
70
72
|
vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
|
|
71
73
|
|
|
74
|
+
# Variant 3: same as VerifiedConstMap, laid out for fewer cache misses when
|
|
75
|
+
# the keys you look up are usually present.
|
|
76
|
+
pm = PairedVerifiedConstMap(d)
|
|
77
|
+
pm["apple"] # -> 100
|
|
78
|
+
pm.get("grape") # -> None
|
|
79
|
+
|
|
72
80
|
# Batches can also be written straight into a buffer you own — no list of
|
|
73
81
|
# Python ints is built.
|
|
74
82
|
import array
|
|
75
83
|
out = array.array("Q", [0]) * 3
|
|
76
84
|
m.get_many_into(["apple", "banana", "cherry"], out) # -> 3; out == [100, 200, 300]
|
|
77
85
|
|
|
78
|
-
#
|
|
86
|
+
# Any kind can be saved and loaded.
|
|
79
87
|
m.save("mymap.cmap")
|
|
80
88
|
m2 = ConstMap.load("mymap.cmap")
|
|
81
89
|
|
|
@@ -84,18 +92,48 @@ blob = m.to_bytes()
|
|
|
84
92
|
m3 = ConstMap.from_bytes(blob)
|
|
85
93
|
```
|
|
86
94
|
|
|
87
|
-
### Choosing between `ConstMap` and `
|
|
95
|
+
### Choosing between `ConstMap`, `VerifiedConstMap` and `PairedVerifiedConstMap`
|
|
88
96
|
|
|
89
|
-
| | `ConstMap` | `VerifiedConstMap` |
|
|
90
|
-
|
|
91
|
-
| Bytes per key | ~9 | ~18 |
|
|
92
|
-
| Lookup of present key | value | value |
|
|
93
|
-
| Lookup of missing key | **undefined garbage** | `KeyError` / `default` / `None` |
|
|
94
|
-
|
|
|
97
|
+
| | `ConstMap` | `VerifiedConstMap` | `PairedVerifiedConstMap` |
|
|
98
|
+
|--|--|--|--|
|
|
99
|
+
| Bytes per key | ~9 | ~18 | ~18 |
|
|
100
|
+
| Lookup of present key | value | value | value |
|
|
101
|
+
| Lookup of missing key | **undefined garbage** | `KeyError` / `default` / `None` | same |
|
|
102
|
+
| Cache lines touched per lookup | 3 | 6 (3 on a miss) | 3 |
|
|
103
|
+
| 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 |
|
|
95
104
|
|
|
96
|
-
The false-positive rate of
|
|
105
|
+
The false-positive rate of the verified maps (a missing key wrongly
|
|
97
106
|
reported as present) is roughly 2⁻⁶⁴, which is negligible in practice.
|
|
98
107
|
|
|
108
|
+
`VerifiedConstMap` keeps its values and its 64-bit check words in two
|
|
109
|
+
separate arrays. A lookup reads three check words, and if they match, three
|
|
110
|
+
value words: six cache lines, in two different arrays. `PairedVerifiedConstMap`
|
|
111
|
+
is the same map with each value stored next to its check word, so the three
|
|
112
|
+
reads bring in both at once (three cache lines), and on x86-64 and AArch64
|
|
113
|
+
each slot is loaded and XORed as one 128-bit word (SSE2 / NEON).
|
|
114
|
+
|
|
115
|
+
The trade-off is what happens on a **miss**. With the split layout, a
|
|
116
|
+
one-at-a-time lookup of an absent key reads only the check array, which is
|
|
117
|
+
half the size of the whole map and so far more likely to be sitting in cache.
|
|
118
|
+
The paired layout always pulls in the value alongside the check, so its
|
|
119
|
+
working set for misses is twice as large. In batched lookups
|
|
120
|
+
(`get_many`, `get_many_into`) the split layout gathers both arrays
|
|
121
|
+
unconditionally, so there the paired layout wins whether keys hit or miss.
|
|
122
|
+
|
|
123
|
+
Measured at the C level (`fcm_*_lookup`, one key at a time, random order
|
|
124
|
+
over the whole key set, best of five), ns per lookup, *hit* = key present,
|
|
125
|
+
*miss* = key absent:
|
|
126
|
+
|
|
127
|
+
| | keys | `VerifiedConstMap` hit | `PairedVerifiedConstMap` hit | `VerifiedConstMap` miss | `PairedVerifiedConstMap` miss |
|
|
128
|
+
|--|--|--|--|--|--|
|
|
129
|
+
| Apple M4 Max | 1M | 9.9 | 9.5 | **6.4** | 8.8 |
|
|
130
|
+
| | 10M | 20.9 | **17.1** | **15.5** | 17.1 |
|
|
131
|
+
| Xeon Gold 6548N | 1M | 17.3 | **12.8** | 13.8 | 13.0 |
|
|
132
|
+
| | 10M | 44.4 | **37.3** | **31.8** | 37.4 |
|
|
133
|
+
|
|
134
|
+
The two serialised formats are the same size but not interchangeable:
|
|
135
|
+
loading one class's bytes with the other raises `ValueError`.
|
|
136
|
+
|
|
99
137
|
## Batched lookups
|
|
100
138
|
|
|
101
139
|
`get_many` takes an iterable of keys and returns a list of values, in the same
|
|
@@ -140,14 +178,25 @@ dominates instead.
|
|
|
140
178
|
|
|
141
179
|
| | | loop over lookup | `lookup_many` | |
|
|
142
180
|
|---|---|---|---|---|
|
|
143
|
-
| **Apple M4 Max** | `ConstMap` cold |
|
|
144
|
-
| | `ConstMap` hot |
|
|
145
|
-
| | `VerifiedConstMap` cold | 9
|
|
146
|
-
| | `VerifiedConstMap` hot |
|
|
147
|
-
|
|
|
148
|
-
| | `
|
|
149
|
-
| | `
|
|
150
|
-
| | `
|
|
181
|
+
| **Apple M4 Max** | `ConstMap` cold | 7.1 | **5.5** | 23% |
|
|
182
|
+
| | `ConstMap` hot | 6.1 | **4.6** | 24% |
|
|
183
|
+
| | `VerifiedConstMap` cold | 11.9 | **8.0** | 33% |
|
|
184
|
+
| | `VerifiedConstMap` hot | 7.4 | **5.8** | 21% |
|
|
185
|
+
| | `PairedVerifiedConstMap` cold | 10.8 | **7.3** | 32% |
|
|
186
|
+
| | `PairedVerifiedConstMap` hot | 6.6 | **4.8** | 27% |
|
|
187
|
+
| **Xeon Gold 6548N** | `ConstMap` cold | 19.1 | **13.6** | 29% |
|
|
188
|
+
| | `ConstMap` hot | 10.7 | **8.5** | 21% |
|
|
189
|
+
| | `VerifiedConstMap` cold | 21.8 | 21.9 | ~0% |
|
|
190
|
+
| | `VerifiedConstMap` hot | 12.6 | **10.5** | 17% |
|
|
191
|
+
| | `PairedVerifiedConstMap` cold | 21.0 | **15.0** | 29% |
|
|
192
|
+
| | `PairedVerifiedConstMap` hot | 12.5 | **9.9** | 21% |
|
|
193
|
+
|
|
194
|
+
These are with the default key hash, XXH64, which is what makes the files
|
|
195
|
+
interchangeable with the Go and Rust implementations (see
|
|
196
|
+
[Interoperability](#interoperability-with-the-go-and-rust-implementations)).
|
|
197
|
+
XXH3, the hash of 0.9 and earlier, is about 1.3 ns faster per 20-byte key on
|
|
198
|
+
the M4 Max and 2 ns on the Xeon; `ConstMap(d, hash="xxh3")` keeps it, at the
|
|
199
|
+
price of files only fastconstmap can read.
|
|
151
200
|
|
|
152
201
|
The one case where batching does not pay is `VerifiedConstMap` in the cold
|
|
153
202
|
regime on the Xeon. That map probes two arrays per lookup, so a fresh batch is
|
|
@@ -155,7 +204,9 @@ bound by a memory latency the overlapping cannot hide. The Go original reports
|
|
|
155
204
|
the same result on the same processor.
|
|
156
205
|
|
|
157
206
|
Through the Python API, ns/key on the same batches — 0.8.0 is the previous
|
|
158
|
-
per-key loop, 0.9.0 the batched path
|
|
207
|
+
per-key loop, 0.9.0 the batched path (both measured with XXH3; with the
|
|
208
|
+
default XXH64 of 0.10, add roughly 1-4 ns/key to the batched columns, and
|
|
209
|
+
1-5 ns to a single `m[k]` that costs 50-120 ns):
|
|
159
210
|
|
|
160
211
|
| | | 0.8.0 | 0.9.0 | `get_many_into` |
|
|
161
212
|
|---|---|---|---|---|
|
|
@@ -247,7 +298,7 @@ Notes and constraints:
|
|
|
247
298
|
use. Drop the map (`del cm`) before calling `shm.close()`.
|
|
248
299
|
- The map is **immutable** — the intended pattern is *write once in the
|
|
249
300
|
producer, then only read in every process*. Concurrent readers need no
|
|
250
|
-
locking.
|
|
301
|
+
locking (see [Threads](#threads-and-free-threaded-python)).
|
|
251
302
|
- `from_buffer` verifies the magic bytes and the FNV-1a checksum, so a
|
|
252
303
|
truncated or corrupt block raises `ValueError` rather than returning
|
|
253
304
|
garbage.
|
|
@@ -256,12 +307,82 @@ Notes and constraints:
|
|
|
256
307
|
all satisfy the alignment requirement; an offset slice of a buffer may
|
|
257
308
|
not, in which case `from_buffer` raises `ValueError` — use
|
|
258
309
|
`from_bytes()` (which copies) instead.
|
|
259
|
-
- `VerifiedConstMap`
|
|
310
|
+
- `VerifiedConstMap` and `PairedVerifiedConstMap` support the same three
|
|
311
|
+
methods.
|
|
260
312
|
|
|
261
313
|
`from_bytes()` remains available when you *want* an owned copy (or need to
|
|
262
314
|
load on a big-endian host): it copies the data and the resulting map owns
|
|
263
315
|
its memory independently of the source buffer.
|
|
264
316
|
|
|
317
|
+
## Threads and free-threaded Python
|
|
318
|
+
|
|
319
|
+
The maps are immutable, so any number of threads can look up keys in the
|
|
320
|
+
same map at once, without locking. The extension supports free-threaded
|
|
321
|
+
Python (3.13t, 3.14t): it does not re-enable the GIL on import, and lookups
|
|
322
|
+
from several threads then run in parallel.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
326
|
+
|
|
327
|
+
m = ConstMap(d)
|
|
328
|
+
with ThreadPoolExecutor(8) as pool:
|
|
329
|
+
results = list(pool.map(m.get_many, batches))
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Building a map from a dict that another thread is modifying is safe (on a
|
|
333
|
+
free-threaded build the map is built from a snapshot of the dict), and so is
|
|
334
|
+
a batched lookup over a list that another thread is modifying. `__init__` can only be called once on a
|
|
335
|
+
map; calling it again raises `TypeError` rather than replacing the data
|
|
336
|
+
under other readers.
|
|
337
|
+
|
|
338
|
+
## Interoperability with the Go and Rust implementations
|
|
339
|
+
|
|
340
|
+
A map saved by any of [`constmap`][constmap] (Go), [`rsconstmap`][rsconstmap]
|
|
341
|
+
(Rust) and fastconstmap loads in the other two, on a little-endian host (which
|
|
342
|
+
is every mainstream one: x86-64, ARM64, RISC-V, Apple silicon). The three share
|
|
343
|
+
the key hash (XXH64), the mixing, the seed sequence and the layout, so for the
|
|
344
|
+
same input Go and Rust write byte-identical files, and fastconstmap writes the
|
|
345
|
+
same bytes apart from one header word:
|
|
346
|
+
|
|
347
|
+
| class | magic | notes |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| `ConstMap` | `CMAP0003` | Go and Rust write `CMAP0001`, the same format minus a 4-byte key count; all three read both |
|
|
350
|
+
| `VerifiedConstMap` | `VMAP0001` | identical; the header word Go and Rust leave zero holds the key count here |
|
|
351
|
+
| `PairedVerifiedConstMap` | `PMAP0001` | likewise |
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
m = ConstMap.load("built-by-go.cmap") # from constmap.SaveToFile / save_to_file
|
|
355
|
+
vm = VerifiedConstMap.load("built-by-rust.vmap")
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Two things to know about a map that came from Go or Rust:
|
|
359
|
+
|
|
360
|
+
- **`len(m)` is 0**, because those writers do not record the key count. Every
|
|
361
|
+
lookup is unaffected.
|
|
362
|
+
- **A Go or Rust `ConstMap` file (`CMAP0001`) cannot be opened with
|
|
363
|
+
`from_buffer`**: its 28-byte header leaves the array 4 bytes off an 8-byte
|
|
364
|
+
boundary in any aligned buffer, so `from_buffer` raises `ValueError`. `load`
|
|
365
|
+
and `from_bytes`, which copy, work as usual. The other two formats have a
|
|
366
|
+
32-byte header and view fine.
|
|
367
|
+
|
|
368
|
+
**Files from fastconstmap 0.9 and earlier** (`CMAP0002` / `VCMP0002`) still
|
|
369
|
+
load and answer correctly: they were built with XXH3, and a map keeps the hash
|
|
370
|
+
its file was built with (`m.hash` tells you which). Re-saving such a map keeps
|
|
371
|
+
its legacy magic, since its table only answers to XXH3; Go and Rust name these
|
|
372
|
+
files in their error message. To get a file the other implementations read,
|
|
373
|
+
rebuild the map from its keys.
|
|
374
|
+
|
|
375
|
+
**Keeping XXH3.** XXH3 is faster than XXH64 on short keys, by about 1-2 ns per
|
|
376
|
+
lookup at the C level. If you do not need the shared format, build with
|
|
377
|
+
`ConstMap(d, hash="xxh3")` (likewise for the other two classes); such a map
|
|
378
|
+
serializes with the legacy magic, readable by fastconstmap only.
|
|
379
|
+
|
|
380
|
+
The interoperability is tested: `tests/interop/` holds files written by the Go
|
|
381
|
+
package from a fixed input, which the test suite loads and checks, and the Go
|
|
382
|
+
and Rust repositories hold files written by this package.
|
|
383
|
+
|
|
384
|
+
[rsconstmap]: https://github.com/lemire/rsconstmap
|
|
385
|
+
|
|
265
386
|
## Benchmark
|
|
266
387
|
|
|
267
388
|
On an Apple M4 Max, with 1,000,000 string keys (`key-{i}-{hex}` shaped
|
|
@@ -315,7 +436,7 @@ python benchmarks/benchmark.py 1000000
|
|
|
315
436
|
|
|
316
437
|
Given *n* (key, value) pairs the algorithm:
|
|
317
438
|
|
|
318
|
-
1. Hashes each key with
|
|
439
|
+
1. Hashes each key with XXH64 (or XXH3 if asked) to a 64-bit value.
|
|
319
440
|
2. Maps each hashed key to three positions `h0, h1, h2` in an array of
|
|
320
441
|
size ~1.125·*n*, using overlapping segments.
|
|
321
442
|
3. Finds, via [peeling][bff], an ordering in which each key has an
|
|
@@ -11,7 +11,8 @@ Given a `dict[str, int]` at build time, you get back a lookup object that:
|
|
|
11
11
|
|
|
12
12
|
- uses **~9 bytes per key** (or ~18 with missing-key detection),
|
|
13
13
|
- answers a lookup in **one xxhash call plus three array reads**,
|
|
14
|
-
- is **immutable** and **serializable** to bytes / a file,
|
|
14
|
+
- is **immutable** and **serializable** to bytes / a file, in a format the
|
|
15
|
+
Go and Rust implementations read too,
|
|
15
16
|
- exposes both a single-key API (`m[key]`) and a **batched** API
|
|
16
17
|
(`m.get_many([...])`, `m.get_many_into([...], buf)`) that amortises Python-C
|
|
17
18
|
call overhead and overlaps the memory accesses of a whole block of keys.
|
|
@@ -32,7 +33,7 @@ dependencies.
|
|
|
32
33
|
## Usage
|
|
33
34
|
|
|
34
35
|
```python
|
|
35
|
-
from fastconstmap import ConstMap, VerifiedConstMap
|
|
36
|
+
from fastconstmap import ConstMap, VerifiedConstMap, PairedVerifiedConstMap
|
|
36
37
|
|
|
37
38
|
d = {"apple": 100, "banana": 200, "cherry": 300}
|
|
38
39
|
|
|
@@ -51,13 +52,19 @@ vm.get("grape", -1) # -> -1
|
|
|
51
52
|
vm["grape"] # raises KeyError
|
|
52
53
|
vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
|
|
53
54
|
|
|
55
|
+
# Variant 3: same as VerifiedConstMap, laid out for fewer cache misses when
|
|
56
|
+
# the keys you look up are usually present.
|
|
57
|
+
pm = PairedVerifiedConstMap(d)
|
|
58
|
+
pm["apple"] # -> 100
|
|
59
|
+
pm.get("grape") # -> None
|
|
60
|
+
|
|
54
61
|
# Batches can also be written straight into a buffer you own — no list of
|
|
55
62
|
# Python ints is built.
|
|
56
63
|
import array
|
|
57
64
|
out = array.array("Q", [0]) * 3
|
|
58
65
|
m.get_many_into(["apple", "banana", "cherry"], out) # -> 3; out == [100, 200, 300]
|
|
59
66
|
|
|
60
|
-
#
|
|
67
|
+
# Any kind can be saved and loaded.
|
|
61
68
|
m.save("mymap.cmap")
|
|
62
69
|
m2 = ConstMap.load("mymap.cmap")
|
|
63
70
|
|
|
@@ -66,18 +73,48 @@ blob = m.to_bytes()
|
|
|
66
73
|
m3 = ConstMap.from_bytes(blob)
|
|
67
74
|
```
|
|
68
75
|
|
|
69
|
-
### Choosing between `ConstMap` and `
|
|
76
|
+
### Choosing between `ConstMap`, `VerifiedConstMap` and `PairedVerifiedConstMap`
|
|
70
77
|
|
|
71
|
-
| | `ConstMap` | `VerifiedConstMap` |
|
|
72
|
-
|
|
73
|
-
| Bytes per key | ~9 | ~18 |
|
|
74
|
-
| Lookup of present key | value | value |
|
|
75
|
-
| Lookup of missing key | **undefined garbage** | `KeyError` / `default` / `None` |
|
|
76
|
-
|
|
|
78
|
+
| | `ConstMap` | `VerifiedConstMap` | `PairedVerifiedConstMap` |
|
|
79
|
+
|--|--|--|--|
|
|
80
|
+
| Bytes per key | ~9 | ~18 | ~18 |
|
|
81
|
+
| Lookup of present key | value | value | value |
|
|
82
|
+
| Lookup of missing key | **undefined garbage** | `KeyError` / `default` / `None` | same |
|
|
83
|
+
| Cache lines touched per lookup | 3 | 6 (3 on a miss) | 3 |
|
|
84
|
+
| 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 |
|
|
77
85
|
|
|
78
|
-
The false-positive rate of
|
|
86
|
+
The false-positive rate of the verified maps (a missing key wrongly
|
|
79
87
|
reported as present) is roughly 2⁻⁶⁴, which is negligible in practice.
|
|
80
88
|
|
|
89
|
+
`VerifiedConstMap` keeps its values and its 64-bit check words in two
|
|
90
|
+
separate arrays. A lookup reads three check words, and if they match, three
|
|
91
|
+
value words: six cache lines, in two different arrays. `PairedVerifiedConstMap`
|
|
92
|
+
is the same map with each value stored next to its check word, so the three
|
|
93
|
+
reads bring in both at once (three cache lines), and on x86-64 and AArch64
|
|
94
|
+
each slot is loaded and XORed as one 128-bit word (SSE2 / NEON).
|
|
95
|
+
|
|
96
|
+
The trade-off is what happens on a **miss**. With the split layout, a
|
|
97
|
+
one-at-a-time lookup of an absent key reads only the check array, which is
|
|
98
|
+
half the size of the whole map and so far more likely to be sitting in cache.
|
|
99
|
+
The paired layout always pulls in the value alongside the check, so its
|
|
100
|
+
working set for misses is twice as large. In batched lookups
|
|
101
|
+
(`get_many`, `get_many_into`) the split layout gathers both arrays
|
|
102
|
+
unconditionally, so there the paired layout wins whether keys hit or miss.
|
|
103
|
+
|
|
104
|
+
Measured at the C level (`fcm_*_lookup`, one key at a time, random order
|
|
105
|
+
over the whole key set, best of five), ns per lookup, *hit* = key present,
|
|
106
|
+
*miss* = key absent:
|
|
107
|
+
|
|
108
|
+
| | keys | `VerifiedConstMap` hit | `PairedVerifiedConstMap` hit | `VerifiedConstMap` miss | `PairedVerifiedConstMap` miss |
|
|
109
|
+
|--|--|--|--|--|--|
|
|
110
|
+
| Apple M4 Max | 1M | 9.9 | 9.5 | **6.4** | 8.8 |
|
|
111
|
+
| | 10M | 20.9 | **17.1** | **15.5** | 17.1 |
|
|
112
|
+
| Xeon Gold 6548N | 1M | 17.3 | **12.8** | 13.8 | 13.0 |
|
|
113
|
+
| | 10M | 44.4 | **37.3** | **31.8** | 37.4 |
|
|
114
|
+
|
|
115
|
+
The two serialised formats are the same size but not interchangeable:
|
|
116
|
+
loading one class's bytes with the other raises `ValueError`.
|
|
117
|
+
|
|
81
118
|
## Batched lookups
|
|
82
119
|
|
|
83
120
|
`get_many` takes an iterable of keys and returns a list of values, in the same
|
|
@@ -122,14 +159,25 @@ dominates instead.
|
|
|
122
159
|
|
|
123
160
|
| | | loop over lookup | `lookup_many` | |
|
|
124
161
|
|---|---|---|---|---|
|
|
125
|
-
| **Apple M4 Max** | `ConstMap` cold |
|
|
126
|
-
| | `ConstMap` hot |
|
|
127
|
-
| | `VerifiedConstMap` cold | 9
|
|
128
|
-
| | `VerifiedConstMap` hot |
|
|
129
|
-
|
|
|
130
|
-
| | `
|
|
131
|
-
| | `
|
|
132
|
-
| | `
|
|
162
|
+
| **Apple M4 Max** | `ConstMap` cold | 7.1 | **5.5** | 23% |
|
|
163
|
+
| | `ConstMap` hot | 6.1 | **4.6** | 24% |
|
|
164
|
+
| | `VerifiedConstMap` cold | 11.9 | **8.0** | 33% |
|
|
165
|
+
| | `VerifiedConstMap` hot | 7.4 | **5.8** | 21% |
|
|
166
|
+
| | `PairedVerifiedConstMap` cold | 10.8 | **7.3** | 32% |
|
|
167
|
+
| | `PairedVerifiedConstMap` hot | 6.6 | **4.8** | 27% |
|
|
168
|
+
| **Xeon Gold 6548N** | `ConstMap` cold | 19.1 | **13.6** | 29% |
|
|
169
|
+
| | `ConstMap` hot | 10.7 | **8.5** | 21% |
|
|
170
|
+
| | `VerifiedConstMap` cold | 21.8 | 21.9 | ~0% |
|
|
171
|
+
| | `VerifiedConstMap` hot | 12.6 | **10.5** | 17% |
|
|
172
|
+
| | `PairedVerifiedConstMap` cold | 21.0 | **15.0** | 29% |
|
|
173
|
+
| | `PairedVerifiedConstMap` hot | 12.5 | **9.9** | 21% |
|
|
174
|
+
|
|
175
|
+
These are with the default key hash, XXH64, which is what makes the files
|
|
176
|
+
interchangeable with the Go and Rust implementations (see
|
|
177
|
+
[Interoperability](#interoperability-with-the-go-and-rust-implementations)).
|
|
178
|
+
XXH3, the hash of 0.9 and earlier, is about 1.3 ns faster per 20-byte key on
|
|
179
|
+
the M4 Max and 2 ns on the Xeon; `ConstMap(d, hash="xxh3")` keeps it, at the
|
|
180
|
+
price of files only fastconstmap can read.
|
|
133
181
|
|
|
134
182
|
The one case where batching does not pay is `VerifiedConstMap` in the cold
|
|
135
183
|
regime on the Xeon. That map probes two arrays per lookup, so a fresh batch is
|
|
@@ -137,7 +185,9 @@ bound by a memory latency the overlapping cannot hide. The Go original reports
|
|
|
137
185
|
the same result on the same processor.
|
|
138
186
|
|
|
139
187
|
Through the Python API, ns/key on the same batches — 0.8.0 is the previous
|
|
140
|
-
per-key loop, 0.9.0 the batched path
|
|
188
|
+
per-key loop, 0.9.0 the batched path (both measured with XXH3; with the
|
|
189
|
+
default XXH64 of 0.10, add roughly 1-4 ns/key to the batched columns, and
|
|
190
|
+
1-5 ns to a single `m[k]` that costs 50-120 ns):
|
|
141
191
|
|
|
142
192
|
| | | 0.8.0 | 0.9.0 | `get_many_into` |
|
|
143
193
|
|---|---|---|---|---|
|
|
@@ -229,7 +279,7 @@ Notes and constraints:
|
|
|
229
279
|
use. Drop the map (`del cm`) before calling `shm.close()`.
|
|
230
280
|
- The map is **immutable** — the intended pattern is *write once in the
|
|
231
281
|
producer, then only read in every process*. Concurrent readers need no
|
|
232
|
-
locking.
|
|
282
|
+
locking (see [Threads](#threads-and-free-threaded-python)).
|
|
233
283
|
- `from_buffer` verifies the magic bytes and the FNV-1a checksum, so a
|
|
234
284
|
truncated or corrupt block raises `ValueError` rather than returning
|
|
235
285
|
garbage.
|
|
@@ -238,12 +288,82 @@ Notes and constraints:
|
|
|
238
288
|
all satisfy the alignment requirement; an offset slice of a buffer may
|
|
239
289
|
not, in which case `from_buffer` raises `ValueError` — use
|
|
240
290
|
`from_bytes()` (which copies) instead.
|
|
241
|
-
- `VerifiedConstMap`
|
|
291
|
+
- `VerifiedConstMap` and `PairedVerifiedConstMap` support the same three
|
|
292
|
+
methods.
|
|
242
293
|
|
|
243
294
|
`from_bytes()` remains available when you *want* an owned copy (or need to
|
|
244
295
|
load on a big-endian host): it copies the data and the resulting map owns
|
|
245
296
|
its memory independently of the source buffer.
|
|
246
297
|
|
|
298
|
+
## Threads and free-threaded Python
|
|
299
|
+
|
|
300
|
+
The maps are immutable, so any number of threads can look up keys in the
|
|
301
|
+
same map at once, without locking. The extension supports free-threaded
|
|
302
|
+
Python (3.13t, 3.14t): it does not re-enable the GIL on import, and lookups
|
|
303
|
+
from several threads then run in parallel.
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
307
|
+
|
|
308
|
+
m = ConstMap(d)
|
|
309
|
+
with ThreadPoolExecutor(8) as pool:
|
|
310
|
+
results = list(pool.map(m.get_many, batches))
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Building a map from a dict that another thread is modifying is safe (on a
|
|
314
|
+
free-threaded build the map is built from a snapshot of the dict), and so is
|
|
315
|
+
a batched lookup over a list that another thread is modifying. `__init__` can only be called once on a
|
|
316
|
+
map; calling it again raises `TypeError` rather than replacing the data
|
|
317
|
+
under other readers.
|
|
318
|
+
|
|
319
|
+
## Interoperability with the Go and Rust implementations
|
|
320
|
+
|
|
321
|
+
A map saved by any of [`constmap`][constmap] (Go), [`rsconstmap`][rsconstmap]
|
|
322
|
+
(Rust) and fastconstmap loads in the other two, on a little-endian host (which
|
|
323
|
+
is every mainstream one: x86-64, ARM64, RISC-V, Apple silicon). The three share
|
|
324
|
+
the key hash (XXH64), the mixing, the seed sequence and the layout, so for the
|
|
325
|
+
same input Go and Rust write byte-identical files, and fastconstmap writes the
|
|
326
|
+
same bytes apart from one header word:
|
|
327
|
+
|
|
328
|
+
| class | magic | notes |
|
|
329
|
+
|---|---|---|
|
|
330
|
+
| `ConstMap` | `CMAP0003` | Go and Rust write `CMAP0001`, the same format minus a 4-byte key count; all three read both |
|
|
331
|
+
| `VerifiedConstMap` | `VMAP0001` | identical; the header word Go and Rust leave zero holds the key count here |
|
|
332
|
+
| `PairedVerifiedConstMap` | `PMAP0001` | likewise |
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
m = ConstMap.load("built-by-go.cmap") # from constmap.SaveToFile / save_to_file
|
|
336
|
+
vm = VerifiedConstMap.load("built-by-rust.vmap")
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Two things to know about a map that came from Go or Rust:
|
|
340
|
+
|
|
341
|
+
- **`len(m)` is 0**, because those writers do not record the key count. Every
|
|
342
|
+
lookup is unaffected.
|
|
343
|
+
- **A Go or Rust `ConstMap` file (`CMAP0001`) cannot be opened with
|
|
344
|
+
`from_buffer`**: its 28-byte header leaves the array 4 bytes off an 8-byte
|
|
345
|
+
boundary in any aligned buffer, so `from_buffer` raises `ValueError`. `load`
|
|
346
|
+
and `from_bytes`, which copy, work as usual. The other two formats have a
|
|
347
|
+
32-byte header and view fine.
|
|
348
|
+
|
|
349
|
+
**Files from fastconstmap 0.9 and earlier** (`CMAP0002` / `VCMP0002`) still
|
|
350
|
+
load and answer correctly: they were built with XXH3, and a map keeps the hash
|
|
351
|
+
its file was built with (`m.hash` tells you which). Re-saving such a map keeps
|
|
352
|
+
its legacy magic, since its table only answers to XXH3; Go and Rust name these
|
|
353
|
+
files in their error message. To get a file the other implementations read,
|
|
354
|
+
rebuild the map from its keys.
|
|
355
|
+
|
|
356
|
+
**Keeping XXH3.** XXH3 is faster than XXH64 on short keys, by about 1-2 ns per
|
|
357
|
+
lookup at the C level. If you do not need the shared format, build with
|
|
358
|
+
`ConstMap(d, hash="xxh3")` (likewise for the other two classes); such a map
|
|
359
|
+
serializes with the legacy magic, readable by fastconstmap only.
|
|
360
|
+
|
|
361
|
+
The interoperability is tested: `tests/interop/` holds files written by the Go
|
|
362
|
+
package from a fixed input, which the test suite loads and checks, and the Go
|
|
363
|
+
and Rust repositories hold files written by this package.
|
|
364
|
+
|
|
365
|
+
[rsconstmap]: https://github.com/lemire/rsconstmap
|
|
366
|
+
|
|
247
367
|
## Benchmark
|
|
248
368
|
|
|
249
369
|
On an Apple M4 Max, with 1,000,000 string keys (`key-{i}-{hex}` shaped
|
|
@@ -297,7 +417,7 @@ python benchmarks/benchmark.py 1000000
|
|
|
297
417
|
|
|
298
418
|
Given *n* (key, value) pairs the algorithm:
|
|
299
419
|
|
|
300
|
-
1. Hashes each key with
|
|
420
|
+
1. Hashes each key with XXH64 (or XXH3 if asked) to a 64-bit value.
|
|
301
421
|
2. Maps each hashed key to three positions `h0, h1, h2` in an array of
|
|
302
422
|
size ~1.125·*n*, using overlapping segments.
|
|
303
423
|
3. Finds, via [peeling][bff], an ordering in which each key has an
|
|
@@ -113,14 +113,18 @@ int main(int argc, char **argv) {
|
|
|
113
113
|
if (!values) { fprintf(stderr, "oom\n"); return 1; }
|
|
114
114
|
for (size_t i = 0; i < map_n; i++) values[i] = i;
|
|
115
115
|
|
|
116
|
-
fcm_constmap_t
|
|
117
|
-
fcm_verified_constmap_t
|
|
116
|
+
fcm_constmap_t cm;
|
|
117
|
+
fcm_verified_constmap_t vm;
|
|
118
|
+
fcm_paired_verified_constmap_t pm;
|
|
118
119
|
if (fcm_constmap_new(&cm, all.keys, values, map_n) != FCM_OK) {
|
|
119
120
|
fprintf(stderr, "construction failed\n"); return 1;
|
|
120
121
|
}
|
|
121
122
|
if (fcm_verified_constmap_new(&vm, all.keys, values, map_n) != FCM_OK) {
|
|
122
123
|
fprintf(stderr, "verified construction failed\n"); return 1;
|
|
123
124
|
}
|
|
125
|
+
if (fcm_paired_verified_constmap_new(&pm, all.keys, values, map_n) != FCM_OK) {
|
|
126
|
+
fprintf(stderr, "paired construction failed\n"); return 1;
|
|
127
|
+
}
|
|
124
128
|
|
|
125
129
|
keyset_t pools[NUM_POOLS];
|
|
126
130
|
for (int p = 0; p < NUM_POOLS; p++) {
|
|
@@ -215,11 +219,51 @@ int main(int argc, char **argv) {
|
|
|
215
219
|
report("lookup_many (hot)", now_sec() - t0, total,
|
|
216
220
|
loop_ns * 1e9 / (double)total);
|
|
217
221
|
|
|
222
|
+
/* ---- PairedVerifiedConstMap, cold ---- */
|
|
223
|
+
printf("\nPairedVerifiedConstMap:\n");
|
|
224
|
+
t0 = now_sec();
|
|
225
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
226
|
+
const keyset_t *q = &pools[i % NUM_POOLS];
|
|
227
|
+
for (size_t j = 0; j < q->n; j++)
|
|
228
|
+
out[j] = fcm_paired_verified_constmap_lookup(&pm, q->keys[j].bytes, q->keys[j].len);
|
|
229
|
+
sink = out[0];
|
|
230
|
+
}
|
|
231
|
+
loop_ns = now_sec() - t0;
|
|
232
|
+
report("loop over lookup (cold)", loop_ns, total, 0.0);
|
|
233
|
+
|
|
234
|
+
t0 = now_sec();
|
|
235
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
236
|
+
const keyset_t *q = &pools[i % NUM_POOLS];
|
|
237
|
+
fcm_paired_verified_constmap_lookup_many(&pm, q->keys, q->n, out);
|
|
238
|
+
sink = out[0];
|
|
239
|
+
}
|
|
240
|
+
report("lookup_many (cold)", now_sec() - t0, total,
|
|
241
|
+
loop_ns * 1e9 / (double)total);
|
|
242
|
+
|
|
243
|
+
/* ---- PairedVerifiedConstMap, hot ---- */
|
|
244
|
+
t0 = now_sec();
|
|
245
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
246
|
+
for (size_t j = 0; j < hot->n; j++)
|
|
247
|
+
out[j] = fcm_paired_verified_constmap_lookup(&pm, hot->keys[j].bytes, hot->keys[j].len);
|
|
248
|
+
sink = out[0];
|
|
249
|
+
}
|
|
250
|
+
loop_ns = now_sec() - t0;
|
|
251
|
+
report("loop over lookup (hot)", loop_ns, total, 0.0);
|
|
252
|
+
|
|
253
|
+
t0 = now_sec();
|
|
254
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
255
|
+
fcm_paired_verified_constmap_lookup_many(&pm, hot->keys, hot->n, out);
|
|
256
|
+
sink = out[0];
|
|
257
|
+
}
|
|
258
|
+
report("lookup_many (hot)", now_sec() - t0, total,
|
|
259
|
+
loop_ns * 1e9 / (double)total);
|
|
260
|
+
|
|
218
261
|
for (int p = 0; p < NUM_POOLS; p++) keyset_free(&pools[p]);
|
|
219
262
|
keyset_free(&all);
|
|
220
263
|
free(values);
|
|
221
264
|
free(out);
|
|
222
265
|
fcm_constmap_free(&cm);
|
|
223
266
|
fcm_verified_constmap_free(&vm);
|
|
267
|
+
fcm_paired_verified_constmap_free(&pm);
|
|
224
268
|
return 0;
|
|
225
269
|
}
|
|
@@ -22,7 +22,7 @@ import sys
|
|
|
22
22
|
import time
|
|
23
23
|
from typing import Callable
|
|
24
24
|
|
|
25
|
-
from fastconstmap import ConstMap, VerifiedConstMap
|
|
25
|
+
from fastconstmap import ConstMap, PairedVerifiedConstMap, VerifiedConstMap
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def make_data(n: int, seed: int = 0xC0FFEE) -> tuple[list[str], dict[str, int]]:
|
|
@@ -118,6 +118,10 @@ def main() -> None:
|
|
|
118
118
|
vm = VerifiedConstMap(d)
|
|
119
119
|
print(f" VerifiedConstMap.__init__ {time.perf_counter()-t0:7.3f} s")
|
|
120
120
|
|
|
121
|
+
t0 = time.perf_counter()
|
|
122
|
+
pm = PairedVerifiedConstMap(d)
|
|
123
|
+
print(f" PairedVerifiedConstMap.__init__ {time.perf_counter()-t0:7.3f} s")
|
|
124
|
+
|
|
121
125
|
t0 = time.perf_counter()
|
|
122
126
|
pyd = dict(d)
|
|
123
127
|
print(f" dict(d) {time.perf_counter()-t0:7.3f} s")
|
|
@@ -130,6 +134,7 @@ def main() -> None:
|
|
|
130
134
|
print("\nMemory:")
|
|
131
135
|
report_size("ConstMap.nbytes", cm.nbytes(), n)
|
|
132
136
|
report_size("VerifiedConstMap.nbytes", vm.nbytes(), n)
|
|
137
|
+
report_size("PairedVerifiedConstMap.nbytes", pm.nbytes(), n)
|
|
133
138
|
dict_total = deep_sizeof(pyd)
|
|
134
139
|
report_size("dict (table+keys+values)", dict_total, n)
|
|
135
140
|
print(f" {'ratio dict / ConstMap':<38} {dict_total / cm.nbytes():.1f}x")
|
|
@@ -140,6 +145,7 @@ def main() -> None:
|
|
|
140
145
|
time_loop("dict[k]", n, lambda i: pyd[query[i]])
|
|
141
146
|
time_loop("ConstMap[k]", n, lambda i: cm[query[i]])
|
|
142
147
|
time_loop("VerifiedConstMap[k]", n, lambda i: vm[query[i]])
|
|
148
|
+
time_loop("PairedVerifiedConstMap[k]", n, lambda i: pm[query[i]])
|
|
143
149
|
|
|
144
150
|
# ---- Batched lookup ----
|
|
145
151
|
# Cold rotates through many distinct random batches, so the cache lines a
|
|
@@ -175,6 +181,10 @@ def main() -> None:
|
|
|
175
181
|
lambda: vm.get_many(cold()))
|
|
176
182
|
time_batch("VerifiedConstMap.get_many_into (cold)", args.batches, args.batch_size,
|
|
177
183
|
lambda: vm.get_many_into(cold(), out))
|
|
184
|
+
time_batch("PairedVerifiedConstMap.get_many (cold)", args.batches, args.batch_size,
|
|
185
|
+
lambda: pm.get_many(cold()))
|
|
186
|
+
time_batch("PairedVerifiedConstMap.get_many_into (cold)", args.batches, args.batch_size,
|
|
187
|
+
lambda: pm.get_many_into(cold(), out))
|
|
178
188
|
|
|
179
189
|
# ---- Serialization ----
|
|
180
190
|
print("\nSerialization:")
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Build a compact lookup structure from any ``dict[str, int]`` (or
|
|
4
4
|
``dict[bytes, int]``). Lookup costs one xxhash and three array reads.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Three classes are exposed:
|
|
7
7
|
|
|
8
8
|
* :class:`ConstMap` — minimal, ~9 bytes/key. Returns undefined values for
|
|
9
9
|
keys that were not in the original mapping. Use when you know upstream
|
|
@@ -12,9 +12,18 @@ Two classes are exposed:
|
|
|
12
12
|
probability ~1 − 2⁻⁶⁴. Behaves like a normal ``dict``: ``m[k]`` raises
|
|
13
13
|
:class:`KeyError`, ``m.get(k, default)`` returns ``default``, ``k in m``
|
|
14
14
|
works.
|
|
15
|
+
* :class:`PairedVerifiedConstMap` — same size and behaviour as
|
|
16
|
+
:class:`VerifiedConstMap`, but stores each value next to its check word so
|
|
17
|
+
a lookup of a present key touches three cache lines instead of six (and
|
|
18
|
+
reads each as one 128-bit word on x86-64 and AArch64). Faster when most
|
|
19
|
+
lookups hit; slower when most miss. Its serialised form is not
|
|
20
|
+
interchangeable with :class:`VerifiedConstMap`'s.
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
or to a file path
|
|
22
|
+
All three are immutable after construction and can be serialised to bytes
|
|
23
|
+
or to a file path, in a format shared with the Go (``constmap``) and Rust
|
|
24
|
+
(``rsconstmap``) implementations: a file saved by any of the three loads in
|
|
25
|
+
the other two, on a little-endian host. Pass ``hash="xxh3"`` to a constructor
|
|
26
|
+
for a faster key hash whose files only fastconstmap reads. All also take batches: ``get_many`` returns a list, and
|
|
18
27
|
``get_many_into`` writes 64-bit words straight into a buffer you own (an
|
|
19
28
|
``array("Q")``, a numpy ``uint64`` array, a ``SharedMemory`` block), which
|
|
20
29
|
allocates nothing per key. A batch is faster than a loop because the map
|
|
@@ -23,7 +32,7 @@ whole block overlap instead of being paid one after another. They can also be pl
|
|
|
23
32
|
``SharedMemory`` block and opened by many processes with no per-process
|
|
24
33
|
copy — see ``serialized_size``, ``write_into`` and ``from_buffer``.
|
|
25
34
|
"""
|
|
26
|
-
from ._fastconstmap import ConstMap, VerifiedConstMap, NOT_FOUND
|
|
35
|
+
from ._fastconstmap import ConstMap, VerifiedConstMap, PairedVerifiedConstMap, NOT_FOUND
|
|
27
36
|
|
|
28
|
-
__all__ = ["ConstMap", "VerifiedConstMap", "NOT_FOUND"]
|
|
29
|
-
__version__ = "0.
|
|
37
|
+
__all__ = ["ConstMap", "VerifiedConstMap", "PairedVerifiedConstMap", "NOT_FOUND"]
|
|
38
|
+
__version__ = "0.10.0"
|