Scan character classes with SIMD in the JIT on x86-64 - #941
Open
mattst88 wants to merge 1 commit into
Open
Conversation
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.
mattst88
force-pushed
the
jit-simd-class
branch
from
August 10, 2026 14:30
0ba053d to
a7a6807
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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,PSUBUSBandPCMPEQBper range,OR'd together, withPCMPEQBalone 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,}\bat 220 MB/s. RunTest and pcre2_jit_test pass.SSE4.2's
PCMPESTRIwas 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.