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

Conversation

@justinvp
Copy link

Try a stack allocated buffer first, then fallback to using the shared array pool.

Results on my machine:

Method Mean Error StdDev Gen 0 Allocated
"TMP" Before 156.51 ns 2.0132 ns 1.8831 ns 0.0875 368 B
"TMP" After 92.88 ns 0.5002 ns 0.4434 ns 0.0209 88 B
"PATH" Before 621.59 ns 2.2668 ns 2.1204 ns 0.9565 4016 B
"PATH" After 287.02 ns 1.0377 ns 0.8666 ns 0.2704 1136 B

"TMP" is under the stack limit and "PATH" is over the limit (uses the pool).

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
Copy link
Member

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.

Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

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)
Copy link
Member

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);
Copy link
Member

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

Copy link
Author

Choose a reason for hiding this comment

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

Yup :)

Copy link
Member

@danmoseley danmoseley left a 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.

@danmoseley
Copy link
Member

@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;
Copy link
Member

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().

Copy link
Author

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”).

@justinvp justinvp force-pushed the getenvironmentvariable branch from 22146e1 to 05bf177 Compare October 16, 2017 18:06
@justinvp
Copy link
Author

I had added clearArray: true when returning the array to the pool out of caution and it didn't appear to significantly impact the perf, but re-running the microbenchmarks again does show a slowdown.

clearArray: true:

Method Mean Error StdDev Gen 0 Allocated
Before 702.23 ns 2.0626 ns 1.8284 ns 1.1282 4736 B
After 723.63 ns 2.8812 ns 2.5541 ns 0.3271 1376 B

clearArray: false:

Method Mean Error StdDev Gen 0 Allocated
Before 684.89 ns 3.7051 ns 3.2845 ns 1.1282 4736 B
After 316.44 ns 1.9137 ns 1.7901 ns 0.3276 1376 B

Thus, I've added a commit that removes clearArray: true since:

  • Any code in the process can already read environment variables whenever it wants
  • Clearing the array before returning it to the pool is not done anywhere else in corelib
  • CoreRT's ExpandEnvironmentVariables implementation uses the pool and doesn't clear the array

Let me know if you have any concerns

@jkotas jkotas merged commit 2f01c28 into dotnet:master Oct 17, 2017
@justinvp justinvp deleted the getenvironmentvariable branch October 17, 2017 21:39
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants