-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[mscorlib] Improve perf for many Char methods #4881
Conversation
|
||
|
||
|
||
/*================================= ConvertFromUtf32 ============================ | ||
** Convert an UTF32 value into a surrogate pair. | ||
==============================================================================*/ | ||
|
||
public static String ConvertFromUtf32(int utf32) | ||
public unsafe static String ConvertFromUtf32(int utf32) |
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.
This is a public method. Does adding unsafe affect the public signature?
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 does not.
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 would, however, like the unsafe modifier scoped to the part of the method that needs it (specifically around the stackalloc). We should strive to minimize the amount of unsafe regions and how much code is in an unsafe context.
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.
@ellismg I'm going to move the stackalloc
part of this PR into a new PR, since (I'm hoping) it won't introduce any merge conflicts and it doesn't really fit in with the rest of this PR. Will address your feedback there.
Good stuff! Assuming you've fixed the corefx test failures, this looks good, espcially the ConvertFromUtf32 allocation stuff |
Could you tease out the stack-allocation 8cba55f into it's own commit? It would also be interesting to understand the impact of each of these changes on the relevant microbenchmarks. |
If we are going to cleanup here, I would rather just always get the values from CharUnicodeInfo so they constants are defined in one place. They are |
char* surrogate = stackalloc char[2]; | ||
surrogate[0] = (char)((utf32 / 0x400) + HIGH_SURROGATE_START); | ||
surrogate[1] = (char)((utf32 % 0x400) + LOW_SURROGATE_START); | ||
return new string(surrogate, 0, 2); |
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'm not sure what may be more performant, stackallocing a char* and constructing a string, or the following that uses an internal method I've seen in System.Globalization, StringBuilder and String itself:
string result = string.FastAllocateString(2);
fixed(char* pResult = result)
{
pResult[0] = (char)((utf32 / 0x400) + HIGH_SURROGATE_START);
pResult[1] = (char)((utf32 % 0x400) + LOW_SURROGATE_START);
}
return result;
@ellismg let me know what you think
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.
@hughbe Maybe, but 1) that's more of an implementation detail (it wouldn't work if you took the code and copy/pasted it outside of mscorlib), and 2) it requires pinning/unpinning the string while we write characters to it, so the benefit from that would be questionable. For now, I'm sticking to stackalloc
.
@ellismg Regarding the constant values, maybe we should alias the consts in this file to the ones in |
@@ -203,7 +203,7 @@ public bool Equals(Char obj) | |||
[Pure] | |||
public static bool IsDigit(char c) { | |||
if (IsLatin1(c)) { | |||
return (c >= '0' && c <= '9'); | |||
return (uint)(c - '0') <= (uint)('9' - '0'); |
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.
Self-note: I'm considering moving all of these boolean returns into a private helper method like the following:
private bool IsBetweenInclusive(char lowerBound, char upperBound)
{
return (uint)(m_value - lowerBound) <= (uint)(upperBound - lowerBound);
}
// Usage:
c.IsBetweenInclusive('0', '9');
This way we avoid repeating the value for the lower bound, and is less verbose/error prone.
Sorry, I was tagged to take a look at this, but I'm a bit busy and not going to be able to pay much attention to .NET Core things for the next couple of days. |
61f3444
to
d44f7ab
Compare
Alright, so I finally got around to making perf tests for this PR. Here are the results:
Since I used Notes:
Would be much appreciated if someone could validate these numbers for me. 😄 |
The benchmark code should be adjusted so that the result of
|
Good point, I had just realized that. I'm going to alter my test scheme to do something like the following: byte unused = 3;
for (int i = 0; i < Outer; i++)
{
var watch = Stopwatch.StartNew();
for (int j = 0; j < Inner; j++)
{
if (char.IsX(c)) unused++;
}
watch.Stop();
Console.WriteLine(watch.Elapsed);
}
// At the end...
GC.KeepAlive(unused); This way, if I understand correctly, the JIT will be forced to generate code for the method as it's being used in a branch.
Wait, really? Why so? (Also I've refactored it into a new static method named |
Yeah, that should work.
The code that the JIT generates for this kind of method isn't very good, see #914 |
My preference would be to just alias them instead of |
Ok guys, so I finally got around to making another round of perf tests for this change. Here is the source code, the old results, and the new results. Since the files are quite large / tedious to go through manually, I wrote a script to analyze the test results (you can view the output here). Notes:
edit: Ok, I've removed all of the ASCII-related changes, and only kept the ones affecting switch statements like I think this is finally ready to be merged. 😄 |
Closing this for now, I have a better one coming up in the future... |
Right now many methods in
Char
are implemented like this (for example):This isn't the most efficient way to implement it though; since these methods are often used in a loop, I've taken advantage of the fact that casting to
uint
causes the value to wrap, which avoids an additional branch. Here is the above method rewritten using this tactic:I've changed a bunch of static methods in
Char
to take advantage of this fact and avoid an additional branch, along with a couple of other changes:stackalloc
, instead of creating a new char array on the heap, forConvertFromUtf32
.HIGH_SURROGATE_END
andLOW_SURROGATE_START
consts, and removed references toCharUnicodeInfo
for getting those values.Note: Some of the corefx tests are failing with my changes, but unfortunately I'm not sure why/for what chars as the error messages are not very helpful.
cc @JonHanna @jkotas @mikedn @hughbe
edit: Looks like the string-and-index overloads actually can't just forward to the char-based ones, that's likely why the tests are failing. Will fix in a moment.