mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-09 03:29:43 -10:00
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:
1 parent
b3a90df974
commit
82512845b4
1 file changed
+70
-4
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user