-
Notifications
You must be signed in to change notification settings - Fork 2.6k
GetEnvironmentVariable: Avoid StringBuilder marshaling overhead #14502
Conversation
Try a stack allocated buffer first, then fallback to using the shared array pool.
| { | ||
| StringBuilder sb = StringBuilderCache.Acquire(128); // A somewhat reasonable default size | ||
| int requiredSize = Win32Native.GetEnvironmentVariable(variable, sb, sb.Capacity); | ||
| Span<char> buffer = stackalloc char[128]; // A somewhat reasonable default size |
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.
is it reasonable? since this is now a stack allocation, maybe it's cheap to be more generous? although, this is probably fine for most variables except path, include, libpath, etc.
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 do we have a rule of thumb for reasonable stackalloc max? In the CoreFX code I see mostly a few bytes, a cutoff of 100, 200, one of 256 is the largest.
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 had the same question. 128 chars (256 bytes) seemed like a reasonable limit... but I could see going a little higher. Not sure how far we want to push it, though.
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 don't have a hard and fast rule, but we've generally stayed at <= 256 bytes.
| int requiredSize = Win32Native.GetEnvironmentVariable(variable, buffer); | ||
|
|
||
| if (requiredSize == 0 && Marshal.GetLastWin32Error() == Win32Native.ERROR_ENVVAR_NOT_FOUND) | ||
| if (requiredSize > buffer.Length) |
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.
confirmed that GetEnvironmentVariable returns the length including terminating null. StringBuilder.Capacity does not include terminating null so that would not have mattered before.
|
|
||
| [DllImport(Interop.Libraries.Kernel32, CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false)] | ||
| internal static extern int GetEnvironmentVariable(string lpName, [Out]StringBuilder lpValue, int size); | ||
| private static extern unsafe int GetEnvironmentVariable(string lpName, char* lpValue, int size); |
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.
you've also eliminated the StringBuilder marshaling it was doing before, which forced a copy.
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/interop-pinvokes.md
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.
Yup :)
danmoseley
left a comment
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.
Looks good to me - thanks! I'll let @stephentoub have a quick look in case I missed some subtlety.
|
@dotnet-bot test Windows_NT x86 full_opt legacy_backend CoreCLR Perf Tests Correctness please (nolog) |
| sb.Capacity = requiredSize; | ||
| sb.Length = 0; | ||
| requiredSize = Win32Native.GetEnvironmentVariable(variable, sb, sb.Capacity); | ||
| return Marshal.GetLastWin32Error() == Win32Native.ERROR_ENVVAR_NOT_FOUND ? null : string.Empty; |
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: I would move the check for error condition right after the PInvoke to make it easier to see that there is nothing modifying Marshal.GetLastWin32Error().
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.
Done. I also changed the condition back to the way it was as the string ctor will take care of returning string.Empty for empty spans (when requiredSize == 0 and the error isn’t “not found”).
22146e1 to
05bf177
Compare
|
I had added
Thus, I've added a commit that removes
Let me know if you have any concerns |
Try a stack allocated buffer first, then fallback to using the shared array pool.
Results on my machine:
"TMP"Before"TMP"After"PATH"Before"PATH"After"TMP"is under the stack limit and"PATH"is over the limit (uses the pool).