Skip to content

Commit afa96bb

Browse files
authored
Merge pull request #4429 from arpadpanyik-arm/convertSequences_Neon
Improve speed of ZSTD_compressSequencesAndLiterals using Neon
2 parents c768d7b + 703f855 commit afa96bb

3 files changed

Lines changed: 435 additions & 21 deletions

File tree

.github/workflows/dev-short-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,8 +434,8 @@ jobs:
434434
make clean
435435
LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make -j check
436436
LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make -j -C tests test-cli-tests
437-
CFLAGS="-march=armv8.2-a+sve2" LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make -j check
438-
CFLAGS="-march=armv8.2-a+sve2" LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make -j -C tests test-cli-tests
437+
CFLAGS="-O3 -march=armv8.2-a+sve2" LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make -j check
438+
CFLAGS="-O3 -march=armv8.2-a+sve2" LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make -j -C tests test-cli-tests
439439
# This test is only compatible with standard libraries that support BTI (Branch Target Identification).
440440
# Unfortunately, the standard library provided on Ubuntu 24.04 does not have this feature enabled.
441441
# make clean

lib/compress/zstd_compress.c

Lines changed: 224 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@
5656
# define ZSTD_HASHLOG3_MAX 17
5757
#endif
5858

59+
60+
/*-*************************************
61+
* Forward declarations
62+
***************************************/
63+
size_t convertSequences_noRepcodes(SeqDef* dstSeqs, const ZSTD_Sequence* inSeqs,
64+
size_t nbSequences);
65+
66+
5967
/*-*************************************
6068
* Helper functions
6169
***************************************/
@@ -7118,7 +7126,7 @@ size_t ZSTD_compressSequences(ZSTD_CCtx* cctx,
71187126
}
71197127

71207128

7121-
#if defined(__AVX2__)
7129+
#if defined(ZSTD_ARCH_X86_AVX2)
71227130

71237131
#include <immintrin.h> /* AVX2 intrinsics */
71247132

@@ -7138,7 +7146,7 @@ size_t ZSTD_compressSequences(ZSTD_CCtx* cctx,
71387146
* @returns > 0 if there is one long length (> 65535),
71397147
* indicating the position, and type.
71407148
*/
7141-
static size_t convertSequences_noRepcodes(
7149+
size_t convertSequences_noRepcodes(
71427150
SeqDef* dstSeqs,
71437151
const ZSTD_Sequence* inSeqs,
71447152
size_t nbSequences)
@@ -7298,7 +7306,7 @@ static size_t convertSequences_noRepcodes(
72987306
* @returns > 0 if there is one long length (> 65535),
72997307
* indicating the position, and type.
73007308
*/
7301-
static size_t convertSequences_noRepcodes(SeqDef* dstSeqs, const ZSTD_Sequence* inSeqs, size_t nbSequences) {
7309+
size_t convertSequences_noRepcodes(SeqDef* dstSeqs, const ZSTD_Sequence* inSeqs, size_t nbSequences) {
73027310
size_t longLen = 0;
73037311

73047312
/* RVV depends on the specific definition of target structures */
@@ -7375,9 +7383,131 @@ static size_t convertSequences_noRepcodes(SeqDef* dstSeqs, const ZSTD_Sequence*
73757383
* but since this implementation is targeting modern systems (>= Sapphire Rapid),
73767384
* it's not useful to develop and maintain code for older pre-AVX2 platforms */
73777385

7378-
#else /* no AVX2 */
7386+
#elif defined(ZSTD_ARCH_ARM_NEON) && (defined(__aarch64__) || defined(_M_ARM64))
73797387

7380-
static size_t convertSequences_noRepcodes(
7388+
size_t convertSequences_noRepcodes(
7389+
SeqDef* dstSeqs,
7390+
const ZSTD_Sequence* inSeqs,
7391+
size_t nbSequences)
7392+
{
7393+
size_t longLen = 0;
7394+
size_t n = 0;
7395+
7396+
/* Neon permutation depends on the specific definition of target structures. */
7397+
ZSTD_STATIC_ASSERT(sizeof(ZSTD_Sequence) == 16);
7398+
ZSTD_STATIC_ASSERT(offsetof(ZSTD_Sequence, offset) == 0);
7399+
ZSTD_STATIC_ASSERT(offsetof(ZSTD_Sequence, litLength) == 4);
7400+
ZSTD_STATIC_ASSERT(offsetof(ZSTD_Sequence, matchLength) == 8);
7401+
ZSTD_STATIC_ASSERT(sizeof(SeqDef) == 8);
7402+
ZSTD_STATIC_ASSERT(offsetof(SeqDef, offBase) == 0);
7403+
ZSTD_STATIC_ASSERT(offsetof(SeqDef, litLength) == 4);
7404+
ZSTD_STATIC_ASSERT(offsetof(SeqDef, mlBase) == 6);
7405+
7406+
if (nbSequences > 3) {
7407+
static const ZSTD_ALIGNED(16) U32 constAddition[4] = {
7408+
ZSTD_REP_NUM, 0, -MINMATCH, 0
7409+
};
7410+
static const ZSTD_ALIGNED(16) U8 constMask[16] = {
7411+
0, 1, 2, 3, 4, 5, 8, 9, 16, 17, 18, 19, 20, 21, 24, 25
7412+
};
7413+
static const ZSTD_ALIGNED(16) U16 constCounter[8] = {
7414+
1, 1, 1, 1, 2, 2, 2, 2
7415+
};
7416+
7417+
const uint32x4_t vaddition = vld1q_u32(constAddition);
7418+
const uint8x16_t vmask = vld1q_u8(constMask);
7419+
uint16x8_t vcounter = vld1q_u16(constCounter);
7420+
uint16x8_t vindex01 = vdupq_n_u16(0);
7421+
uint16x8_t vindex23 = vdupq_n_u16(0);
7422+
7423+
do {
7424+
/* Load 4 ZSTD_Sequence (64 bytes). */
7425+
const uint32x4_t vin0 = vld1q_u32(&inSeqs[n + 0].offset);
7426+
const uint32x4_t vin1 = vld1q_u32(&inSeqs[n + 1].offset);
7427+
const uint32x4_t vin2 = vld1q_u32(&inSeqs[n + 2].offset);
7428+
const uint32x4_t vin3 = vld1q_u32(&inSeqs[n + 3].offset);
7429+
7430+
/* Add {ZSTD_REP_NUM, 0, -MINMATCH, 0} to each vector. */
7431+
const uint8x16x2_t vadd01 = { {
7432+
vreinterpretq_u8_u32(vaddq_u32(vin0, vaddition)),
7433+
vreinterpretq_u8_u32(vaddq_u32(vin1, vaddition)),
7434+
} };
7435+
const uint8x16x2_t vadd23 = { {
7436+
vreinterpretq_u8_u32(vaddq_u32(vin2, vaddition)),
7437+
vreinterpretq_u8_u32(vaddq_u32(vin3, vaddition)),
7438+
} };
7439+
7440+
/* Shuffle and pack bytes so each vector contains 2 SeqDef structures. */
7441+
const uint8x16_t vout01 = vqtbl2q_u8(vadd01, vmask);
7442+
const uint8x16_t vout23 = vqtbl2q_u8(vadd23, vmask);
7443+
7444+
/* Pack the upper 16-bits of 32-bit lanes for overflow check. */
7445+
uint16x8_t voverflow01 = vuzp2q_u16(vreinterpretq_u16_u8(vadd01.val[0]),
7446+
vreinterpretq_u16_u8(vadd01.val[1]));
7447+
uint16x8_t voverflow23 = vuzp2q_u16(vreinterpretq_u16_u8(vadd23.val[0]),
7448+
vreinterpretq_u16_u8(vadd23.val[1]));
7449+
7450+
/* Store 4 SeqDef structures. */
7451+
vst1q_u32(&dstSeqs[n + 0].offBase, vreinterpretq_u32_u8(vout01));
7452+
vst1q_u32(&dstSeqs[n + 2].offBase, vreinterpretq_u32_u8(vout23));
7453+
7454+
/* Create masks in case of overflow. */
7455+
voverflow01 = vcgtzq_s16(vreinterpretq_s16_u16(voverflow01));
7456+
voverflow23 = vcgtzq_s16(vreinterpretq_s16_u16(voverflow23));
7457+
7458+
/* Update overflow indices. */
7459+
vindex01 = vbslq_u16(voverflow01, vcounter, vindex01);
7460+
vindex23 = vbslq_u16(voverflow23, vcounter, vindex23);
7461+
7462+
/* Update counter for overflow check. */
7463+
vcounter = vaddq_u16(vcounter, vdupq_n_u16(4));
7464+
7465+
n += 4;
7466+
} while(n < nbSequences - 3);
7467+
7468+
/* Fixup indices in the second vector, we saved an additional counter
7469+
in the loop to update the second overflow index, we need to add 2
7470+
here when the indices are not 0. */
7471+
{ uint16x8_t nonzero = vtstq_u16(vindex23, vindex23);
7472+
vindex23 = vsubq_u16(vindex23, nonzero);
7473+
vindex23 = vsubq_u16(vindex23, nonzero);
7474+
}
7475+
7476+
/* Merge indices in the vectors, maximums are needed. */
7477+
vindex01 = vmaxq_u16(vindex01, vindex23);
7478+
vindex01 = vmaxq_u16(vindex01, vextq_u16(vindex01, vindex01, 4));
7479+
7480+
/* Compute `longLen`, maximums of matchLength and litLength
7481+
with a preference on litLength. */
7482+
{ U64 maxLitMatchIndices = vgetq_lane_u64(vreinterpretq_u64_u16(vindex01), 0);
7483+
size_t maxLitIndex = (maxLitMatchIndices >> 16) & 0xFFFF;
7484+
size_t maxMatchIndex = (maxLitMatchIndices >> 32) & 0xFFFF;
7485+
longLen = maxLitIndex > maxMatchIndex ? maxLitIndex + nbSequences
7486+
: maxMatchIndex;
7487+
}
7488+
}
7489+
7490+
/* Handle remaining elements. */
7491+
for (; n < nbSequences; n++) {
7492+
dstSeqs[n].offBase = OFFSET_TO_OFFBASE(inSeqs[n].offset);
7493+
dstSeqs[n].litLength = (U16)inSeqs[n].litLength;
7494+
dstSeqs[n].mlBase = (U16)(inSeqs[n].matchLength - MINMATCH);
7495+
/* Check for long length > 65535. */
7496+
if (UNLIKELY(inSeqs[n].matchLength > 65535 + MINMATCH)) {
7497+
assert(longLen == 0);
7498+
longLen = n + 1;
7499+
}
7500+
if (UNLIKELY(inSeqs[n].litLength > 65535)) {
7501+
assert(longLen == 0);
7502+
longLen = n + nbSequences + 1;
7503+
}
7504+
}
7505+
return longLen;
7506+
}
7507+
7508+
#else /* No vectorization. */
7509+
7510+
size_t convertSequences_noRepcodes(
73817511
SeqDef* dstSeqs,
73827512
const ZSTD_Sequence* inSeqs,
73837513
size_t nbSequences)
@@ -7388,7 +7518,7 @@ static size_t convertSequences_noRepcodes(
73887518
dstSeqs[n].offBase = OFFSET_TO_OFFBASE(inSeqs[n].offset);
73897519
dstSeqs[n].litLength = (U16)inSeqs[n].litLength;
73907520
dstSeqs[n].mlBase = (U16)(inSeqs[n].matchLength - MINMATCH);
7391-
/* check for long length > 65535 */
7521+
/* Check for long length > 65535. */
73927522
if (UNLIKELY(inSeqs[n].matchLength > 65535+MINMATCH)) {
73937523
assert(longLen == 0);
73947524
longLen = n + 1;
@@ -7604,29 +7734,104 @@ BlockSummary ZSTD_get1BlockSummary(const ZSTD_Sequence* seqs, size_t nbSeqs)
76047734

76057735
#else
76067736

7737+
/*
7738+
* The function assumes `litMatchLength` is a packed 64-bit value where the
7739+
* lower 32 bits represent the match length. The check varies based on the
7740+
* system's endianness:
7741+
* - On little-endian systems, it verifies if the entire 64-bit value is at most
7742+
* 0xFFFFFFFF, indicating the match length (lower 32 bits) is zero.
7743+
* - On big-endian systems, it directly checks if the lower 32 bits are zero.
7744+
*
7745+
* @returns 1 if the match length is zero, 0 otherwise.
7746+
*/
7747+
FORCE_INLINE_TEMPLATE int matchLengthHalfIsZero(U64 litMatchLength)
7748+
{
7749+
if (MEM_isLittleEndian()) {
7750+
return litMatchLength <= 0xFFFFFFFFULL;
7751+
} else {
7752+
return (U32)litMatchLength == 0;
7753+
}
7754+
}
7755+
76077756
BlockSummary ZSTD_get1BlockSummary(const ZSTD_Sequence* seqs, size_t nbSeqs)
76087757
{
7609-
size_t totalMatchSize = 0;
7610-
size_t litSize = 0;
7611-
size_t n;
7758+
/* Use multiple accumulators for efficient use of wide out-of-order machines. */
7759+
U64 litMatchSize0 = 0;
7760+
U64 litMatchSize1 = 0;
7761+
U64 litMatchSize2 = 0;
7762+
U64 litMatchSize3 = 0;
7763+
size_t n = 0;
7764+
7765+
ZSTD_STATIC_ASSERT(offsetof(ZSTD_Sequence, litLength) + 4 == offsetof(ZSTD_Sequence, matchLength));
7766+
ZSTD_STATIC_ASSERT(offsetof(ZSTD_Sequence, matchLength) + 4 == offsetof(ZSTD_Sequence, rep));
76127767
assert(seqs);
7613-
for (n=0; n<nbSeqs; n++) {
7614-
totalMatchSize += seqs[n].matchLength;
7615-
litSize += seqs[n].litLength;
7616-
if (seqs[n].matchLength == 0) {
7768+
7769+
if (nbSeqs > 3) {
7770+
/* Process the input in 4 independent streams to reach high throughput. */
7771+
do {
7772+
/* Load `litLength` and `matchLength` as a packed `U64`. It is safe
7773+
* to use 64-bit unsigned arithmetic here because the sum of `litLength`
7774+
* and `matchLength` cannot exceed the block size, so the 32-bit
7775+
* subparts will never overflow. */
7776+
U64 litMatchLength = MEM_read64(&seqs[n].litLength);
7777+
litMatchSize0 += litMatchLength;
7778+
if (matchLengthHalfIsZero(litMatchLength)) {
7779+
assert(seqs[n].offset == 0);
7780+
goto _out;
7781+
}
7782+
7783+
litMatchLength = MEM_read64(&seqs[n + 1].litLength);
7784+
litMatchSize1 += litMatchLength;
7785+
if (matchLengthHalfIsZero(litMatchLength)) {
7786+
n += 1;
7787+
assert(seqs[n].offset == 0);
7788+
goto _out;
7789+
}
7790+
7791+
litMatchLength = MEM_read64(&seqs[n + 2].litLength);
7792+
litMatchSize2 += litMatchLength;
7793+
if (matchLengthHalfIsZero(litMatchLength)) {
7794+
n += 2;
7795+
assert(seqs[n].offset == 0);
7796+
goto _out;
7797+
}
7798+
7799+
litMatchLength = MEM_read64(&seqs[n + 3].litLength);
7800+
litMatchSize3 += litMatchLength;
7801+
if (matchLengthHalfIsZero(litMatchLength)) {
7802+
n += 3;
7803+
assert(seqs[n].offset == 0);
7804+
goto _out;
7805+
}
7806+
7807+
n += 4;
7808+
} while(n < nbSeqs - 3);
7809+
}
7810+
7811+
for (; n < nbSeqs; n++) {
7812+
U64 litMatchLength = MEM_read64(&seqs[n].litLength);
7813+
litMatchSize0 += litMatchLength;
7814+
if (matchLengthHalfIsZero(litMatchLength)) {
76177815
assert(seqs[n].offset == 0);
7618-
break;
7816+
goto _out;
76197817
}
76207818
}
7621-
if (n==nbSeqs) {
7622-
BlockSummary bs;
7819+
/* At this point n == nbSeqs, so no end terminator. */
7820+
{ BlockSummary bs;
76237821
bs.nbSequences = ERROR(externalSequences_invalid);
76247822
return bs;
76257823
}
7824+
_out:
7825+
litMatchSize0 += litMatchSize1 + litMatchSize2 + litMatchSize3;
76267826
{ BlockSummary bs;
7627-
bs.nbSequences = n+1;
7628-
bs.blockSize = litSize + totalMatchSize;
7629-
bs.litSize = litSize;
7827+
bs.nbSequences = n + 1;
7828+
if (MEM_isLittleEndian()) {
7829+
bs.litSize = (U32)litMatchSize0;
7830+
bs.blockSize = bs.litSize + (litMatchSize0 >> 32);
7831+
} else {
7832+
bs.litSize = litMatchSize0 >> 32;
7833+
bs.blockSize = bs.litSize + (U32)litMatchSize0;
7834+
}
76307835
return bs;
76317836
}
76327837
}

0 commit comments

Comments
 (0)