-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Faster IndexOf for substrings #63285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 31 commits
34bf37a
03ae4da
5cfdb16
0638617
e36fdc6
85c2320
8918ab6
cb32d34
cda6b50
8af9270
87c26d0
652b42d
53cefad
d465407
22921fd
2c851bc
9308d82
dac974a
3554ad3
de87ec2
cb7541f
b0b04ad
141e236
e664ad3
bff8419
dcc9d81
3def5e0
a52138b
f86e323
f2372a0
38ef9a9
f5e6192
4827ddc
d601351
3a005bc
9fefe81
c118dd2
7e8b100
d805701
3bb56f0
e9df891
cb60535
7c55951
3f0b4c3
4d860a1
48f4fc7
7c3b834
c68a07a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,12 +22,26 @@ public static int IndexOf(ref byte searchSpace, int searchSpaceLength, ref byte | |
| if (valueLength == 0) | ||
| return 0; // A zero-length sequence is always treated as "found" at the start of the search space. | ||
|
|
||
| int valueTailLength = valueLength - 1; | ||
|
|
||
| if (valueTailLength == 0) | ||
| { | ||
| // for single-byte values use plain IndexOf | ||
| return IndexOf(ref searchSpace, value, searchSpaceLength); | ||
| } | ||
|
|
||
| byte valueHead = value; | ||
| ref byte valueTail = ref Unsafe.Add(ref value, 1); | ||
| int valueTailLength = valueLength - 1; | ||
| int offset = 0; | ||
| nuint valueTailNLength = (nuint)(uint)valueTailLength; | ||
|
|
||
| if (Vector128.IsHardwareAccelerated && searchSpaceLength - valueTailLength >= Vector128<byte>.Count) | ||
| { | ||
| goto SEARCH_TWO_BYTES; | ||
| } | ||
|
|
||
| int remainingSearchSpaceLength = searchSpaceLength - valueTailLength; | ||
|
|
||
| int offset = 0; | ||
| while (remainingSearchSpaceLength > 0) | ||
| { | ||
| // Do a quick search for the first element of "value". | ||
|
|
@@ -42,13 +56,119 @@ public static int IndexOf(ref byte searchSpace, int searchSpaceLength, ref byte | |
| break; // The unsearched portion is now shorter than the sequence we're looking for. So it can't be there. | ||
|
|
||
| // Found the first element of "value". See if the tail matches. | ||
| if (SequenceEqual(ref Unsafe.Add(ref searchSpace, offset + 1), ref valueTail, (nuint)valueTailLength)) // The (nuint)-cast is necessary to pick the correct overload | ||
| if (SequenceEqual(ref Unsafe.Add(ref searchSpace, offset + 1), ref valueTail, valueTailNLength)) // The (nuint)-cast is necessary to pick the correct overload | ||
| return offset; // The tail matched. Return a successful find. | ||
|
|
||
| remainingSearchSpaceLength--; | ||
| offset++; | ||
| } | ||
| return -1; | ||
|
|
||
| // Based on http://0x80.pl/articles/simd-strfind.html#algorithm-1-generic-simd "Algorithm 1: Generic SIMD" by Wojciech Muła | ||
| // Some details about the implementation can also be found in https://github.com/dotnet/runtime/pull/63285 | ||
| SEARCH_TWO_BYTES: | ||
| if (Avx2.IsSupported && searchSpaceLength - valueTailLength >= Vector256<byte>.Count) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why Avx2.IsSupported rather than Vector256.IsHardwareAccelerated? If we need to use Avx2 here, that seems like a failure of the Vector types we should fix.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stephentoub I personally am not a fan of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I maintain that if this can't be successful just using Vector*, something is wrong. A key point of these APIs is to not have to use or understand the low-level intrinsics.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like I stated in one of the other comments, I'm fine with us updating
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a follow up issue opened then, to remove use of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, if it's not going to be addressed in this PR, we need to address it subsequently soon.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tannergooding, is this something you're able to follow up on?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I opened #64309 |
||
| { | ||
| // Find the last unique (which is not equal to ch1) byte | ||
| // the algorithm is fine if both are equal, just a little bit less efficient | ||
| byte ch2Val = Unsafe.Add(ref value, valueTailLength); | ||
| int ch1ch2Distance = valueTailLength; | ||
| while (ch2Val == value && ch1ch2Distance > 1) | ||
|
stephentoub marked this conversation as resolved.
|
||
| ch2Val = Unsafe.Add(ref value, --ch1ch2Distance); | ||
|
|
||
| Vector256<byte> ch1 = Vector256.Create(value); | ||
| Vector256<byte> ch2 = Vector256.Create(ch2Val); | ||
|
|
||
| do | ||
| { | ||
| Vector256<byte> cmpCh1 = Vector256.Equals(ch1, Vector256.LoadUnsafe(ref searchSpace, (nuint)offset)); | ||
| Vector256<byte> cmpCh2 = Vector256.Equals(ch2, Vector256.LoadUnsafe(ref searchSpace, (nuint)(offset + ch1ch2Distance))); | ||
| Vector256<byte> cmpAnd = (cmpCh1 & cmpCh2).AsByte(); | ||
|
|
||
| // Early out: cmpAnd is all zeros | ||
| if (cmpAnd != Vector256<byte>.Zero) | ||
| { | ||
| uint mask = cmpAnd.ExtractMostSignificantBits(); | ||
| do | ||
| { | ||
| int bitPos = BitOperations.TrailingZeroCount(mask); | ||
| if (valueTailNLength == 1 || // we already matched two bytes | ||
| SequenceEqual( | ||
| ref Unsafe.Add(ref searchSpace, offset + bitPos + 1), | ||
|
EgorBo marked this conversation as resolved.
Outdated
|
||
| ref valueTail, | ||
| valueTailNLength)) | ||
| { | ||
| return offset + bitPos; | ||
| } | ||
| // Clear the lowest set bit | ||
| mask = BitOperations.ResetLowestSetBit(mask); | ||
| } while (mask != 0); | ||
| } | ||
|
|
||
| offset += Vector256<byte>.Count; | ||
|
|
||
| if (offset + valueTailLength == searchSpaceLength) | ||
| return -1; | ||
|
|
||
| // Overlap with the current chunk if there is not enough room for the next one | ||
| if (offset + valueTailLength + Vector256<byte>.Count > searchSpaceLength) | ||
|
EgorBo marked this conversation as resolved.
Outdated
|
||
| offset = searchSpaceLength - valueTailLength - Vector256<byte>.Count; | ||
|
|
||
| } while (true); | ||
| } | ||
|
|
||
| if (Vector128.IsHardwareAccelerated) | ||
| { | ||
| // Find the last unique (which is not equal to ch1) byte | ||
| // the algorithm is fine if both are equal, just a little bit less efficient | ||
| byte ch2Val = Unsafe.Add(ref value, valueTailLength); | ||
| int ch1ch2Distance = valueTailLength; | ||
| while (ch2Val == value && ch1ch2Distance > 1) | ||
| ch2Val = Unsafe.Add(ref value, --ch1ch2Distance); | ||
|
|
||
| Vector128<byte> ch1 = Vector128.Create(value); | ||
| Vector128<byte> ch2 = Vector128.Create(ch2Val); | ||
|
|
||
| do | ||
| { | ||
| Vector128<byte> cmpCh1 = Vector128.Equals(ch1, Vector128.LoadUnsafe(ref searchSpace, (nuint)offset)); | ||
| Vector128<byte> cmpCh2 = Vector128.Equals(ch2, Vector128.LoadUnsafe(ref searchSpace, (nuint)(offset + ch1ch2Distance))); | ||
| Vector128<byte> cmpAnd = (cmpCh1 & cmpCh2).AsByte(); | ||
|
|
||
| // Early out: cmpAnd is all zeros | ||
| // it's especially important for ARM where ExtractMostSignificantBits is not cheap | ||
| if (cmpAnd != Vector128<byte>.Zero) | ||
| { | ||
| uint mask = cmpAnd.ExtractMostSignificantBits(); | ||
| do | ||
| { | ||
| int bitPos = BitOperations.TrailingZeroCount(mask); | ||
| if (valueTailNLength == 1 || // we already matched two bytes | ||
| SequenceEqual( | ||
| ref Unsafe.Add(ref searchSpace, offset + bitPos + 1), | ||
| ref valueTail, | ||
| valueTailNLength)) | ||
| { | ||
| return offset + bitPos; | ||
| } | ||
| // Clear the lowest set bit | ||
| mask = BitOperations.ResetLowestSetBit(mask); | ||
| } while (mask != 0); | ||
| } | ||
| offset += Vector128<byte>.Count; | ||
|
|
||
| if (offset + valueTailLength == searchSpaceLength) | ||
| return -1; | ||
|
|
||
| // Overlap with the current chunk if there is not enough room for the next one | ||
| if (offset + valueTailLength + Vector128<byte>.Count > searchSpaceLength) | ||
| offset = searchSpaceLength - valueTailLength - Vector128<byte>.Count; | ||
|
|
||
| } while (true); | ||
| } | ||
|
|
||
| Debug.Fail("Unreachable"); | ||
| return -1; | ||
| } | ||
|
|
||
| // Adapted from IndexOf(...) | ||
|
|
@@ -416,11 +536,24 @@ public static int LastIndexOf(ref byte searchSpace, int searchSpaceLength, ref b | |
| if (valueLength == 0) | ||
| return searchSpaceLength; // A zero-length sequence is always treated as "found" at the end of the search space. | ||
|
|
||
| byte valueHead = value; | ||
| ref byte valueTail = ref Unsafe.Add(ref value, 1); | ||
| int valueTailLength = valueLength - 1; | ||
|
|
||
| if (valueTailLength == 0) | ||
| { | ||
| // for single-byte values use plain LastIndexOf | ||
| return LastIndexOf(ref searchSpace, value, searchSpaceLength); | ||
| } | ||
|
|
||
| byte valueHead = value; | ||
| ref byte valueTail = ref Unsafe.Add(ref value, 1); | ||
| int offset = 0; | ||
| nuint valueTailNLength = (nuint)(uint)valueTailLength; | ||
|
|
||
| if (Vector128.IsHardwareAccelerated && searchSpaceLength - valueTailLength >= Vector128<byte>.Count) | ||
| { | ||
| goto SEARCH_TWO_BYTES; | ||
| } | ||
|
|
||
| while (true) | ||
| { | ||
| Debug.Assert(0 <= offset && offset <= searchSpaceLength); // Ensures no deceptive underflows in the computation of "remainingSearchSpaceLength". | ||
|
|
@@ -434,12 +567,120 @@ public static int LastIndexOf(ref byte searchSpace, int searchSpaceLength, ref b | |
| break; | ||
|
|
||
| // Found the first element of "value". See if the tail matches. | ||
| if (SequenceEqual(ref Unsafe.Add(ref searchSpace, relativeIndex + 1), ref valueTail, (nuint)(uint)valueTailLength)) // The (nunit)-cast is necessary to pick the correct overload | ||
| if (SequenceEqual(ref Unsafe.Add(ref searchSpace, relativeIndex + 1), ref valueTail, valueTailNLength)) // The (nunit)-cast is necessary to pick the correct overload | ||
| return relativeIndex; // The tail matched. Return a successful find. | ||
|
|
||
| offset += remainingSearchSpaceLength - relativeIndex; | ||
| } | ||
| return -1; | ||
|
|
||
| // Based on http://0x80.pl/articles/simd-strfind.html#algorithm-1-generic-simd "Algorithm 1: Generic SIMD" by Wojciech Muła | ||
| // Some details about the implementation can also be found in https://github.com/dotnet/runtime/pull/63285 | ||
| SEARCH_TWO_BYTES: | ||
| if (Avx2.IsSupported && searchSpaceLength - valueTailLength >= Vector256<byte>.Count) | ||
| { | ||
| offset = searchSpaceLength - valueTailLength - Vector256<byte>.Count; | ||
|
|
||
| // Find the last unique (which is not equal to ch1) byte | ||
| // the algorithm is fine if both are equal, just a little bit less efficient | ||
| byte ch2Val = Unsafe.Add(ref value, valueTailLength); | ||
| int ch1ch2Distance = valueTailLength; | ||
| while (ch2Val == value && ch1ch2Distance > 1) | ||
| ch2Val = Unsafe.Add(ref value, --ch1ch2Distance); | ||
|
|
||
| Vector256<byte> ch1 = Vector256.Create(value); | ||
| Vector256<byte> ch2 = Vector256.Create(ch2Val); | ||
|
|
||
| do | ||
| { | ||
| Vector256<byte> cmpCh1 = Vector256.Equals(ch1, Vector256.LoadUnsafe(ref searchSpace, (nuint)offset)); | ||
| Vector256<byte> cmpCh2 = Vector256.Equals(ch2, Vector256.LoadUnsafe(ref searchSpace, (nuint)(offset + ch1ch2Distance))); | ||
| Vector256<byte> cmpAnd = (cmpCh1 & cmpCh2).AsByte(); | ||
|
|
||
| // Early out: cmpAnd is all zeros | ||
| if (cmpAnd != Vector256<byte>.Zero) | ||
| { | ||
| uint mask = cmpAnd.ExtractMostSignificantBits(); | ||
| do | ||
| { | ||
| // unlike IndexOf, here we use LZCNT to process matches starting from the end | ||
| int bitPos = 31 - BitOperations.LeadingZeroCount(mask); | ||
| if (valueTailNLength == 1 || // we already matched two bytes | ||
| SequenceEqual( | ||
| ref Unsafe.Add(ref searchSpace, offset + bitPos + 1), | ||
| ref valueTail, | ||
| valueTailNLength)) | ||
| { | ||
| return bitPos + offset; | ||
| } | ||
| // Clear the highest set bit. | ||
| mask = BitOperations.ResetBit(mask, bitPos); | ||
| } while (mask != 0); | ||
| } | ||
|
|
||
| offset -= Vector256<byte>.Count; | ||
| if (offset == -Vector256<byte>.Count) | ||
| return -1; | ||
| // Overlap with the current chunk if there is not enough room for the next one | ||
| if (offset < 0) | ||
| offset = 0; | ||
|
|
||
| } while (true); | ||
| } | ||
| if (Vector128.IsHardwareAccelerated) | ||
| { | ||
| offset = searchSpaceLength - valueTailLength - Vector128<byte>.Count; | ||
|
|
||
| // Find the last unique (which is not equal to ch1) byte | ||
| // the algorithm is fine if both are equal, just a little bit less efficient | ||
| byte ch2Val = Unsafe.Add(ref value, valueTailLength); | ||
| int ch1ch2Distance = valueTailLength; | ||
| while (ch2Val == value && ch1ch2Distance > 1) | ||
| ch2Val = Unsafe.Add(ref value, --ch1ch2Distance); | ||
|
|
||
| Vector128<byte> ch1 = Vector128.Create(value); | ||
| Vector128<byte> ch2 = Vector128.Create(ch2Val); | ||
|
|
||
| do | ||
| { | ||
| Vector128<byte> cmpCh1 = Vector128.Equals(ch1, Vector128.LoadUnsafe(ref searchSpace, (nuint)offset)); | ||
| Vector128<byte> cmpCh2 = Vector128.Equals(ch2, Vector128.LoadUnsafe(ref searchSpace, (nuint)(offset + ch1ch2Distance))); | ||
| Vector128<byte> cmpAnd = (cmpCh1 & cmpCh2).AsByte(); | ||
|
|
||
| // Early out: cmpAnd is all zeros | ||
| // it's especially important for ARM where ExtractMostSignificantBits is not cheap | ||
| if (cmpAnd != Vector128<byte>.Zero) | ||
| { | ||
| uint mask = cmpAnd.ExtractMostSignificantBits(); | ||
| do | ||
| { | ||
| // unlike IndexOf, here we use LZCNT to process matches starting from the end | ||
| int bitPos = 31 - BitOperations.LeadingZeroCount(mask); | ||
| if (valueTailNLength == 1 || // we already matched two bytes | ||
| SequenceEqual( | ||
| ref Unsafe.Add(ref searchSpace, offset + bitPos + 1), | ||
| ref valueTail, | ||
| valueTailNLength)) | ||
| { | ||
| return bitPos + offset; | ||
| } | ||
| // Clear the highest set bit. | ||
| mask = BitOperations.ResetBit(mask, bitPos); | ||
| } while (mask != 0); | ||
| } | ||
|
|
||
| offset -= Vector128<byte>.Count; | ||
| if (offset == -Vector128<byte>.Count) | ||
| return -1; | ||
| // Overlap with the current chunk if there is not enough room for the next one | ||
| if (offset < 0) | ||
| offset = 0; | ||
|
|
||
| } while (true); | ||
| } | ||
|
|
||
| Debug.Fail("Unreachable"); | ||
|
EgorBo marked this conversation as resolved.
Outdated
|
||
| return -1; | ||
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveOptimization)] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.