fastconstmap 0.7.0__tar.gz → 0.9.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.7.0 → fastconstmap-0.9.0}/MANIFEST.in +1 -1
- {fastconstmap-0.7.0/fastconstmap.egg-info → fastconstmap-0.9.0}/PKG-INFO +120 -27
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/README.md +119 -26
- fastconstmap-0.9.0/benchmarks/bench_batch.c +225 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/benchmarks/benchmark.py +62 -25
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/examples/example.py +10 -1
- fastconstmap-0.9.0/examples/shared_memory_example.py +57 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap/__init__.py +9 -4
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap/_fastconstmap.pyi +7 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0/fastconstmap.egg-info}/PKG-INFO +120 -27
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap.egg-info/SOURCES.txt +4 -1
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/pyproject.toml +1 -1
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/constmap.c +90 -2
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/constmap.h +20 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/fastconstmap_module.c +205 -133
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/tests/test_constmap.py +137 -1
- fastconstmap-0.9.0/tests/test_shared_memory.py +226 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/LICENSE +0 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap/py.typed +0 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap.egg-info/dependency_links.txt +0 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap.egg-info/top_level.txt +0 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/setup.cfg +0 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/setup.py +0 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/third_party/xxhash/LICENSE +0 -0
- {fastconstmap-0.7.0 → fastconstmap-0.9.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.9.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
|
|
@@ -31,7 +31,8 @@ Given a `dict[str, int]` at build time, you get back a lookup object that:
|
|
|
31
31
|
- answers a lookup in **one xxhash call plus three array reads**,
|
|
32
32
|
- is **immutable** and **serializable** to bytes / a file,
|
|
33
33
|
- exposes both a single-key API (`m[key]`) and a **batched** API
|
|
34
|
-
(`m.get_many([...])`) that amortises Python-C
|
|
34
|
+
(`m.get_many([...])`, `m.get_many_into([...], buf)`) that amortises Python-C
|
|
35
|
+
call overhead and overlaps the memory accesses of a whole block of keys.
|
|
35
36
|
|
|
36
37
|
This package is a C port of the Go library
|
|
37
38
|
[`github.com/lemire/constmap`][constmap]. It vendors
|
|
@@ -68,6 +69,12 @@ vm.get("grape", -1) # -> -1
|
|
|
68
69
|
vm["grape"] # raises KeyError
|
|
69
70
|
vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
|
|
70
71
|
|
|
72
|
+
# Batches can also be written straight into a buffer you own — no list of
|
|
73
|
+
# Python ints is built.
|
|
74
|
+
import array
|
|
75
|
+
out = array.array("Q", [0]) * 3
|
|
76
|
+
m.get_many_into(["apple", "banana", "cherry"], out) # -> 3; out == [100, 200, 300]
|
|
77
|
+
|
|
71
78
|
# Either kind can be saved and loaded.
|
|
72
79
|
m.save("mymap.cmap")
|
|
73
80
|
m2 = ConstMap.load("mymap.cmap")
|
|
@@ -89,6 +96,87 @@ m3 = ConstMap.from_bytes(blob)
|
|
|
89
96
|
The false-positive rate of `VerifiedConstMap` (a missing key wrongly
|
|
90
97
|
reported as present) is roughly 2⁻⁶⁴, which is negligible in practice.
|
|
91
98
|
|
|
99
|
+
## Batched lookups
|
|
100
|
+
|
|
101
|
+
`get_many` takes an iterable of keys and returns a list of values, in the same
|
|
102
|
+
order:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
cm.get_many(["apple", "banana", "cherry"]) # -> [100, 200, 300]
|
|
106
|
+
vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`get_many_into` is the same lookup writing 64-bit words into a buffer you own,
|
|
110
|
+
so a repeated batch allocates nothing at all — no list, and no Python `int` per
|
|
111
|
+
key. The buffer can be an `array("Q")`, a numpy `uint64` array, a
|
|
112
|
+
`SharedMemory` block, or any other writable buffer of at least `8 * len(keys)`
|
|
113
|
+
bytes. It returns the number of values written:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import array
|
|
117
|
+
out = array.array("Q", [0]) * 4096
|
|
118
|
+
for batch in batches:
|
|
119
|
+
n = cm.get_many_into(batch, out)
|
|
120
|
+
use(out[:n])
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`VerifiedConstMap.get_many_into` writes `fastconstmap.NOT_FOUND` (`2**64 - 1`)
|
|
124
|
+
for a key that was not in the original mapping — a buffer of raw words has no
|
|
125
|
+
room for a Python default.
|
|
126
|
+
|
|
127
|
+
A batch is faster than a loop over single lookups because it hashes a block of
|
|
128
|
+
eight keys before gathering any values. That lets the three array reads of all
|
|
129
|
+
eight keys be in flight at once, instead of each key's loads waiting behind the
|
|
130
|
+
hashing of the key before it. A lookup is memory-latency bound as soon as the
|
|
131
|
+
map outgrows the last-level cache, which is where the gain comes from.
|
|
132
|
+
|
|
133
|
+
### Measured
|
|
134
|
+
|
|
135
|
+
At the C level, 2000-key batches against a 1,000,000-key map, ns/key, medians
|
|
136
|
+
of three runs (`benchmarks/bench_batch.c`). *Cold* rotates through 64 distinct
|
|
137
|
+
random batches so the cache lines the lookups touch are not already resident;
|
|
138
|
+
*hot* replays one batch, so the touched region stays cache-resident and hashing
|
|
139
|
+
dominates instead.
|
|
140
|
+
|
|
141
|
+
| | | loop over lookup | `lookup_many` | |
|
|
142
|
+
|---|---|---|---|---|
|
|
143
|
+
| **Apple M4 Max** | `ConstMap` cold | 5.5 | **3.8** | 30% |
|
|
144
|
+
| | `ConstMap` hot | 4.2 | **3.1** | 27% |
|
|
145
|
+
| | `VerifiedConstMap` cold | 9.6 | **7.8** | 19% |
|
|
146
|
+
| | `VerifiedConstMap` hot | 5.4 | **5.2** | 4% |
|
|
147
|
+
| **Xeon Gold 6548N** | `ConstMap` cold | 12.6 | **10.4** | 18% |
|
|
148
|
+
| | `ConstMap` hot | 6.3 | **5.0** | 21% |
|
|
149
|
+
| | `VerifiedConstMap` cold | 15.9 | 16.6 | −5% |
|
|
150
|
+
| | `VerifiedConstMap` hot | 7.9 | **6.7** | 15% |
|
|
151
|
+
|
|
152
|
+
The one case where batching does not pay is `VerifiedConstMap` in the cold
|
|
153
|
+
regime on the Xeon. That map probes two arrays per lookup, so a fresh batch is
|
|
154
|
+
bound by a memory latency the overlapping cannot hide. The Go original reports
|
|
155
|
+
the same result on the same processor.
|
|
156
|
+
|
|
157
|
+
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:
|
|
159
|
+
|
|
160
|
+
| | | 0.8.0 | 0.9.0 | `get_many_into` |
|
|
161
|
+
|---|---|---|---|---|
|
|
162
|
+
| **Apple M4 Max** | `ConstMap` cold | 30.7 | **20.6** | **14.2** |
|
|
163
|
+
| | `ConstMap` hot | 16.4 | **14.1** | **6.1** |
|
|
164
|
+
| | `VerifiedConstMap` cold | 35.5 | 35.3 | **23.9** |
|
|
165
|
+
| **Xeon Gold 6548N** | `ConstMap` cold | 34.9 | **25.5** | **16.5** |
|
|
166
|
+
| | `ConstMap` hot | 18.7 | **17.2** | **8.8** |
|
|
167
|
+
| | `VerifiedConstMap` cold | 37.6 | **31.9** | **23.3** |
|
|
168
|
+
|
|
169
|
+
What is left in the `get_many` column is the cost of boxing each value in a
|
|
170
|
+
Python `int` and building the list, which is why `get_many_into` — which does
|
|
171
|
+
neither — is another 30-60% faster again.
|
|
172
|
+
|
|
173
|
+
To reproduce:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
cc -O3 -std=c11 -Isrc benchmarks/bench_batch.c src/constmap.c -lm -o bench_batch
|
|
177
|
+
./bench_batch 1000000
|
|
178
|
+
```
|
|
179
|
+
|
|
92
180
|
## Keys and values
|
|
93
181
|
|
|
94
182
|
- **Keys** may be `str` or `bytes`. `str` is encoded as UTF-8 internally;
|
|
@@ -176,41 +264,46 @@ its memory independently of the source buffer.
|
|
|
176
264
|
|
|
177
265
|
## Benchmark
|
|
178
266
|
|
|
179
|
-
On an Apple
|
|
180
|
-
|
|
267
|
+
On an Apple M4 Max, with 1,000,000 string keys (`key-{i}-{hex}` shaped
|
|
268
|
+
strings). Lookups query every key exactly once, in random order, from a list
|
|
269
|
+
built in that order:
|
|
181
270
|
|
|
182
271
|
```
|
|
183
|
-
=== fastconstmap benchmark — n = 1,000,000 keys, python 3.14.
|
|
272
|
+
=== fastconstmap benchmark — n = 1,000,000 keys, python 3.14.5 ===
|
|
184
273
|
|
|
185
274
|
Construction:
|
|
186
|
-
ConstMap.__init__
|
|
187
|
-
VerifiedConstMap.__init__
|
|
188
|
-
dict(d)
|
|
275
|
+
ConstMap.__init__ 0.145 s
|
|
276
|
+
VerifiedConstMap.__init__ 0.142 s
|
|
277
|
+
dict(d) 0.004 s
|
|
189
278
|
|
|
190
279
|
Memory:
|
|
191
|
-
ConstMap.nbytes
|
|
192
|
-
VerifiedConstMap.nbytes
|
|
193
|
-
dict (table+keys+values)
|
|
194
|
-
ratio dict / ConstMap
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
ConstMap.get_many
|
|
205
|
-
|
|
280
|
+
ConstMap.nbytes 9,043,968 bytes (9.04 bytes/key)
|
|
281
|
+
VerifiedConstMap.nbytes 18,087,936 bytes (18.09 bytes/key)
|
|
282
|
+
dict (table+keys+values) 118,380,958 bytes (118.38 bytes/key)
|
|
283
|
+
ratio dict / ConstMap 13.1x
|
|
284
|
+
|
|
285
|
+
Single lookup, every key once in random order (1,000,000 ops):
|
|
286
|
+
dict[k] 336.1 ns/op (0.336 s total)
|
|
287
|
+
ConstMap[k] 95.2 ns/op (0.095 s total)
|
|
288
|
+
VerifiedConstMap[k] 112.3 ns/op (0.112 s total)
|
|
289
|
+
|
|
290
|
+
Batched lookup, 2000 × 2000:
|
|
291
|
+
dict comprehension (cold) 148.7 ns/op (0.30 ms/batch of 2000)
|
|
292
|
+
ConstMap.get_many (cold) 24.2 ns/op (0.05 ms/batch of 2000)
|
|
293
|
+
ConstMap.get_many (hot) 15.6 ns/op (0.03 ms/batch of 2000)
|
|
294
|
+
ConstMap.get_many_into (cold) 14.2 ns/op (0.03 ms/batch of 2000)
|
|
295
|
+
ConstMap.get_many_into (hot) 6.1 ns/op (0.01 ms/batch of 2000)
|
|
296
|
+
VerifiedConstMap.get_many (cold) 33.7 ns/op (0.07 ms/batch of 2000)
|
|
297
|
+
VerifiedConstMap.get_many_into (cold) 23.9 ns/op (0.05 ms/batch of 2000)
|
|
206
298
|
|
|
207
299
|
Serialization:
|
|
208
|
-
ConstMap.to_bytes
|
|
209
|
-
ConstMap.from_bytes
|
|
300
|
+
ConstMap.to_bytes 0.010 s (9,044,008 bytes)
|
|
301
|
+
ConstMap.from_bytes 0.010 s
|
|
210
302
|
```
|
|
211
303
|
|
|
212
|
-
|
|
213
|
-
up at once
|
|
304
|
+
The single-lookup numbers are dominated by Python call overhead, not by the
|
|
305
|
+
map: use `get_many` when you have an array of keys to look up at once, and
|
|
306
|
+
`get_many_into` when you have somewhere to put the results.
|
|
214
307
|
|
|
215
308
|
To reproduce:
|
|
216
309
|
|
|
@@ -13,7 +13,8 @@ Given a `dict[str, int]` at build time, you get back a lookup object that:
|
|
|
13
13
|
- answers a lookup in **one xxhash call plus three array reads**,
|
|
14
14
|
- is **immutable** and **serializable** to bytes / a file,
|
|
15
15
|
- exposes both a single-key API (`m[key]`) and a **batched** API
|
|
16
|
-
(`m.get_many([...])`) that amortises Python-C
|
|
16
|
+
(`m.get_many([...])`, `m.get_many_into([...], buf)`) that amortises Python-C
|
|
17
|
+
call overhead and overlaps the memory accesses of a whole block of keys.
|
|
17
18
|
|
|
18
19
|
This package is a C port of the Go library
|
|
19
20
|
[`github.com/lemire/constmap`][constmap]. It vendors
|
|
@@ -50,6 +51,12 @@ vm.get("grape", -1) # -> -1
|
|
|
50
51
|
vm["grape"] # raises KeyError
|
|
51
52
|
vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
|
|
52
53
|
|
|
54
|
+
# Batches can also be written straight into a buffer you own — no list of
|
|
55
|
+
# Python ints is built.
|
|
56
|
+
import array
|
|
57
|
+
out = array.array("Q", [0]) * 3
|
|
58
|
+
m.get_many_into(["apple", "banana", "cherry"], out) # -> 3; out == [100, 200, 300]
|
|
59
|
+
|
|
53
60
|
# Either kind can be saved and loaded.
|
|
54
61
|
m.save("mymap.cmap")
|
|
55
62
|
m2 = ConstMap.load("mymap.cmap")
|
|
@@ -71,6 +78,87 @@ m3 = ConstMap.from_bytes(blob)
|
|
|
71
78
|
The false-positive rate of `VerifiedConstMap` (a missing key wrongly
|
|
72
79
|
reported as present) is roughly 2⁻⁶⁴, which is negligible in practice.
|
|
73
80
|
|
|
81
|
+
## Batched lookups
|
|
82
|
+
|
|
83
|
+
`get_many` takes an iterable of keys and returns a list of values, in the same
|
|
84
|
+
order:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
cm.get_many(["apple", "banana", "cherry"]) # -> [100, 200, 300]
|
|
88
|
+
vm.get_many(["banana", "grape"], default=-1) # -> [200, -1]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`get_many_into` is the same lookup writing 64-bit words into a buffer you own,
|
|
92
|
+
so a repeated batch allocates nothing at all — no list, and no Python `int` per
|
|
93
|
+
key. The buffer can be an `array("Q")`, a numpy `uint64` array, a
|
|
94
|
+
`SharedMemory` block, or any other writable buffer of at least `8 * len(keys)`
|
|
95
|
+
bytes. It returns the number of values written:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import array
|
|
99
|
+
out = array.array("Q", [0]) * 4096
|
|
100
|
+
for batch in batches:
|
|
101
|
+
n = cm.get_many_into(batch, out)
|
|
102
|
+
use(out[:n])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`VerifiedConstMap.get_many_into` writes `fastconstmap.NOT_FOUND` (`2**64 - 1`)
|
|
106
|
+
for a key that was not in the original mapping — a buffer of raw words has no
|
|
107
|
+
room for a Python default.
|
|
108
|
+
|
|
109
|
+
A batch is faster than a loop over single lookups because it hashes a block of
|
|
110
|
+
eight keys before gathering any values. That lets the three array reads of all
|
|
111
|
+
eight keys be in flight at once, instead of each key's loads waiting behind the
|
|
112
|
+
hashing of the key before it. A lookup is memory-latency bound as soon as the
|
|
113
|
+
map outgrows the last-level cache, which is where the gain comes from.
|
|
114
|
+
|
|
115
|
+
### Measured
|
|
116
|
+
|
|
117
|
+
At the C level, 2000-key batches against a 1,000,000-key map, ns/key, medians
|
|
118
|
+
of three runs (`benchmarks/bench_batch.c`). *Cold* rotates through 64 distinct
|
|
119
|
+
random batches so the cache lines the lookups touch are not already resident;
|
|
120
|
+
*hot* replays one batch, so the touched region stays cache-resident and hashing
|
|
121
|
+
dominates instead.
|
|
122
|
+
|
|
123
|
+
| | | loop over lookup | `lookup_many` | |
|
|
124
|
+
|---|---|---|---|---|
|
|
125
|
+
| **Apple M4 Max** | `ConstMap` cold | 5.5 | **3.8** | 30% |
|
|
126
|
+
| | `ConstMap` hot | 4.2 | **3.1** | 27% |
|
|
127
|
+
| | `VerifiedConstMap` cold | 9.6 | **7.8** | 19% |
|
|
128
|
+
| | `VerifiedConstMap` hot | 5.4 | **5.2** | 4% |
|
|
129
|
+
| **Xeon Gold 6548N** | `ConstMap` cold | 12.6 | **10.4** | 18% |
|
|
130
|
+
| | `ConstMap` hot | 6.3 | **5.0** | 21% |
|
|
131
|
+
| | `VerifiedConstMap` cold | 15.9 | 16.6 | −5% |
|
|
132
|
+
| | `VerifiedConstMap` hot | 7.9 | **6.7** | 15% |
|
|
133
|
+
|
|
134
|
+
The one case where batching does not pay is `VerifiedConstMap` in the cold
|
|
135
|
+
regime on the Xeon. That map probes two arrays per lookup, so a fresh batch is
|
|
136
|
+
bound by a memory latency the overlapping cannot hide. The Go original reports
|
|
137
|
+
the same result on the same processor.
|
|
138
|
+
|
|
139
|
+
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:
|
|
141
|
+
|
|
142
|
+
| | | 0.8.0 | 0.9.0 | `get_many_into` |
|
|
143
|
+
|---|---|---|---|---|
|
|
144
|
+
| **Apple M4 Max** | `ConstMap` cold | 30.7 | **20.6** | **14.2** |
|
|
145
|
+
| | `ConstMap` hot | 16.4 | **14.1** | **6.1** |
|
|
146
|
+
| | `VerifiedConstMap` cold | 35.5 | 35.3 | **23.9** |
|
|
147
|
+
| **Xeon Gold 6548N** | `ConstMap` cold | 34.9 | **25.5** | **16.5** |
|
|
148
|
+
| | `ConstMap` hot | 18.7 | **17.2** | **8.8** |
|
|
149
|
+
| | `VerifiedConstMap` cold | 37.6 | **31.9** | **23.3** |
|
|
150
|
+
|
|
151
|
+
What is left in the `get_many` column is the cost of boxing each value in a
|
|
152
|
+
Python `int` and building the list, which is why `get_many_into` — which does
|
|
153
|
+
neither — is another 30-60% faster again.
|
|
154
|
+
|
|
155
|
+
To reproduce:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
cc -O3 -std=c11 -Isrc benchmarks/bench_batch.c src/constmap.c -lm -o bench_batch
|
|
159
|
+
./bench_batch 1000000
|
|
160
|
+
```
|
|
161
|
+
|
|
74
162
|
## Keys and values
|
|
75
163
|
|
|
76
164
|
- **Keys** may be `str` or `bytes`. `str` is encoded as UTF-8 internally;
|
|
@@ -158,41 +246,46 @@ its memory independently of the source buffer.
|
|
|
158
246
|
|
|
159
247
|
## Benchmark
|
|
160
248
|
|
|
161
|
-
On an Apple
|
|
162
|
-
|
|
249
|
+
On an Apple M4 Max, with 1,000,000 string keys (`key-{i}-{hex}` shaped
|
|
250
|
+
strings). Lookups query every key exactly once, in random order, from a list
|
|
251
|
+
built in that order:
|
|
163
252
|
|
|
164
253
|
```
|
|
165
|
-
=== fastconstmap benchmark — n = 1,000,000 keys, python 3.14.
|
|
254
|
+
=== fastconstmap benchmark — n = 1,000,000 keys, python 3.14.5 ===
|
|
166
255
|
|
|
167
256
|
Construction:
|
|
168
|
-
ConstMap.__init__
|
|
169
|
-
VerifiedConstMap.__init__
|
|
170
|
-
dict(d)
|
|
257
|
+
ConstMap.__init__ 0.145 s
|
|
258
|
+
VerifiedConstMap.__init__ 0.142 s
|
|
259
|
+
dict(d) 0.004 s
|
|
171
260
|
|
|
172
261
|
Memory:
|
|
173
|
-
ConstMap.nbytes
|
|
174
|
-
VerifiedConstMap.nbytes
|
|
175
|
-
dict (table+keys+values)
|
|
176
|
-
ratio dict / ConstMap
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
ConstMap.get_many
|
|
187
|
-
|
|
262
|
+
ConstMap.nbytes 9,043,968 bytes (9.04 bytes/key)
|
|
263
|
+
VerifiedConstMap.nbytes 18,087,936 bytes (18.09 bytes/key)
|
|
264
|
+
dict (table+keys+values) 118,380,958 bytes (118.38 bytes/key)
|
|
265
|
+
ratio dict / ConstMap 13.1x
|
|
266
|
+
|
|
267
|
+
Single lookup, every key once in random order (1,000,000 ops):
|
|
268
|
+
dict[k] 336.1 ns/op (0.336 s total)
|
|
269
|
+
ConstMap[k] 95.2 ns/op (0.095 s total)
|
|
270
|
+
VerifiedConstMap[k] 112.3 ns/op (0.112 s total)
|
|
271
|
+
|
|
272
|
+
Batched lookup, 2000 × 2000:
|
|
273
|
+
dict comprehension (cold) 148.7 ns/op (0.30 ms/batch of 2000)
|
|
274
|
+
ConstMap.get_many (cold) 24.2 ns/op (0.05 ms/batch of 2000)
|
|
275
|
+
ConstMap.get_many (hot) 15.6 ns/op (0.03 ms/batch of 2000)
|
|
276
|
+
ConstMap.get_many_into (cold) 14.2 ns/op (0.03 ms/batch of 2000)
|
|
277
|
+
ConstMap.get_many_into (hot) 6.1 ns/op (0.01 ms/batch of 2000)
|
|
278
|
+
VerifiedConstMap.get_many (cold) 33.7 ns/op (0.07 ms/batch of 2000)
|
|
279
|
+
VerifiedConstMap.get_many_into (cold) 23.9 ns/op (0.05 ms/batch of 2000)
|
|
188
280
|
|
|
189
281
|
Serialization:
|
|
190
|
-
ConstMap.to_bytes
|
|
191
|
-
ConstMap.from_bytes
|
|
282
|
+
ConstMap.to_bytes 0.010 s (9,044,008 bytes)
|
|
283
|
+
ConstMap.from_bytes 0.010 s
|
|
192
284
|
```
|
|
193
285
|
|
|
194
|
-
|
|
195
|
-
up at once
|
|
286
|
+
The single-lookup numbers are dominated by Python call overhead, not by the
|
|
287
|
+
map: use `get_many` when you have an array of keys to look up at once, and
|
|
288
|
+
`get_many_into` when you have somewhere to put the results.
|
|
196
289
|
|
|
197
290
|
To reproduce:
|
|
198
291
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* bench_batch — C-level microbenchmark for the batched lookups.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Go original's batch_bench_test.go (constmap v1.1.0): a 2000-key
|
|
5
|
+
* batch against a 1,000,000-key map, reported in ns/key.
|
|
6
|
+
*
|
|
7
|
+
* cold — rotates through 64 distinct random batches, so the cache lines the
|
|
8
|
+
* lookups touch are not already resident and memory latency shows.
|
|
9
|
+
* hot — replays one batch, so the touched region of the array stays
|
|
10
|
+
* cache-resident and hashing dominates instead.
|
|
11
|
+
*
|
|
12
|
+
* Build:
|
|
13
|
+
* cc -O3 -std=c11 -Isrc benchmarks/bench_batch.c src/constmap.c -lm -o bench_batch
|
|
14
|
+
*/
|
|
15
|
+
/* clock_gettime is POSIX; -std=c11 alone does not expose it. */
|
|
16
|
+
#define _POSIX_C_SOURCE 200809L
|
|
17
|
+
|
|
18
|
+
#include "constmap.h"
|
|
19
|
+
|
|
20
|
+
#include <stdint.h>
|
|
21
|
+
#include <stdio.h>
|
|
22
|
+
#include <stdlib.h>
|
|
23
|
+
#include <string.h>
|
|
24
|
+
#include <time.h>
|
|
25
|
+
|
|
26
|
+
#define MAP_N 1000000
|
|
27
|
+
#define BATCH_SIZE 2000
|
|
28
|
+
#define NUM_POOLS 64
|
|
29
|
+
#define REPEATS 2000
|
|
30
|
+
|
|
31
|
+
static uint64_t rng_state = 0x853c49e6748fea9bULL;
|
|
32
|
+
|
|
33
|
+
static uint64_t next_rand(void) {
|
|
34
|
+
rng_state ^= rng_state << 13;
|
|
35
|
+
rng_state ^= rng_state >> 7;
|
|
36
|
+
rng_state ^= rng_state << 17;
|
|
37
|
+
return rng_state;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static double now_sec(void) {
|
|
41
|
+
struct timespec ts;
|
|
42
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
43
|
+
return (double)ts.tv_sec + 1e-9 * (double)ts.tv_nsec;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Key text lives in one arena, in index order, exactly as a caller's key list
|
|
47
|
+
* would. Each batch clones the keys it draws into its own arena so that a
|
|
48
|
+
* lookup is not also charged for a scattered walk over the arena — that cost
|
|
49
|
+
* belongs to whatever produced the keys, not to the map. */
|
|
50
|
+
typedef struct {
|
|
51
|
+
char *arena;
|
|
52
|
+
fcm_key_t *keys;
|
|
53
|
+
size_t n;
|
|
54
|
+
} keyset_t;
|
|
55
|
+
|
|
56
|
+
static void keyset_free(keyset_t *ks) {
|
|
57
|
+
free(ks->arena);
|
|
58
|
+
free(ks->keys);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static int keyset_build(keyset_t *ks, size_t n) {
|
|
62
|
+
const size_t stride = 32;
|
|
63
|
+
ks->n = n;
|
|
64
|
+
ks->arena = malloc(n * stride);
|
|
65
|
+
ks->keys = malloc(n * sizeof(fcm_key_t));
|
|
66
|
+
if (!ks->arena || !ks->keys) return -1;
|
|
67
|
+
for (size_t i = 0; i < n; i++) {
|
|
68
|
+
char *p = ks->arena + i * stride;
|
|
69
|
+
int len = snprintf(p, stride, "key-%zu-%08x", i,
|
|
70
|
+
(unsigned)(next_rand() & 0x3FFFFFFF));
|
|
71
|
+
ks->keys[i].bytes = p;
|
|
72
|
+
ks->keys[i].len = (size_t)len;
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Draw BATCH_SIZE keys at random from `all`, cloned into a compact arena. */
|
|
78
|
+
static int batch_build(keyset_t *batch, const keyset_t *all) {
|
|
79
|
+
const size_t stride = 32;
|
|
80
|
+
batch->n = BATCH_SIZE;
|
|
81
|
+
batch->arena = malloc(BATCH_SIZE * stride);
|
|
82
|
+
batch->keys = malloc(BATCH_SIZE * sizeof(fcm_key_t));
|
|
83
|
+
if (!batch->arena || !batch->keys) return -1;
|
|
84
|
+
for (size_t i = 0; i < BATCH_SIZE; i++) {
|
|
85
|
+
size_t src = (size_t)(next_rand() % all->n);
|
|
86
|
+
char *p = batch->arena + i * stride;
|
|
87
|
+
memcpy(p, all->keys[src].bytes, all->keys[src].len);
|
|
88
|
+
batch->keys[i].bytes = p;
|
|
89
|
+
batch->keys[i].len = all->keys[src].len;
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static volatile uint64_t sink;
|
|
95
|
+
|
|
96
|
+
static void report(const char *label, double elapsed, size_t keys, double baseline) {
|
|
97
|
+
double ns = elapsed * 1e9 / (double)keys;
|
|
98
|
+
if (baseline > 0.0) {
|
|
99
|
+
printf(" %-34s %7.2f ns/key %+5.0f%%\n", label, ns,
|
|
100
|
+
100.0 * (baseline - ns) / baseline);
|
|
101
|
+
} else {
|
|
102
|
+
printf(" %-34s %7.2f ns/key\n", label, ns);
|
|
103
|
+
}
|
|
104
|
+
(void)keys;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
int main(int argc, char **argv) {
|
|
108
|
+
size_t map_n = argc > 1 ? (size_t)strtoull(argv[1], NULL, 10) : MAP_N;
|
|
109
|
+
|
|
110
|
+
keyset_t all;
|
|
111
|
+
if (keyset_build(&all, map_n) < 0) { fprintf(stderr, "oom\n"); return 1; }
|
|
112
|
+
uint64_t *values = malloc(map_n * sizeof(uint64_t));
|
|
113
|
+
if (!values) { fprintf(stderr, "oom\n"); return 1; }
|
|
114
|
+
for (size_t i = 0; i < map_n; i++) values[i] = i;
|
|
115
|
+
|
|
116
|
+
fcm_constmap_t cm;
|
|
117
|
+
fcm_verified_constmap_t vm;
|
|
118
|
+
if (fcm_constmap_new(&cm, all.keys, values, map_n) != FCM_OK) {
|
|
119
|
+
fprintf(stderr, "construction failed\n"); return 1;
|
|
120
|
+
}
|
|
121
|
+
if (fcm_verified_constmap_new(&vm, all.keys, values, map_n) != FCM_OK) {
|
|
122
|
+
fprintf(stderr, "verified construction failed\n"); return 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
keyset_t pools[NUM_POOLS];
|
|
126
|
+
for (int p = 0; p < NUM_POOLS; p++) {
|
|
127
|
+
if (batch_build(&pools[p], &all) < 0) { fprintf(stderr, "oom\n"); return 1; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
uint64_t *out = malloc(BATCH_SIZE * sizeof(uint64_t));
|
|
131
|
+
if (!out) { fprintf(stderr, "oom\n"); return 1; }
|
|
132
|
+
|
|
133
|
+
printf("=== fastconstmap batched lookups — %zu keys, batches of %d ===\n\n",
|
|
134
|
+
map_n, BATCH_SIZE);
|
|
135
|
+
|
|
136
|
+
double t0, loop_ns;
|
|
137
|
+
size_t total = (size_t)REPEATS * BATCH_SIZE;
|
|
138
|
+
|
|
139
|
+
/* ---- ConstMap, cold ---- */
|
|
140
|
+
printf("ConstMap:\n");
|
|
141
|
+
t0 = now_sec();
|
|
142
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
143
|
+
const keyset_t *q = &pools[i % NUM_POOLS];
|
|
144
|
+
for (size_t j = 0; j < q->n; j++)
|
|
145
|
+
out[j] = fcm_constmap_lookup(&cm, q->keys[j].bytes, q->keys[j].len);
|
|
146
|
+
sink = out[0];
|
|
147
|
+
}
|
|
148
|
+
loop_ns = now_sec() - t0;
|
|
149
|
+
report("loop over lookup (cold)", loop_ns, total, 0.0);
|
|
150
|
+
|
|
151
|
+
t0 = now_sec();
|
|
152
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
153
|
+
const keyset_t *q = &pools[i % NUM_POOLS];
|
|
154
|
+
fcm_constmap_lookup_many(&cm, q->keys, q->n, out);
|
|
155
|
+
sink = out[0];
|
|
156
|
+
}
|
|
157
|
+
report("lookup_many (cold)", now_sec() - t0, total,
|
|
158
|
+
loop_ns * 1e9 / (double)total);
|
|
159
|
+
|
|
160
|
+
/* ---- ConstMap, hot ---- */
|
|
161
|
+
const keyset_t *hot = &pools[0];
|
|
162
|
+
t0 = now_sec();
|
|
163
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
164
|
+
for (size_t j = 0; j < hot->n; j++)
|
|
165
|
+
out[j] = fcm_constmap_lookup(&cm, hot->keys[j].bytes, hot->keys[j].len);
|
|
166
|
+
sink = out[0];
|
|
167
|
+
}
|
|
168
|
+
loop_ns = now_sec() - t0;
|
|
169
|
+
report("loop over lookup (hot)", loop_ns, total, 0.0);
|
|
170
|
+
|
|
171
|
+
t0 = now_sec();
|
|
172
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
173
|
+
fcm_constmap_lookup_many(&cm, hot->keys, hot->n, out);
|
|
174
|
+
sink = out[0];
|
|
175
|
+
}
|
|
176
|
+
report("lookup_many (hot)", now_sec() - t0, total,
|
|
177
|
+
loop_ns * 1e9 / (double)total);
|
|
178
|
+
|
|
179
|
+
/* ---- VerifiedConstMap, cold ---- */
|
|
180
|
+
printf("\nVerifiedConstMap:\n");
|
|
181
|
+
t0 = now_sec();
|
|
182
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
183
|
+
const keyset_t *q = &pools[i % NUM_POOLS];
|
|
184
|
+
for (size_t j = 0; j < q->n; j++)
|
|
185
|
+
out[j] = fcm_verified_constmap_lookup(&vm, q->keys[j].bytes, q->keys[j].len);
|
|
186
|
+
sink = out[0];
|
|
187
|
+
}
|
|
188
|
+
loop_ns = now_sec() - t0;
|
|
189
|
+
report("loop over lookup (cold)", loop_ns, total, 0.0);
|
|
190
|
+
|
|
191
|
+
t0 = now_sec();
|
|
192
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
193
|
+
const keyset_t *q = &pools[i % NUM_POOLS];
|
|
194
|
+
fcm_verified_constmap_lookup_many(&vm, q->keys, q->n, out);
|
|
195
|
+
sink = out[0];
|
|
196
|
+
}
|
|
197
|
+
report("lookup_many (cold)", now_sec() - t0, total,
|
|
198
|
+
loop_ns * 1e9 / (double)total);
|
|
199
|
+
|
|
200
|
+
/* ---- VerifiedConstMap, hot ---- */
|
|
201
|
+
t0 = now_sec();
|
|
202
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
203
|
+
for (size_t j = 0; j < hot->n; j++)
|
|
204
|
+
out[j] = fcm_verified_constmap_lookup(&vm, hot->keys[j].bytes, hot->keys[j].len);
|
|
205
|
+
sink = out[0];
|
|
206
|
+
}
|
|
207
|
+
loop_ns = now_sec() - t0;
|
|
208
|
+
report("loop over lookup (hot)", loop_ns, total, 0.0);
|
|
209
|
+
|
|
210
|
+
t0 = now_sec();
|
|
211
|
+
for (int i = 0; i < REPEATS; i++) {
|
|
212
|
+
fcm_verified_constmap_lookup_many(&vm, hot->keys, hot->n, out);
|
|
213
|
+
sink = out[0];
|
|
214
|
+
}
|
|
215
|
+
report("lookup_many (hot)", now_sec() - t0, total,
|
|
216
|
+
loop_ns * 1e9 / (double)total);
|
|
217
|
+
|
|
218
|
+
for (int p = 0; p < NUM_POOLS; p++) keyset_free(&pools[p]);
|
|
219
|
+
keyset_free(&all);
|
|
220
|
+
free(values);
|
|
221
|
+
free(out);
|
|
222
|
+
fcm_constmap_free(&cm);
|
|
223
|
+
fcm_verified_constmap_free(&vm);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|