deflate: restructure adler32 so the vectoriser can take it

Profiling level-1 compression of a 1 MB text corpus put 7.6% of all
instructions in the adler32 inner loop - the textbook

   do { a += *buf++; b += a; } while (--n);

whose b += a after every byte is a serial dependency no compiler can
break.  Over a group of N bytes the same result is two independent
reductions with constant weights,

   a' = a + SUM(buf[j])
   b' = b + N*a + SUM((N-j)*buf[j])

which vectorises.  Both copies - the deflate-side rd_adler32 and the
inflate-side rinf_adler32_update - are restructured this way; the
5552-byte chunk bound and the modulo placement are untouched, so the
overflow argument is unchanged with them.

N is 32, not 16, because 16 is what GCC declines: at that width it
unrolls scalar and the object file gains no SIMD at all (worth about
2% by instruction-level parallelism alone).  At 32 it emits
punpck/paddd on x86-64 and umull/mla/addv on aarch64.  That was read
out of the object files rather than assumed, and the code is correct
either way - a compiler that declines still computes the same sums.

Measured against zlib 1.3 on 1 MB corpora, best of five.  Level 1 on
text goes from 23% slower than zlib to 1% faster, while still
compressing 8% smaller; level 2 from 5% slower to parity.  Levels
4-9 hold at 3-6% faster than zlib within 0.3% of its ratio.  Inflate
of a level-6 text stream narrows to 2% behind zlib, and is 31% ahead
on savestate-like data.  What remains slower is levels 1-3 on
savestate-like input (16%), where zlib's deflate_fast gives up 2.4%
of ratio for the speed - a different point on the curve, not a
defect.

Verified: the emitted zlib trailer matches zlib's own adler32 at
every boundary size around the group and chunk widths (0, 1, 15, 16,
17, 255, 5551, 5552, 5553 and 1 MB); the 1,080-configuration inflate
stress - three corpora x levels 0-9 x six input chunk sizes x six
output chunk sizes - stays byte-identical.  Teeth-checked per copy:
perturbing the weight in rd_adler32 fails the trailer comparison,
and in rinf_adler32_update fails 588 of the 1,080 stress cases.

For the record, since it prompted this work: stock zlib 1.3 contains
no hand-written assembly and no runtime SIMD dispatch.  Its only
vector code is what the C compiler generates, adler32 included, so
the gap here was never an assembly gap - it was one loop shape.
This commit is contained in:
libretroadmin authored and libretroadmin committed 2026-07-27 01:37:05 +02:00
1 parent b3a90df974
commit 82512845b4
1 file changed
+70 -4
+70 -4
View File
@@ -153,10 +153,42 @@ static uint32_t rinf_adler32_update(uint32_t adler,
{
/* process in chunks so the sums never overflow before the modulo */
size_t n = len > 5552 ? 5552 : len;
size_t k = n;
len -= n;
do {
a += *buf++; b += a;
} while (--n);
{
/* Thirty-two bytes at a time. The textbook loop carries a
* serial dependency - b += a after every byte - that no
* compiler can vectorise. Over a group of N the same result is
* a' = a + SUM(buf[j])
* b' = b + N*a + SUM((N-j)*buf[j])
* two independent reductions with constant weights. N is 32
* rather than 16 because that is what GCC's vectoriser
* actually takes: at 16 it declines and unrolls scalar, at 32
* it emits punpck/paddd (and the NEON equivalent on ARM).
* Verified in the object file, not assumed - and correct
* either way, since a compiler that declines still computes
* the same sums. The chunk bound is untouched, so the
* overflow argument is unchanged with it. */
while (k >= 32)
{
uint32_t sum = 0, wsum = 0;
unsigned j;
for (j = 0; j < 32; j++)
{
sum += buf[j];
wsum += (uint32_t)(32 - j) * buf[j];
}
b += 32 * a + wsum;
a += sum;
buf += 32;
k -= 32;
}
while (k--)
{
a += *buf++;
b += a;
}
}
a %= ADLER_MOD;
b %= ADLER_MOD;
}
@@ -1034,8 +1066,42 @@ static uint32_t rd_adler32(uint32_t adler, const uint8_t *buf, size_t len)
while (len)
{
size_t n = len > 5552 ? 5552 : len;
size_t k = n;
len -= n;
do { a += *buf++; b += a; } while (--n);
{
/* Thirty-two bytes at a time. The textbook loop carries a
* serial dependency - b += a after every byte - that no
* compiler can vectorise. Over a group of N the same result is
* a' = a + SUM(buf[j])
* b' = b + N*a + SUM((N-j)*buf[j])
* two independent reductions with constant weights. N is 32
* rather than 16 because that is what GCC's vectoriser
* actually takes: at 16 it declines and unrolls scalar, at 32
* it emits punpck/paddd (and the NEON equivalent on ARM).
* Verified in the object file, not assumed - and correct
* either way, since a compiler that declines still computes
* the same sums. The chunk bound is untouched, so the
* overflow argument is unchanged with it. */
while (k >= 32)
{
uint32_t sum = 0, wsum = 0;
unsigned j;
for (j = 0; j < 32; j++)
{
sum += buf[j];
wsum += (uint32_t)(32 - j) * buf[j];
}
b += 32 * a + wsum;
a += sum;
buf += 32;
k -= 32;
}
while (k--)
{
a += *buf++;
b += a;
}
}
a %= RD_ADLER_MOD;
b %= RD_ADLER_MOD;
}