pack-bitmap: cache object positions during fill

The previous commits removed some redundant work from bitmap generation by avoiding unnecessary tree recursion and by reusing selected bitmaps that have already been computed. Even with those changes in place, there is still an extremely hot path from `fill_bitmap_commit()` and `fill_bitmap_tree()` to translate object IDs into their corresponding bit positions in order to generate their bitmaps. In a small repository, this overhead is not significant. However, in a very large repository (e.g., the one that we have been using as a benchmark over the past several commits with ~57M total objects), the overhead of locating object bit positions (often repeatedly) adds up significantly. Combat this by adding a small, direct-mapped cache to the bitmap writer which maps object IDs to their corresponding bit positions. Size the cache according to the number of objects being written, with fixed lower and upper bounds so small repositories do not pay for a large table and large repositories can avoid most repeated packlist and MIDX lookups. On my machine with (a somewhat outdated) GCC 15.2.0, each entry in the cache is 40 bytes wide: $ pahole -C bitmap_pos_cache_entry pack-bitmap-write.o struct bitmap_pos_cache_entry { struct object_id oid; /* 0 36 */ uint32_t pos; /* 36 4 */ /* size: 40, cachelines: 1, members: 2 */ /* last cacheline: 40 bytes */ }; , and we will allocate up to 2^21 entries for a maximum total of 80 MiB of cache overhead. In our example repository from above and in earlier commits, this results in a ~9.4% reduction in runtime relative to the previous commit: +------------------+-------------+-------------+---------------------+ | | HEAD^ | HEAD | Delta | +------------------+-------------+-------------+---------------------+ | elapsed | 324.8 s | 294.1 s | -30.7 s (-9.4%) | | cycles | 1,508.6 B | 1,365.5 B | -143.0 B (-9.5%) | | instructions | 1,436.6 B | 1,389.8 B | -46.9 B (-3.3%) | | CPI | 1.050 | 0.983 | -0.068 (-6.4%) | +------------------+-------------+-------------+---------------------+ When generating bitmaps on this repository (to produce the above timings), the cache grew to its maximum size of 80 MiB, and resulted in 1.024B cache hits and 59.957M cache misses. Signed-off-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Taylor Blau committed May 27, 2026 at 15:56 UTC c720bbcc53f223236220c7a879f0a0e73e5d3739
2 files changed +94 -1
pack-bitmap-write.c
+87 -1
@@ -89,6 +89,7 @@ void bitmap_writer_free(struct bitmap_writer *writer)
89 ewah_free(writer->tags);
90
91 kh_destroy_oid_map(writer->bitmaps);
92 + free(writer->pos_cache);
93
94 kh_foreach_value(writer->pseudo_merge_commits, idx,
95 free_pseudo_merge_commit_idx(idx));
@@ -213,15 +214,92 @@ void bitmap_writer_push_commit(struct bitmap_writer *writer,
214 writer->selected_nr++;
215 }
216
217 +struct bitmap_pos_cache_entry {
218 + struct object_id oid;
219 + uint32_t pos;
220 +};
221 +
222 +#define BITMAP_POS_MIN_CACHE_SIZE (1U << 10)
223 +#define BITMAP_POS_MAX_CACHE_SIZE (1U << 21)
224 +#define BITMAP_POS_CACHE_VALID (1U << 31)
225 +
226 +static void bitmap_writer_init_pos_cache(struct bitmap_writer *writer)
227 +{
228 + if (writer->pos_cache)
229 + return;
230 +
231 + writer->pos_cache_nr = BITMAP_POS_MIN_CACHE_SIZE;
232 +
233 + while (writer->pos_cache_nr < writer->to_pack->nr_objects &&
234 + writer->pos_cache_nr < BITMAP_POS_MAX_CACHE_SIZE)
235 + writer->pos_cache_nr <<= 1;
236 +
237 + CALLOC_ARRAY(writer->pos_cache, writer->pos_cache_nr);
238 +}
239 +
240 +static size_t bitmap_writer_pos_cache_slot(struct bitmap_writer *writer,
241 + const struct object_id *oid)
242 +{
243 + return oidhash(oid) & (writer->pos_cache_nr - 1);
244 +}
245 +
246 +static bool bitmap_writer_pos_cache_valid(struct bitmap_writer *writer,
247 + size_t slot)
248 +{
249 + return !!(writer->pos_cache[slot].pos & BITMAP_POS_CACHE_VALID);
250 +}
251 +
252 +static int find_cached_object_pos(struct bitmap_writer *writer,
253 + const struct object_id *oid, uint32_t *pos)
254 +{
255 + size_t slot = bitmap_writer_pos_cache_slot(writer, oid);
256 +
257 + if (bitmap_writer_pos_cache_valid(writer, slot) &&
258 + oideq(&writer->pos_cache[slot].oid, oid)) {
259 + writer->pos_cache_hits++;
260 + *pos = writer->pos_cache[slot].pos & ~BITMAP_POS_CACHE_VALID;
261 + return 1;
262 + }
263 +
264 + writer->pos_cache_misses++;
265 + return 0;
266 +}
267 +
268 +static uint32_t store_cached_object_pos(struct bitmap_writer *writer,
269 + const struct object_id *oid,
270 + uint32_t pos)
271 +{
272 + size_t slot;
273 +
274 + if (pos & BITMAP_POS_CACHE_VALID)
275 + return pos; /* too large to cache */
276 +
277 + slot = bitmap_writer_pos_cache_slot(writer, oid);
278 +
279 + oidcpy(&writer->pos_cache[slot].oid, oid);
280 + writer->pos_cache[slot].pos = pos | BITMAP_POS_CACHE_VALID;
281 +
282 + return pos;
283 +}
284 +
285 static uint32_t find_object_pos(struct bitmap_writer *writer,
286 const struct object_id *oid, int *found)
287 {
288 struct object_entry *entry;
289 uint32_t pos;
290
291 + bitmap_writer_init_pos_cache(writer);
292 +
293 + if (find_cached_object_pos(writer, oid, &pos)) {
294 + if (found)
295 + *found = 1;
296 + return pos;
297 + }
298 +
299 entry = packlist_find(writer->to_pack, oid);
300 if (entry) {
301 uint32_t base_objects = 0;
302 +
303 if (writer->midx)
304 base_objects = writer->midx->num_objects +
305 writer->midx->num_objects_in_base;
@@ -239,7 +317,7 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
317
318 if (found)
319 *found = 1;
242 - return pos;
320 + return store_cached_object_pos(writer, oid, pos);
321
322 missing:
323 if (found)
@@ -662,6 +740,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
740 writer->progress = start_progress(writer->repo,
741 "Building bitmaps",
742 writer->selected_nr);
743 +
744 + writer->pos_cache_hits = 0;
745 + writer->pos_cache_misses = 0;
746 +
747 trace2_region_enter("pack-bitmap-write", "building_bitmaps_total",
748 writer->repo);
749
@@ -726,6 +808,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
808 trace2_data_intmax("pack-bitmap-write", writer->repo,
809 "fill_bitmap_commit_found_ancestor_nr",
810 fill_bitmap_commit_found_ancestor_nr);
811 + trace2_data_intmax("pack-bitmap-write", writer->repo,
812 + "bitmap_pos_cache_hits", writer->pos_cache_hits);
813 + trace2_data_intmax("pack-bitmap-write", writer->repo,
814 + "bitmap_pos_cache_misses", writer->pos_cache_misses);
815
816 stop_progress(&writer->progress);
817
pack-bitmap.h
+7
@@ -132,6 +132,8 @@ int bitmap_has_oid_in_uninteresting(struct bitmap_index *, const struct object_i
132
133 off_t get_disk_usage_from_bitmap(struct bitmap_index *, struct rev_info *);
134
135 +struct bitmap_pos_cache_entry;
136 +
137 struct bitmap_writer {
138 struct repository *repo;
139 struct ewah_bitmap *commits;
@@ -143,6 +145,11 @@ struct bitmap_writer {
145 struct packing_data *to_pack;
146 struct multi_pack_index *midx; /* if appending to a MIDX chain */
147
148 + struct bitmap_pos_cache_entry *pos_cache;
149 + size_t pos_cache_nr;
150 + uint64_t pos_cache_hits;
151 + uint64_t pos_cache_misses;
152 +
153 struct bitmapped_commit *selected;
154 unsigned int selected_nr, selected_alloc;
155