Skip to content

Scan character classes with SIMD in the JIT on x86-64 - #941

Open
mattst88 wants to merge 1 commit into
PCRE2Project:mainfrom
mattst88:jit-simd-class
Open

Scan character classes with SIMD in the JIT on x86-64#941
mattst88 wants to merge 1 commit into
PCRE2Project:mainfrom
mattst88:jit-simd-class

Conversation

@mattst88

Copy link
Copy Markdown
Contributor

fast_forward_start_bits() tests the start bitmap one code unit at a time on every architecture, so a pattern whose first character is a class scans far more slowly than one starting with a literal, even though the JIT already has a vectorized scan for the literal case.

Add a hook for it, alongside the three that already exist, and implement it for x86-64 with SSE2. The bitmap is reduced to a list of ranges at compile time; the scan then tests each range with the usual unsigned range idiom, where subtracting the low bound makes the range start at zero and a saturating subtract of the span leaves zero exactly for the bytes inside it, since anything below the low bound wraps to a value larger than the span. That is PSUBB, PSUBUSB and PCMPEQB per range, OR'd together, with PCMPEQB alone for a single character. Bitmaps needing more than four ranges keep the existing scan.

Vectorizing only pays for a sparse class. The byte-at-a-time loop stops at the first code unit in the class, so where the class is dense it stops almost immediately, while a vector loop has already tested a whole block. Measured on \b\w{12,}\b, whose class accepts 63 code units, the vector scan was 38.6% slower. Classes accepting more than 48 code units are therefore left alone.

Measured over a 4MB subject on an i7-1370P, scanning for a class that does not occur:

  • [QXZ] goes from 1445 to 25961 MB/s
  • [0-9]{6} from 3337 to 33440 MB/s.

Every other pattern measured is unchanged, including \b\w{12,}\b at 220 MB/s. RunTest and pcre2_jit_test pass.

SSE4.2's PCMPESTRI was measured as an alternative. It handles up to eight ranges in one instruction and so is flat in the number of ranges, but it is slower than this sequence for one or two ranges, which is what real classes mostly are: 11.5 GB/s against 28.5 GB/s for a single range. It also has no 256-bit form, so it would foreclose widening this loop to AVX2 later. PCMPISTRI, the faster of the two, cannot be used at all, because it treats a zero byte as the end of the subject.

@mattst88

Copy link
Copy Markdown
Contributor Author

This was something that I noticed could be improved when investigating one of the regressions on Alpha.

Not sure if I should add support for necessary ops to sljit first and then use them here or whether the approach this patch takes is okay. Happy to do either.

fast_forward_start_bits() tests the start bitmap one code unit at a time
on every architecture, so a pattern whose first character is a class
scans far more slowly than one starting with a literal, even though the
JIT already has a vectorized scan for the literal case.

Add a hook for it, alongside the three that already exist, and implement
it for x86-64 with SSE2. The bitmap is reduced to a list of ranges at
compile time; the scan then tests each range with the usual unsigned
range idiom, where subtracting the low bound makes the range start at
zero and a saturating subtract of the span leaves zero exactly for the
bytes inside it, since anything below the low bound wraps to a value
larger than the span. That is PSUBB, PSUBUSB and PCMPEQB per range, OR'd
together, with PCMPEQB alone for a single character. Bitmaps needing
more than four ranges keep the existing scan.

Vectorizing only pays for a sparse class. The byte-at-a-time loop stops
at the first code unit in the class, so where the class is dense it
stops almost immediately, while a vector loop has already tested a whole
block. Measured on \b\w{12,}\b, whose class accepts 63 code units, the
vector scan was 38.6% slower. Classes accepting more than 48 code units
are therefore left alone.

Measured over a 4MB subject on an i7-1370P, scanning for a class that
does not occur: [QXZ] goes from 1445 to 25961 MB/s, and [0-9]{6} from
3337 to 33440 MB/s. Every other pattern measured is unchanged, including
\b\w{12,}\b at 220 MB/s. RunTest and pcre2_jit_test pass.

SSE4.2's PCMPESTRI was measured as an alternative. It handles up to
eight ranges in one instruction and so is flat in the number of ranges,
but it is slower than this sequence for one or two ranges, which is what
real classes mostly are: 11.5 GB/s against 28.5 GB/s for a single range.
It also has no 256-bit form, so it would foreclose widening this loop to
AVX2 later. PCMPISTRI, the faster of the two, cannot be used at all,
because it treats a zero byte as the end of the subject.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant