-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Use ValueStringBuilder in preference to StringBuilderCache #26934
Conversation
We are trying to get rid of the existing usages of StringBuilderCache, not add more of them. Also, the StringBuilderCache is 360 chars max, and the stacktrace is often going to bigger than that. |
And when it misses this limit, it will turn into Gen2 garbage that has worse side-effect than a bit of Gen0 garbage. |
|
I wish "objects on stack" feature supported StringBuilder |
Yes, ValueStringBuilder would be better. |
89c0a10
to
d2be6bc
Compare
Had to add |
Updated summary: Saves 44MB of allocations for the 20k Exceptions thrown async, caught, captured, |
That is a lot of code to add for saving allocations on exceptional paths. It does not feel worth it to me. |
Also changed |
src/System.Private.CoreLib/shared/System/Text/ValueStringBuilder.cs
Outdated
Show resolved
Hide resolved
src/System.Private.CoreLib/shared/System/String.Manipulation.cs
Outdated
Show resolved
Hide resolved
src/System.Private.CoreLib/shared/System/String.Manipulation.cs
Outdated
Show resolved
Hide resolved
Ok, that is a convincing argument. |
I've also changed all of the less unusual uses of StringBuilderCache to use ValueStringBuilder now (not the ones that pass the cached StringBuilder around) |
src/System.Private.CoreLib/shared/System/Text/ValueStringBuilder.AppendFormat.cs
Outdated
Show resolved
Hide resolved
{ | ||
if (s is null) return; |
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.
Does enough places pass null into this to make this worth it?
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 aggressive inlined everywhere.)
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.
.ToString
on object returning a string?
doesn't help :) But no, putting the guards in the call sites.
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.
.ToString on object returning a string? doesn't help :)
Other than in advanced implementations like Format using this, where is ToString being passed to Append on types that don't override it?
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.
.ToString
in JoinCore<T>
on T
; .ToString
in JoinCore
on object
etc changes in b232228
Are basically on object
or T
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.
Right, that's what I meant by advanced implementations, helpers in core types like string itself. And such methods need to check for null, anyway. We've had bugs filed in the past because sometimes they haven't, e.g.
https://github.com/dotnet/coreclr/issues/6785
dotnet/roslyn#34085
Whether it's the caller or callee checking, someone needs to check.
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.
There is a lot of places that need the null check. I am thinking it would be better to just keep it inside the method. @benaadams @stephentoub Do you have an opinion?
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'd prefer it inside:
- Better matches StringBuilder
- Easier to consume
- Doesn't add a lot of complexity
- Will lead to less IL (if not asm) due to number of call sites that needed the check
@@ -159,15 +159,16 @@ public static string Concat<T>(IEnumerable<T> values) | |||
|
|||
// Create the StringBuilder, add the chars we've already enumerated, | |||
// add the rest, and then get the resulting string. | |||
StringBuilder result = StringBuilderCache.Acquire(); | |||
Span<char> initialBuffer = stackalloc char[64]; |
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.
The stack reserved sizes in this file can be probably be a bit higher, like 128 chars.
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.
We've been fairly loose / non-prescriptive with how we do stackallocs like this in the past. In various places around corelib, we use various limits for stackalloc char when we don't know the actual upper limit, including 256, 128, 64, and 32 (lots of other sizes are used of course when we do have an upper bound). Not as part of this change, but it seems we should probably standardize with some const somewhere that everyone just uses.
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 agree that a general reasonable sized buffer constant would be useful.
We often know the typical maximum size, e.g. 32-chars for a number formatting. I assume that we would keep these scenario specific maximum sizes.
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 assume that we would keep these scenario specific maximum sizes.
Yup.
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.
Might also want to harmonize with the jit's threshold for converting fixed sized locallocs to locals, which is currently 32. Back when I set this I did not see many > 32 cases.
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.
Here we have char
, so that threshold would be 16 (= 32 bytes)?
cc @JeremyKuhne Is this change compatible with what we are planning around more efficient formatting? |
|
||
do | ||
{ | ||
currentValue = en.Current; | ||
|
||
if (currentValue != null) | ||
{ | ||
result.Append(currentValue.ToString()); | ||
string? str = currentValue.ToString(); |
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.
There is a chance that the T
supports ISpanFormattable
. Calling the object overload should avoid the additional string allocation in that case.
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.
Yes, it should be done in separate change.
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's certainly possible, but I have to wonder how common it actually is. Calling Concat (not Join) with a bunch of integers, floating-point values, Guids, DateTimes, and TimeSpans is going to result in a bunch of gobbledeegook, as there won't be any separators between the values. Do you have an example where this would happen and be worth optimizing for? My gut would be to avoid the interface check as it actually succeeding would be super rare.
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 thinking more of the future- in theory we'd want to make this interface public eventually, no?
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 thinking more of the future
Even then, what's an example where this would be valuable? It would need to be a T
where performance is important and where TryFormat would include the necessary punctuation or spacing to make Concat a useful rendering.
In trying to find such examples (that might implement ISpanFormattable
in the future), I'm not finding any... in fact I'm finding very little usage of this overload in general.
If you have such cases in mind, I'd be interested in seeing them.
Thanks.
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 would need to be a T where performance is important ...
And then you probably wouldn't want to be using an IEnumerable<T>
overload?
Yes, this would. cc: @jaredpar |
|
@@ -299,7 +303,8 @@ internal string ToString(TraceFormat traceFormat) | |||
// Append original method name e.g. +MoveNext() | |||
sb.Append('+'); | |||
sb.Append(methodName); | |||
sb.Append('(').Append(')'); | |||
sb.Append('('); | |||
sb.Append(')'); |
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 haven't tested... is sb.Append('('); sb.Append(')');
significantly faster than sb.Append("()");
, and if so, is it in the context of this method? Otherwise, I'd just assume using the simpler call.
if (_innerException != null) | ||
{ | ||
innerException = _innerException.ToString(); | ||
} |
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.
string? innerException = _innerException?.ToString();
?
innerException = _innerException.ToString(); | ||
} | ||
|
||
int initialLength = 128; |
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.
Where does this 128
come from? Would be worth a comment, even if it's just to say it's an arbitrary guess at the size required.
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.
Might be clearer to just have + 128
when calling the constructor.
.Append(' '); | ||
sb.Append(((IFormattable)this.GetElement(i)).ToString("G", invariant)); | ||
sb.Append(','); | ||
sb.Append(' '); |
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.
Same question/comment as earlier... I wonder if sb.Append(", ");
would be better, just for simplicity reasons.
@@ -646,17 +667,41 @@ public static string Join(string? separator, IEnumerable<string?> values) | |||
} | |||
|
|||
// Null separator and values are handled by the StringBuilder |
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.
Per the other conversation we should probably change Append to accept nulls again. But if we don't, this comment will need to be updated.
result.Append(str); | ||
} | ||
} | ||
while (en.MoveNext()); |
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 assume this duplication can be reverted once Append is changed to allow null? (Even if it's not, I think it's worth avoiding the duplication and just having the explicit null check on each append for the separator.)
if (provider != null) | ||
{ | ||
cf = (ICustomFormatter?)provider.GetFormat(typeof(ICustomFormatter)); | ||
} |
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.
var cf = (ICustomFormatter?)provider?.GetFormat(typeof(ICustomFormatter));
?
|
||
int pos = 0; | ||
int len = format.Length; | ||
char ch = '\x0'; |
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.
Nit: '\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.
Was just a copy paste from StringBuilder https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Text/StringBuilder.cs#L1549
But makes sense to change.
break; | ||
} | ||
} | ||
// If it neither then treat the character as just text. |
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.
Nit: "If it neither" => "If it's neither"
Also it'd be nice if there were a blank line above this.
} | ||
} | ||
// Is it a opening brace? | ||
if (ch == '{') |
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.
else if?
// Index ::= ('0'-'9')+ WS* | ||
// | ||
pos++; | ||
// If reached end of text then error (Unexpected end of text) |
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.
Nit: blank line above this
ch = format[pos]; | ||
|
||
pos++; | ||
// Is it a closing brace? |
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.
Nit: blank line above this
// | ||
// Start of parsing of Argument Hole. | ||
// Argument Hole ::= { Index (, WS* Alignment WS*)? (: Formatting)? } | ||
// |
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.
Nit: can you remove the //
s followed by nothing?
{ | ||
FormatError(); | ||
} | ||
ch = format[pos]; |
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.
Nit: blank line above this
{ | ||
FormatError(); | ||
} | ||
// Parse alignment digits. |
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.
Nit: blank line above this
{ | ||
width = width * 10 + ch - '0'; | ||
pos++; | ||
// If reached end of text then error. (Unexpected end of text) |
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.
Nit: blank line above this. I'll stop commenting on these.
} | ||
} | ||
|
||
private static void FormatError() |
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.
Nit: "FormatError" => "ThrowFormatException"
@@ -822,6 +822,7 @@ | |||
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UTF7Encoding.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)System\Text\UTF8Encoding.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)System\Text\ValueStringBuilder.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)System\Text\ValueStringBuilder.AppendFormat.cs" /> |
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.
Can we remove StringBuilderCache.cs from corelib as well, or are there still other uses?
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.
There's still a few other uses; they are more problematic ones as one method gets the cached string builder, then it returns it, passes to other methods, then releases it later. Didn't tackle them as it would over-complicate the review.
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.
Thanks for working on this.
Replaced by #27441 |
Saves 44MB of allocations for the 20k Exceptions thrown async, caught, captured,
.Throw()
, caught and thenthrow
https://gist.github.com/benaadams/49633777609ed91e8b41624b72026bf9Before
After