#1241·zlib

Optimize put_short() macro in trees.c by caching pending state and emitting two bytes together

Author: ParkHanbumCreated May 21, 2026Updated Jun 2, 2026

Hi, I noticed a small source-level optimization opportunity in trees.c around the put_short() macro.

Current code:

c
#define put_short(s, w) { \
    put_byte(s, (uch)((w) & 0xff)); \
    put_byte(s, (uch)((ush)(w) >> 8)); \
}

Since put_byte() expands to:

c
s->pending_buf[s->pending++] = (Bytef)(c)

the compiler has to be conservative between the two byte stores. In particular, the store through pending_buf may alias fields in s, so LLVM/Clang keeps extra reloads of s->bi_buf, s->pending, and s->pending_buf in several hot paths.

One possible improvement is to cache the word, pending offset, and pending buffer pointer inside put_short():

c
#define put_short(s, w) do { \
    ush put_short_w = (ush)(w); \
    ulg put_short_p = (s)->pending; \
    Bytef *put_short_buf = (s)->pending_buf; \
    put_short_buf[put_short_p] = (Bytef)put_short_w; \
    put_short_buf[put_short_p + 1] = (Bytef)(put_short_w >> 8); \
    (s)->pending = put_short_p + 2; \
} while (0)

This keeps the same byte order, but gives the optimizer a much simpler pattern. On x86-64 with Clang -O3, a reduced version changes from roughly:

asm
movzbl  16(%rdi), %eax
...
movb    %al, (%rcx,%rdx)
movzbl  17(%rdi), %eax
...
movb    %al, (%rcx,%rdx)

to:

movzwl  16(%rdi), %eax
movq    (%rdi), %rcx
movq    8(%rdi), %rdx
movw    %ax, (%rcx,%rdx)
addq    $2, %rdx
movq    %rdx, 8(%rdi)

I also did a compile-only check on trees.c using Clang -O3 on x86-64. The object .text size changed as follows:

original : 12481 bytes modified : 11847 bytes delta : -634 bytes

Some function-size changes: _tr_stored_block : 404 -> 277 _tr_flush_bits : 137 -> 112 _tr_align : 354 -> 264 compress_block : 1124 -> 946 send_tree : 1369 -> 1111 _tr_flush_block : 2965 -> 3031

The net result was still a reduction of 634 bytes in .text.

One semantic point: this version updates s->pending once after both byte stores, instead of after each byte store. That should be valid if pending_buf is treated as a separate pending output buffer and does not overlap the deflate_state fields themselves, which seems to be the intended zlib invariant. If that assumption is considered too strong, a more conservative variant would still be useful:

c
#define put_short(s, w) do { \
    ush put_short_w = (ush)(w); \
    put_byte(s, (uch)(put_short_w & 0xff)); \
    put_byte(s, (uch)(put_short_w >> 8)); \
} while (0)

sample 1 - keep w temporary : https://compiler-explorer.com/z/TqbErE859 sample 2 - use cached w : https://compiler-explorer.com/z/PWbbevobP

notice I found this while looking for LLVM optimization patterns in generated IR. If this direction looks acceptable, I would be happy to prepare and submit a patch.