reftable/block: reuse `zstream` state on inflation

When calling `inflateInit()` and `inflate()`, the zlib library will allocate several data structures for the underlying `zstream` to keep track of various information. Thus, when inflating repeatedly, it is possible to optimize memory allocation patterns by reusing the `zstream` and then calling `inflateReset()` on it to prepare it for the next chunk of data to inflate. This is exactly what the reftable code is doing: when iterating through reflogs we need to potentially inflate many log blocks, but we discard the `zstream` every single time. Instead, as we reuse the `block_reader` for each of the blocks anyway, we can initialize the `zstream` once and then reuse it for subsequent inflations. Refactor the code to do so, which leads to a significant reduction in the number of allocations. The following measurements were done when iterating through 1 million reflog entries. Before: HEAP SUMMARY: in use at exit: 13,473 bytes in 122 blocks total heap usage: 23,028 allocs, 22,906 frees, 162,813,552 bytes allocated After: HEAP SUMMARY: in use at exit: 13,473 bytes in 122 blocks total heap usage: 302 allocs, 180 frees, 88,352 bytes allocated Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Apr 8, 2024 at 14:17 UTC ce1f213cc91cf545736048f28117fe1de89b8134
3 files changed +19 -10
reftable/block.c
+15 -10
@@ -198,7 +198,6 @@ int block_reader_init(struct block_reader *br, struct reftable_block *block,
198 uint32_t block_header_skip = 4 + header_off;
199 uLong dst_len = sz - block_header_skip;
200 uLong src_len = block->len - block_header_skip;
201 - z_stream stream = {0};
201
202 /* Log blocks specify the *uncompressed* size in their header. */
203 REFTABLE_ALLOC_GROW(br->uncompressed_data, sz,
@@ -207,16 +206,21 @@ int block_reader_init(struct block_reader *br, struct reftable_block *block,
206 /* Copy over the block header verbatim. It's not compressed. */
207 memcpy(br->uncompressed_data, block->data, block_header_skip);
208
210 - err = inflateInit(&stream);
209 + if (!br->zstream) {
210 + REFTABLE_CALLOC_ARRAY(br->zstream, 1);
211 + err = inflateInit(br->zstream);
212 + } else {
213 + err = inflateReset(br->zstream);
214 + }
215 if (err != Z_OK) {
216 err = REFTABLE_ZLIB_ERROR;
217 goto done;
218 }
219
216 - stream.next_in = block->data + block_header_skip;
217 - stream.avail_in = src_len;
218 - stream.next_out = br->uncompressed_data + block_header_skip;
219 - stream.avail_out = dst_len;
220 + br->zstream->next_in = block->data + block_header_skip;
221 + br->zstream->avail_in = src_len;
222 + br->zstream->next_out = br->uncompressed_data + block_header_skip;
223 + br->zstream->avail_out = dst_len;
224
225 /*
226 * We know both input as well as output size, and we know that
@@ -225,15 +229,14 @@ int block_reader_init(struct block_reader *br, struct reftable_block *block,
229 * here to instruct zlib to inflate the data in one go, which
230 * is more efficient than using `Z_NO_FLUSH`.
231 */
228 - err = inflate(&stream, Z_FINISH);
229 - inflateEnd(&stream);
232 + err = inflate(br->zstream, Z_FINISH);
233 if (err != Z_STREAM_END) {
234 err = REFTABLE_ZLIB_ERROR;
235 goto done;
236 }
237 err = 0;
238
236 - if (stream.total_out + block_header_skip != sz) {
239 + if (br->zstream->total_out + block_header_skip != sz) {
240 err = REFTABLE_FORMAT_ERROR;
241 goto done;
242 }
@@ -242,7 +245,7 @@ int block_reader_init(struct block_reader *br, struct reftable_block *block,
245 reftable_block_done(block);
246 block->data = br->uncompressed_data;
247 block->len = sz;
245 - full_block_size = src_len + block_header_skip - stream.avail_in;
248 + full_block_size = src_len + block_header_skip - br->zstream->avail_in;
249 } else if (full_block_size == 0) {
250 full_block_size = sz;
251 } else if (sz < full_block_size && sz < block->len &&
@@ -275,6 +278,8 @@ done:
278
279 void block_reader_release(struct block_reader *br)
280 {
281 + inflateEnd(br->zstream);
282 + reftable_free(br->zstream);
283 reftable_free(br->uncompressed_data);
284 reftable_block_done(&br->block);
285 }
reftable/block.h
+3
@@ -56,6 +56,8 @@ int block_writer_finish(struct block_writer *w);
56 /* clears out internally allocated block_writer members. */
57 void block_writer_release(struct block_writer *bw);
58
59 +struct z_stream;
60 +
61 /* Read a block. */
62 struct block_reader {
63 /* offset of the block header; nonzero for the first block in a
@@ -67,6 +69,7 @@ struct block_reader {
69 int hash_size;
70
71 /* Uncompressed data for log entries. */
72 + z_stream *zstream;
73 unsigned char *uncompressed_data;
74 size_t uncompressed_cap;
75
reftable/reader.c
+1
@@ -459,6 +459,7 @@ static int reader_seek_linear(struct table_iter *ti,
459 * we would not do a linear search there anymore.
460 */
461 memset(&next.br.block, 0, sizeof(next.br.block));
462 + next.br.zstream = NULL;
463 next.br.uncompressed_data = NULL;
464 next.br.uncompressed_cap = 0;
465