Optimize put_short() macro in trees.c by caching pending state and emitting two bytes together
Hi, I noticed a small source-level optimization opportunity in trees.c around the put_short() macro.
Current code:
#define put_short(s, w) { \
put_byte(s, (uch)((w) & 0xff)); \
put_byte(s, (uch)((ush)(w) >> 8)); \
}Since put_byte() expands to:
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():
#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:
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:
#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.
Source: madler/zlib