Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Remove some of usages of StringBuilderCache from System.Runtime.Extensions#30072

Merged
jkotas merged 14 commits intodotnet:masterfrom
leotsarev:remove-stringbuildercache-from-system-runtime-extensions
Jun 2, 2018
Merged

Remove some of usages of StringBuilderCache from System.Runtime.Extensions#30072
jkotas merged 14 commits intodotnet:masterfrom
leotsarev:remove-stringbuildercache-from-system-runtime-extensions

Conversation

@leotsarev
Copy link
Contributor

@leotsarev leotsarev commented Jun 2, 2018

fixes #29997
That's a work in progress to get initial feedback about removing.
Checklist

  • System.ApplicationId
  • System.Environment.ExpandEnvironmentVariablesCore()

ExpandEnvironmentVariablesCore benchmark

 Metric Unit Iterations Average STDEV.S Min Max
BEFORE Duration msec 16 655,7176231 83,74468942 616,708306 923,3642414
AFTER Duration msec 16 644,7916818 41,45682354 613,3610511 778,3421872
BEFORE Allocation bytes 16 48000000 0 48000000 48000000
AFTER Allocation bytes 16 48000000 0 48000000 48000000

Copy link
Member

@ViktorHofer ViktorHofer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JIT is pretty good with optimizing stackallocing constant buffer sizes. Because of that I would remove the variable part and always alloc up to a the threshold like 255, as it makes no measurable difference if you alloc 128 or 255 chars.

hexOrder[j++] = HexDigit(digit);
}
return result;
return new string(hexOrder);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you know the exact number of chars you can use the new string.Create overload with Span which is the absolute fastest way to build a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you talking about
public static string Create<TState>(int length, TState state, SpanAction<char, TState> action) ?
How I can use it — I couldn't find any clue for now...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From unit test:

char[] input = expected.ToCharArray();
            string result = string.Create(input.Length, input, (span, state) =>
            {
                Assert.Same(input, state);
                for (int i = 0; i < state.Length; i++)
                {
                    span[i] = state[i];
                }
            });

That's really the faster than new string(span) ??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the fastest way to create a string. For more info around that read the midsection about Span in the Runtime from @stephentoub: https://msdn.microsoft.com/en-us/magazine/mt814808.aspx

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course without creating the char array from the unit test... You just need the length, you want to avoid heap allocations where possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes pushed, thanks for enlightening.
Am I right that we are relying that JIT will optimize out allocation of lambda here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @stephentoub for delegate costs.

result = new string(hexOrder);
if (sArray == null) return null;

Span<char> hexOrder = stackalloc char[sArray.Length * 2];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you want to be defensive here to avoid a stack overflow. I don't know what are the usual sizes of the given array is, but a good rule is to limit the stackalloc size to ~255 chars. As already commented below, if you know the fixed size to begin with and it's not too large for allocating on the stack you probably want to use string.Create instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stackalloc removed here

@leotsarev leotsarev changed the title [WIP] Remove StringBuilderCache from System.Runtime.Extensions Remove StringBuilderCache from System.Runtime.Extensions Jun 2, 2018
public override string ToString ()
{
StringBuilder sb = StringBuilderCache.Acquire();
Span<char> charSpan = stackalloc char[255];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a typical length of these ApplicationIds? 255 is fine, but if they're all going to be much less than that, it'd be nice to save on the stack space by using 128 or something smaller (but only if we think that the 99% case will fit into that smaller size).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

culture="en-us", version="1.0.0.0", publicKeyToken="43cb1e8e7a352766", processorArchitecture="x86"
has 98 chars, leaving 30 chars for application name. I don't have a statictics for application names, but I met already 38 char exe name in wild. I'm rather go to 255. What's your decision?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me. thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I met already 38 char exe name in wild

As Stephen said, this is about optimizing for the 99% case. I think that it is safe to say that 99% of the .exes out there have name shorter than 38 chars.

Copy link
Member

@danmoseley danmoseley Jun 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ViktorHofer you said

The JIT is pretty good with optimizing stackallocing constant buffer sizes

Could you explain? I'm not familiar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

result = new string(hexOrder);
}
return result;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of string.Create to avoid the char[] allocation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kudos to @ViktorHofer

Copy link
Member

@jkotas jkotas Jun 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to append the characters directly to the ValueStringBuilder, and completely avoid the String allocation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and done

StringBuilder sb = StringBuilderCache.Acquire(value.Length);
HtmlEncode(value, index, sb);
return StringBuilderCache.GetStringAndRelease(sb);
Span<char> charSpan = stackalloc char[value.Length];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dangerous. value could be really big, in which case we'd try to stackalloc that much space and could easily result in a stack overflow / crash. This should instead just using a fixed amount, e.g. 256, or not use stack space at all and just pass an empty span to ValueStringBuilder. Or update ValueStringBuilder with a capacity constructor.

Note, though, that @benjamin-hodgson already has a PR that addresses WebUtility at #27250, so it probably makes sense to just undo these WebUtility.cs changes and let his PR handle that.

Copy link
Member

@ViktorHofer ViktorHofer Jun 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of this PR I would recommend to just pass a stackalloc char[255] to the ValueStringBuilder and let @benjamin-hodgson fine grain the stack allocation in his PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ViktorHofer, that makes me nervous, given that these changes showed a regression on throughput in his PR: #27250 (comment).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that's unfortunate, I didn't know about the regression.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, do you like me to undo WebUtility changes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but you also need to undo the removal of the StringBuilderCache from the assembly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course: done

@leotsarev leotsarev changed the title Remove StringBuilderCache from System.Runtime.Extensions Remove some of usages of StringBuilderCache from System.Runtime.Extensions Jun 2, 2018
@ViktorHofer
Copy link
Member

ViktorHofer commented Jun 2, 2018

For the ApplicationId.ToString() and the Environment.ExpandEnvironmentVariablesCore (Unix) changes we would need perf measurements. Could you please do that?

@@ -51,7 +52,7 @@ private static string ExpandEnvironmentVariablesCore(string name)
}
result.Append(name.Substring(lastPos));
Copy link
Member

@jkotas jkotas Jun 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can replace result.Append(name.Substring will result.Append(name.AsSpan to void unnecessary allocations while you are on it.

Copy link
Contributor Author

@leotsarev leotsarev Jun 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's still one left in line 53

@danmoseley
Copy link
Member

Thanks @leotsarev

@leotsarev
Copy link
Contributor Author

Thanks everybody for input.
@ViktorHofer I'll try to do add perfomance tests

public override string ToString ()
{
StringBuilder sb = StringBuilderCache.Acquire();
Span<char> charSpan = stackalloc char[127];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

127 is a weird number. Why not use 128 which spans from 0 - 127?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The space on the stack is aligned to pointer sized chunks anyway. If you use 127, there will be one unused byte.

128 is better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Member

@ViktorHofer ViktorHofer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perf tests would be nice but not a must for me. Thanks a lot! 👍

@karelz karelz added the Hackathon Issues picked for Hackathon label Jun 2, 2018
@leotsarev
Copy link
Contributor Author

@ViktorHofer I measured old & new implementation of ExpandEnvironmentVariablesCore (using existing perf test)
It shows negligible perfomance win (near margin of error), but no win on allocations. Latter makes me suspicions about should we trust this result.
However, artificially adding allocations (by allocing array into volatile field) does show difference on this benchmark.
Does anybody have a clue why this could happen?
Do you have cloud infra to run perf test "before" and "after" PR?

@jkotas
Copy link
Member

jkotas commented Jun 2, 2018

Have you measured it on Unix? Note that the implementation you are changing is Unix specific. Could you please share the source to your perf test?

@leotsarev
Copy link
Contributor Author

@jkotas no, but I temporary changed code to use Unix-specfic code on Windows

@jkotas jkotas merged commit b987a3b into dotnet:master Jun 2, 2018
@jkotas
Copy link
Member

jkotas commented Jun 2, 2018

Thank you for your contribution. If you get to the bottom of why the perf tests do not work properly for you, please share the results. But as Victor said we are not really worried about it given the nature of the change.

@karelz karelz added this to the 3.0 milestone Jun 2, 2018
@leotsarev leotsarev deleted the remove-stringbuildercache-from-system-runtime-extensions branch June 3, 2018 16:52
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…sions (dotnet/corefx#30072)

* Remove StringBuilderCache from ApplicationId

* Remove StringBuilderCache in favor of ValueStringBuilder in Environment.Unix.ExpandEnvironmentVariablesCore

Commit migrated from dotnet/corefx@b987a3b
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove StringBuilderCache from System.Runtime.Extensions

8 participants