Scan character classes with SIMD in the JIT on x86-64 - #941
Conversation
|
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. |
0ba053d to
a7a6807
Compare
zherczeg
left a comment
There was a problem hiding this comment.
This looks like a good improvement.
The other similar functions also support relative offset, so not only [range] but also .{n}[range] is also supported where n is < 15. I.e. the first few characters are too complex to search, but there is a simple range after them.
| than max_ranges of them, in which case the caller must fall back to testing | ||
| the bitmap one code unit at a time. */ | ||
|
|
||
| static int extract_start_bits_ranges(const sljit_u8 *bits, start_bits_range *ranges, int max_ranges) |
There was a problem hiding this comment.
I think there is a similar function in the code somewhere. Not sure it can be used here.
There was a problem hiding this comment.
It was the scan at the top of optimize_class_ranges(). The first commit of the
series now moves that into extract_class_ranges(), which takes a limit on the
number of entries instead of the fixed MAX_CLASS_RANGE_SIZE, and sits before
the include of pcre2_jit_simd_inc.h so both users see it.
extract_start_bits_ranges() then only has to turn the transition list into
inclusive pairs. optimize_class_ranges() is otherwise unchanged: the
membership of the highest code unit, which its loop used to leave behind in
bit, is read from the bitmap instead.
| /* PSUBUSB dst, span */ | ||
| emit_sse2_op(compiler, 0xd8, dst_ind, span_ind); | ||
| /* PCMPEQB dst, zero */ | ||
| emit_sse2_op(compiler, 0x74, dst_ind, zero_ind); |
There was a problem hiding this comment.
This looks like a good idea
| /* Emit an SSE2 instruction operating on two vector registers. A REX prefix is | ||
| needed for xmm8 and above, which the start-bits scan reaches as soon as it | ||
| holds the bounds of more than two ranges. */ | ||
| static SLJIT_INLINE void emit_sse2_op(struct sljit_compiler *compiler, sljit_u8 opcode, |
There was a problem hiding this comment.
Maybe such helpers could be used to improve code readability.
There was a problem hiding this comment.
Done in the second commit of the series. emit_sse2_op() became
emit_vector_op(), taking an opcode and three register indices and picking the
two operand SSE2 form or the three operand AVX2 one from the register width, and
fast_forward_char_pair_sse2_compare() is rewritten on top of it, losing about
60 lines of hand assembled prefix bytes.
The one place the two shapes genuinely differ is the two character compare,
where the 256 bit form does with one instruction what the 128 bit form needs two
for, so that case no longer falls into the shared switch.
Since the AVX2 path is disabled at runtime I could not exercise it the usual way,
so I compiled the old and the new function side by side against a recording stub
and compared their output: 98304 cases, every combination of compare type,
register width, step and register assignment, byte identical.
|
This looks nice. However, I recommend that we bump it to the next release, rather than merge it and release it in a hurry. I'm just trying to get all the required bugfixes in, and stabilise the branch ahead of the upcoming release. |
optimize_class_ranges() reduces a 256 bit class bitmap to the code unit values at which membership of the class changes, so that a class of a few ranges can be tested with a handful of comparisons. Move that scan into extract_class_ranges(), placed before the include of pcre2_jit_simd_inc.h so that the vectorized scans can use it too, and give it a limit on the number of entries rather than the fixed MAX_CLASS_RANGE_SIZE. The membership of the highest code unit, which the loop used to leave behind in bit, is read from the bitmap instead. No functional change.
fast_forward_char_pair_sse2_compare() builds each instruction byte by byte, patching the prefix in place for the AVX2 form, so that the same four bytes are assembled seven times over in slightly different ways. Move the encoding into emit_vector_op(), which takes an opcode and three register indices and picks the two operand SSE2 form or the three operand AVX2 one from the register width. The 256 bit path for a two character compare no longer reaches the shared switch, since what it does with one instruction the 128 bit path needs two of. The helper also emits a REX prefix, which the byte-by-byte code had no need for, so that later users can reach the upper vector registers. The emitted bytes are unchanged for every combination of compare type, register width, step and register assignment.
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 with extract_class_ranges(); 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.
The bounds of each range live in a vector register of their own, so the
scan needs twelve of them, more than the Win64 ABI leaves as scratch
registers. It is therefore limited to the other x86-64 ABIs, as the
character pair scan already is.
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.
The other fast-forward scans take an offset, so a pattern such as
.{3}abc searches for the literal three code units in and steps back,
rather than giving up because the first positions are too complex to
search. The vectorized class scan only handled a class at the start of
the match, so .{3}[QXZ] fell back to the byte-at-a-time loop.
scan_prefix() already walks the class bitmap at each position, but it
expands it into the chars array and keeps at most five characters, so a
larger class leaves nothing behind but "any character". Record the bitmap
itself alongside the array. A position keeps its bitmap only while one and
the same class reaches it, since a second alternative, a literal or an
unbounded class all make it describe less than the position accepts.
fast_forward_first_n_chars() then picks the sparsest such position, but
only where it would otherwise give up: a position holding one or two
characters gets a cheaper scan of its own, and a run of them gets the
shift table, both of which beat testing a handful of ranges.
fast_forward_start_bits_simd() gains the offset, along with the check that
the subject still holds a block once STR_PTR has moved forward, and the
UTF restart which skips a candidate that is not on a character boundary.
At offset zero neither is needed and neither is emitted, so the scan
already in use is unchanged.
Measured over a 4MB subject on an i7-1370P:
- .{3}[QXZ] goes from 450 to 25798 MB/s
- .{2}[0-9]{6} from 647 to 33424 MB/s.
A dense class at an offset, .{3}\w{12}, is left alone as before at 195
MB/s, and a.{4}[QXZ] keeps its single character scan at 1486 MB/s. Every
other pattern measured is unchanged. RunTest and pcre2_jit_test pass at
all three code unit widths and on x86-32.
a7a6807 to
c78bbaa
Compare
|
Thanks for the review. The two inline comments are answered in their threads;
This is the fourth commit of the series.
Two things worth flagging separately. The scan as I first posted it was broken on Win64. It holds the bounds of each I also tried handing the bound registers out densely rather than two per range, The branch is force-pushed and rebased on current main. Tested at all three code |
fast_forward_start_bits()tests the start bitmap one code unit at a time onevery 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.
This adds a hook for it and implements it for x86-64 with SSE2, both for a class
at the start of the match and for one at a relative offset, so that
.{3}[QXZ]is searched as well as
[QXZ].Commits
Add a helper to split a class bitmap into ranges.
optimize_class_ranges()already reduced a 256 bit bitmap to the code unit values at which membership
changes. That scan moves into
extract_class_ranges(), placed before theinclude of
pcre2_jit_simd_inc.hso the vectorized scan can use it too, andtakes a limit on the number of entries rather than the fixed
MAX_CLASS_RANGE_SIZE. No functional change.Emit the x86 vector compares through one helper.
fast_forward_char_pair_sse2_compare()assembled each instruction byte by byte,patching the prefix in place for the AVX2 form, so the same four bytes were put
together seven times over in slightly different ways. The encoding moves into
emit_vector_op(), which takes an opcode and three register indices and picksthe two operand SSE2 form or the three operand AVX2 one from the register width.
It also emits a REX prefix, which the byte-by-byte code had no need for, so that
later users can reach the upper vector registers. The emitted bytes are
unchanged for every combination of compare type, register width, step and
register assignment.
Scan character classes with SIMD in the JIT on x86-64. 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,with
PCMPEQBalone for a single character. Bitmaps needing more than fourranges keep the existing scan.
Scan for a character class at a relative offset in the JIT.
scan_prefix()already walks the class bitmap at each position, but expands it into the
charsarray and keeps at most
MAX_DIFF_CHARScharacters, so a larger class leftnothing behind but "any character". It now records the bitmap itself alongside
the array. A position keeps its bitmap only while one and the same class reaches
it, since a second alternative, a literal or an unbounded class all make it
describe less than the position accepts.
fast_forward_first_n_chars()then picks the sparsest such position, but onlywhere it would otherwise give up: a position holding one or two characters gets
a cheaper scan of its own, and a run of them gets the shift table, both of which
beat testing a handful of ranges.
Numbers
Measured over a 4MB subject on an i7-1370P, scanning for a class that does not
occur:
[QXZ][0-9]{6}.{3}[QXZ].{2}[0-9]{6}Every other pattern measured is unchanged, including
a.{4}[QXZ]at 1486 MB/s,which keeps its single character scan rather than being taken over by the class
scan.
Notes
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, and
\b\w{12,}\band.{3}\w{12}are unchanged.The bounds of each range live in a vector register of their own, so the scan
needs twelve of them, more than the Win64 ABI leaves as scratch registers. It is
limited to the other x86-64 ABIs, as
fast_forward_char_pair_simd()already is,with a compile time assertion tying the range limit to
SLJIT_NUMBER_OF_SCRATCH_VECTOR_REGISTERS.SSE4.2's
PCMPESTRIwas measured as an alternative. It handles up to eightranges 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 azero byte as the end of the subject.
RunTest and pcre2_jit_test pass at all three code unit widths, on x86-32, and
with the SIMD scans compiled out.