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.
Files changed (25) hide show
  1. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/MANIFEST.in +1 -1
  2. {fastconstmap-0.7.0/fastconstmap.egg-info → fastconstmap-0.9.0}/PKG-INFO +120 -27
  3. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/README.md +119 -26
  4. fastconstmap-0.9.0/benchmarks/bench_batch.c +225 -0
  5. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/benchmarks/benchmark.py +62 -25
  6. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/examples/example.py +10 -1
  7. fastconstmap-0.9.0/examples/shared_memory_example.py +57 -0
  8. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap/__init__.py +9 -4
  9. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap/_fastconstmap.pyi +7 -0
  10. {fastconstmap-0.7.0 → fastconstmap-0.9.0/fastconstmap.egg-info}/PKG-INFO +120 -27
  11. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap.egg-info/SOURCES.txt +4 -1
  12. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/pyproject.toml +1 -1
  13. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/constmap.c +90 -2
  14. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/constmap.h +20 -0
  15. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/fastconstmap_module.c +205 -133
  16. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/tests/test_constmap.py +137 -1
  17. fastconstmap-0.9.0/tests/test_shared_memory.py +226 -0
  18. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/LICENSE +0 -0
  19. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap/py.typed +0 -0
  20. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap.egg-info/dependency_links.txt +0 -0
  21. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/fastconstmap.egg-info/top_level.txt +0 -0
  22. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/setup.cfg +0 -0
  23. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/setup.py +0 -0
  24. {fastconstmap-0.7.0 → fastconstmap-0.9.0}/src/third_party/xxhash/LICENSE +0 -0
  25. {fastconstmap-0.7.0 → fastconstmap-0.9.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastconstmap
3
- Version: 0.7.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 call overhead.
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 M-series CPU, with 1,000,000 string keys
180
- (`key-{i}-{hex}` shaped strings):
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.3 ===
272
+ === fastconstmap benchmark — n = 1,000,000 keys, python 3.14.5 ===
184
273
 
185
274
  Construction:
186
- ConstMap.__init__ 0.145 s
187
- VerifiedConstMap.__init__ 0.129 s
188
- dict(d) 0.005 s
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 9,043,968 bytes (9.04 bytes/key)
192
- VerifiedConstMap.nbytes 18,087,936 bytes (18.09 bytes/key)
193
- dict (table+keys+values) 118,380,958 bytes (118.38 bytes/key)
194
- ratio dict / ConstMap 13.1x
195
-
196
-
197
- Single lookup, 2,000,000 ops:
198
- dict[k] 397.7 ns/op (0.795 s total)
199
- ConstMap[k] 179.3 ns/op (0.359 s total)
200
- VerifiedConstMap[k] 213.2 ns/op (0.426 s total)
201
-
202
- Batched lookup, 2000 × 1024:
203
- dict comprehension 31.9 ns/op (0.03 ms/batch of 1024)
204
- ConstMap.get_many 14.6 ns/op (0.01 ms/batch of 1024)
205
- VerifiedConstMap.get_many 16.5 ns/op (0.02 ms/batch of 1024)
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 0.009 s (9,044,004 bytes)
209
- ConstMap.from_bytes 0.009 s
300
+ ConstMap.to_bytes 0.010 s (9,044,008 bytes)
301
+ ConstMap.from_bytes 0.010 s
210
302
  ```
211
303
 
212
- For better performance use `get_many` when you have an array of keys to look
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 call overhead.
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 M-series CPU, with 1,000,000 string keys
162
- (`key-{i}-{hex}` shaped strings):
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.3 ===
254
+ === fastconstmap benchmark — n = 1,000,000 keys, python 3.14.5 ===
166
255
 
167
256
  Construction:
168
- ConstMap.__init__ 0.145 s
169
- VerifiedConstMap.__init__ 0.129 s
170
- dict(d) 0.005 s
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 9,043,968 bytes (9.04 bytes/key)
174
- VerifiedConstMap.nbytes 18,087,936 bytes (18.09 bytes/key)
175
- dict (table+keys+values) 118,380,958 bytes (118.38 bytes/key)
176
- ratio dict / ConstMap 13.1x
177
-
178
-
179
- Single lookup, 2,000,000 ops:
180
- dict[k] 397.7 ns/op (0.795 s total)
181
- ConstMap[k] 179.3 ns/op (0.359 s total)
182
- VerifiedConstMap[k] 213.2 ns/op (0.426 s total)
183
-
184
- Batched lookup, 2000 × 1024:
185
- dict comprehension 31.9 ns/op (0.03 ms/batch of 1024)
186
- ConstMap.get_many 14.6 ns/op (0.01 ms/batch of 1024)
187
- VerifiedConstMap.get_many 16.5 ns/op (0.02 ms/batch of 1024)
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 0.009 s (9,044,004 bytes)
191
- ConstMap.from_bytes 0.009 s
282
+ ConstMap.to_bytes 0.010 s (9,044,008 bytes)
283
+ ConstMap.from_bytes 0.010 s
192
284
  ```
193
285
 
194
- For better performance use `get_many` when you have an array of keys to look
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
+ }