-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Improve performance of string.IndexOfAny for 2 & 3 char searches #13219
Changes from 1 commit
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 |
---|---|---|
|
@@ -96,8 +96,98 @@ public int IndexOfAny(char[] anyOf, int startIndex) | |
} | ||
|
||
[Pure] | ||
public int IndexOfAny(char[] anyOf, int startIndex, int count) | ||
{ | ||
if (anyOf == null) | ||
{ | ||
throw new ArgumentNullException(nameof(anyOf)); | ||
} | ||
|
||
if ((uint)startIndex > (uint)Length) | ||
{ | ||
throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index); | ||
} | ||
|
||
if ((uint)count > (uint)(Length - startIndex)) | ||
{ | ||
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count); | ||
} | ||
|
||
if (anyOf.Length == 2) | ||
{ | ||
// Very common optimization for directory separators (/, \), quotes (", '), brackets, etc | ||
return IndexOfAny(anyOf[0], anyOf[1], startIndex, count); | ||
} | ||
else if (anyOf.Length == 3) | ||
{ | ||
return IndexOfAny(anyOf[0], anyOf[1], anyOf[2], startIndex, count); | ||
} | ||
|
||
return IndexOfCharArray(anyOf, startIndex, count); | ||
} | ||
|
||
private unsafe int IndexOfAny(char value1, char value2, int startIndex, int count) | ||
{ | ||
fixed (char* pChars = &_firstChar) | ||
{ | ||
char* pCh = pChars + startIndex; | ||
|
||
while (count > 0) | ||
{ | ||
if (*pCh == value1) | ||
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. This depends on JIT CSEing Why not:
like in the 3 argument overload? 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 got the same time between the two variations so I just went with the simpler one. 3 char was better with a variable. Will change. |
||
goto ReturnIndex; | ||
|
||
if (*pCh == value2) | ||
goto ReturnIndex; | ||
|
||
// Possibly reads outside of count and can include null terminator | ||
// Handled in the return logic | ||
if (*(pCh + 1) == value1) | ||
goto ReturnIndex1; | ||
|
||
if (*(pCh + 1) == value2) | ||
goto ReturnIndex1; | ||
|
||
pCh += 2; | ||
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. Is there any reason why we only unroll 2x here? The other methods in this file, 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. Most of the usages of 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. You might want to look at that test code once you've got too much time: https://gist.github.com/dnickless/eb4807d698b7032b625a8d04794e90d8 |
||
count -= 2; | ||
} | ||
|
||
return -1; | ||
|
||
ReturnIndex: | ||
return (int)(pCh - pChars); | ||
|
||
ReturnIndex1: | ||
return (count == 1 ? -1 : (int)(pCh - pChars) + 1); | ||
} | ||
} | ||
|
||
private unsafe int IndexOfAny(char value1, char value2, char value3, int startIndex, int count) | ||
{ | ||
fixed (char* pChars = &_firstChar) | ||
{ | ||
char* pCh = pChars + startIndex; | ||
|
||
while (count > 0) | ||
{ | ||
char c = *pCh; | ||
|
||
if (c == value1 || c == value2 || c == value3) | ||
goto ReturnIndex; | ||
|
||
pCh++; | ||
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. Would 4x unrolling here not make sense, too? I didn't test this case but I would expect it to have a similar effect like with all other methods in this file. 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. 3 char is a lot less common. This was really about giving a lot better performance with minimal code. |
||
count--; | ||
} | ||
|
||
return -1; | ||
|
||
ReturnIndex: | ||
return (int)(pCh - pChars); | ||
} | ||
} | ||
|
||
[MethodImplAttribute(MethodImplOptions.InternalCall)] | ||
public extern int IndexOfAny(char[] anyOf, int startIndex, int count); | ||
private extern int IndexOfCharArray(char[] anyOf, int startIndex, int count); | ||
|
||
|
||
// Determines the position within this string of the first occurrence of the specified | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may not be common enough to warrant it (and thus may hurt the more common cases of 2/3), but I wonder if it's worthwhile adding here:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub The only place I could think of that would be using a 1 char array was the Unix path handling code. That looked to be taken care of with the Path internal changes so I didn't bother with it.
But true to performance testing form the 2 char call is actually 10% better if it has another
if
before it. Not so if I change that to aswitch
though which is interesting. Will run a few more tests on the other paths.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope sorry it was the 3 char test that was 10% better. 2 char is 4% worse. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These kind of odd variations are typically caused by things like code alignment or branch prediction that will vary from build to build. One has to run it under Intel profiler to tell what is going on. I typically just look at the JITed code whether it looks fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is worthwhile to add it, and also Length == 0. Otherwise, the API has unexpected perf cliff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😢 but don't optimize for them early? e.g.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems like a good compromise. Done.