reftable/merged: make subiters own their records

For each subiterator, the merged table needs to track their current record. This record is owned by the priority queue though instead of by the merged iterator. This is not optimal performance-wise. For one, we need to move around records whenever we add or remove a record from the priority queue. Thus, the bigger the entries the more bytes we need to copy around. And compared to pointers, a reftable record is rather on the bigger side. The other issue is that this makes it harder to reuse the records. Refactor the code so that the merged iterator tracks ownership of the records per-subiter. Instead of having records in the priority queue, we can now use mere pointers to the per-subiter records. This also allows us to swap records between the caller and the per-subiter record instead of doing an actual copy via `reftable_record_copy_from()`, which removes the need to release the caller-provided record. This results in a noticeable speedup when iterating through many refs. The following benchmark iterates through 1 million refs: Benchmark 1: show-ref: single matching ref (revision = HEAD~) Time (mean ± σ): 145.5 ms ± 4.5 ms [User: 142.5 ms, System: 2.8 ms] Range (min … max): 141.3 ms … 177.0 ms 1000 runs Benchmark 2: show-ref: single matching ref (revision = HEAD) Time (mean ± σ): 139.0 ms ± 4.7 ms [User: 136.1 ms, System: 2.8 ms] Range (min … max): 134.2 ms … 182.2 ms 1000 runs Summary show-ref: single matching ref (revision = HEAD) ran 1.05 ± 0.05 times faster than show-ref: single matching ref (revision = HEAD~) This refactoring also allows a subsequent refactoring where we start reusing memory allocated by the reftable records because we do not need to release the caller-provided record anymore. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Mar 4, 2024 at 11:48 UTC bb2d6be4c1010af3f92a7f4a6feaab957e0e906b
4 files changed +49 -56
reftable/merged.c
+28 -26
@@ -17,8 +17,13 @@ https://developers.google.com/open-source/licenses/bsd
17 #include "reftable-error.h"
18 #include "system.h"
19
20 +struct merged_subiter {
21 + struct reftable_iterator iter;
22 + struct reftable_record rec;
23 +};
24 +
25 struct merged_iter {
21 - struct reftable_iterator *stack;
26 + struct merged_subiter *subiters;
27 struct merged_iter_pqueue pq;
28 uint32_t hash_id;
29 size_t stack_len;
@@ -32,16 +37,18 @@ static int merged_iter_init(struct merged_iter *mi)
37 for (size_t i = 0; i < mi->stack_len; i++) {
38 struct pq_entry e = {
39 .index = i,
40 + .rec = &mi->subiters[i].rec,
41 };
42 int err;
43
38 - reftable_record_init(&e.rec, mi->typ);
39 - err = iterator_next(&mi->stack[i], &e.rec);
44 + reftable_record_init(&mi->subiters[i].rec, mi->typ);
45 + err = iterator_next(&mi->subiters[i].iter,
46 + &mi->subiters[i].rec);
47 if (err < 0)
48 return err;
49 if (err > 0) {
43 - reftable_iterator_destroy(&mi->stack[i]);
44 - reftable_record_release(&e.rec);
50 + reftable_iterator_destroy(&mi->subiters[i].iter);
51 + reftable_record_release(&mi->subiters[i].rec);
52 continue;
53 }
54
@@ -56,9 +63,11 @@ static void merged_iter_close(void *p)
63 struct merged_iter *mi = p;
64
65 merged_iter_pqueue_release(&mi->pq);
59 - for (size_t i = 0; i < mi->stack_len; i++)
60 - reftable_iterator_destroy(&mi->stack[i]);
61 - reftable_free(mi->stack);
66 + for (size_t i = 0; i < mi->stack_len; i++) {
67 + reftable_iterator_destroy(&mi->subiters[i].iter);
68 + reftable_record_release(&mi->subiters[i].rec);
69 + }
70 + reftable_free(mi->subiters);
71 }
72
73 static int merged_iter_advance_nonnull_subiter(struct merged_iter *mi,
@@ -66,17 +75,16 @@ static int merged_iter_advance_nonnull_subiter(struct merged_iter *mi,
75 {
76 struct pq_entry e = {
77 .index = idx,
78 + .rec = &mi->subiters[idx].rec,
79 };
80 int err;
81
72 - reftable_record_init(&e.rec, mi->typ);
73 - err = iterator_next(&mi->stack[idx], &e.rec);
82 + err = iterator_next(&mi->subiters[idx].iter, &mi->subiters[idx].rec);
83 if (err < 0)
84 return err;
76 -
85 if (err > 0) {
78 - reftable_iterator_destroy(&mi->stack[idx]);
79 - reftable_record_release(&e.rec);
86 + reftable_iterator_destroy(&mi->subiters[idx].iter);
87 + reftable_record_release(&mi->subiters[idx].rec);
88 return 0;
89 }
90
@@ -86,7 +94,7 @@ static int merged_iter_advance_nonnull_subiter(struct merged_iter *mi,
94
95 static int merged_iter_advance_subiter(struct merged_iter *mi, size_t idx)
96 {
89 - if (iterator_is_null(&mi->stack[idx]))
97 + if (iterator_is_null(&mi->subiters[idx].iter))
98 return 0;
99 return merged_iter_advance_nonnull_subiter(mi, idx);
100 }
@@ -121,25 +129,19 @@ static int merged_iter_next_entry(struct merged_iter *mi,
129 struct pq_entry top = merged_iter_pqueue_top(mi->pq);
130 int cmp;
131
124 - cmp = reftable_record_cmp(&top.rec, &entry.rec);
132 + cmp = reftable_record_cmp(top.rec, entry.rec);
133 if (cmp > 0)
134 break;
135
136 merged_iter_pqueue_remove(&mi->pq);
137 err = merged_iter_advance_subiter(mi, top.index);
138 if (err < 0)
131 - goto done;
132 - reftable_record_release(&top.rec);
139 + return err;
140 }
141
135 - reftable_record_release(rec);
136 - *rec = entry.rec;
142 mi->advance_index = entry.index;
138 -
139 -done:
140 - if (err)
141 - reftable_record_release(&entry.rec);
142 - return err;
143 + SWAP(*rec, *entry.rec);
144 + return 0;
145 }
146
147 static int merged_iter_next(struct merged_iter *mi, struct reftable_record *rec)
@@ -257,10 +259,10 @@ static int merged_table_seek_record(struct reftable_merged_table *mt,
259 struct merged_iter *p;
260 int err;
261
260 - REFTABLE_CALLOC_ARRAY(merged.stack, mt->stack_len);
262 + REFTABLE_CALLOC_ARRAY(merged.subiters, mt->stack_len);
263 for (size_t i = 0; i < mt->stack_len; i++) {
264 err = reftable_table_seek_record(&mt->stack[i],
263 - &merged.stack[merged.stack_len], rec);
265 + &merged.subiters[merged.stack_len].iter, rec);
266 if (err < 0)
267 goto out;
268 if (!err)
reftable/pq.c
+2 -6
@@ -14,7 +14,7 @@ https://developers.google.com/open-source/licenses/bsd
14
15 int pq_less(struct pq_entry *a, struct pq_entry *b)
16 {
17 - int cmp = reftable_record_cmp(&a->rec, &b->rec);
17 + int cmp = reftable_record_cmp(a->rec, b->rec);
18 if (cmp == 0)
19 return a->index > b->index;
20 return cmp < 0;
@@ -82,10 +82,6 @@ void merged_iter_pqueue_add(struct merged_iter_pqueue *pq, const struct pq_entry
82
83 void merged_iter_pqueue_release(struct merged_iter_pqueue *pq)
84 {
85 - int i = 0;
86 - for (i = 0; i < pq->len; i++) {
87 - reftable_record_release(&pq->heap[i].rec);
88 - }
85 FREE_AND_NULL(pq->heap);
90 - pq->len = pq->cap = 0;
86 + memset(pq, 0, sizeof(*pq));
87 }
reftable/pq.h
+1 -1
@@ -13,7 +13,7 @@ https://developers.google.com/open-source/licenses/bsd
13
14 struct pq_entry {
15 size_t index;
16 - struct reftable_record rec;
16 + struct reftable_record *rec;
17 };
18
19 struct merged_iter_pqueue {
reftable/pq_test.c
+18 -23
@@ -27,48 +27,43 @@ void merged_iter_pqueue_check(struct merged_iter_pqueue pq)
27
28 static void test_pq(void)
29 {
30 - char *names[54] = { NULL };
31 - int N = ARRAY_SIZE(names) - 1;
32 -
30 struct merged_iter_pqueue pq = { NULL };
31 + struct reftable_record recs[54];
32 + int N = ARRAY_SIZE(recs) - 1, i;
33 char *last = NULL;
34
36 - int i = 0;
35 for (i = 0; i < N; i++) {
38 - char name[100];
39 - snprintf(name, sizeof(name), "%02d", i);
40 - names[i] = xstrdup(name);
36 + struct strbuf refname = STRBUF_INIT;
37 + strbuf_addf(&refname, "%02d", i);
38 +
39 + reftable_record_init(&recs[i], BLOCK_TYPE_REF);
40 + recs[i].u.ref.refname = strbuf_detach(&refname, NULL);
41 }
42
43 i = 1;
44 do {
45 - struct pq_entry e = { .rec = { .type = BLOCK_TYPE_REF,
46 - .u.ref = {
47 - .refname = names[i],
48 - } } };
45 + struct pq_entry e = {
46 + .rec = &recs[i],
47 + };
48 +
49 merged_iter_pqueue_add(&pq, &e);
50 merged_iter_pqueue_check(pq);
51 +
52 i = (i * 7) % N;
53 } while (i != 1);
54
55 while (!merged_iter_pqueue_is_empty(pq)) {
56 struct pq_entry e = merged_iter_pqueue_remove(&pq);
56 - struct reftable_record *rec = &e.rec;
57 merged_iter_pqueue_check(pq);
58
59 - EXPECT(reftable_record_type(rec) == BLOCK_TYPE_REF);
60 - if (last) {
61 - EXPECT(strcmp(last, rec->u.ref.refname) < 0);
62 - }
63 - /* this is names[i], so don't dealloc. */
64 - last = rec->u.ref.refname;
65 - rec->u.ref.refname = NULL;
66 - reftable_record_release(rec);
67 - }
68 - for (i = 0; i < N; i++) {
69 - reftable_free(names[i]);
59 + EXPECT(reftable_record_type(e.rec) == BLOCK_TYPE_REF);
60 + if (last)
61 + EXPECT(strcmp(last, e.rec->u.ref.refname) < 0);
62 + last = e.rec->u.ref.refname;
63 }
64
65 + for (i = 0; i < N; i++)
66 + reftable_record_release(&recs[i]);
67 merged_iter_pqueue_release(&pq);
68 }
69