-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Reduce 99% of allocations when starting processes with derived env vars on Unix #126162
Description
I've noticed that on Unix we allocate way more managed memory than on Windows when starting new Process with default env vars.
On Windows, when the user has not modified the environment variables exposed by ProcessStartInfo, we just don't need to prepare the environment variables block:
runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
Lines 482 to 487 in 116bd0d
| string? environmentBlock = null; | |
| if (startInfo._environmentVariables != null) | |
| { | |
| creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; | |
| environmentBlock = GetEnvironmentVariablesBlock(startInfo._environmentVariables!); | |
| } |
It's derived from the parent process (doc):
A pointer to the environment block for the new process. If this parameter is NULL, the new process uses the environment of the calling process.
According to LLMs, BSD-based Unixes treat NULL passed as envp to execve as "Use the current process environment".
However, I was not able to verify that using existing docs (so even if it's an implementation detail, we should not rely on that).
So let's assume there is documented mechanism to just inherit the env vars from the parent process on Unix.
Both posix_spawn and execve expect char *const envp, which can be obtained just by using environ.
The problem is that environ is global mutable process state (it can be modified using setenv/unsetenv/putenv methods).
But.. fork creates a copy of the parent process and we need to provide the pointer to execve. So my understanding is that it's safe to pass environ obtained after fork to execve.
Which means that if the user has not modified env vars exposed by ProcessStartInfo we can pass null to the native layer (SystemNative_ForkAndExecProcess) and avoid the following:
- creating an array of
key=valuestrings (code) - allocating an unmanaged memory to hold utf8 representation (code)
- encoding
stringtoutf8(code)
When I've run such experiment in my proof of concept repo (adamsitnik/ProcessPlayground#43), I got following bechmark results:
156x less memory allocation for synchronous execution (102KB → 0.57KB)
34x less memory allocation for async execution (104KB → 2.6KB)
There are two gotaches:
- BCL itself is not modyfing
environ, we would need to make it perform a sys-call after each call toEnvironment.SetEnvironmentVariable. I believe it's fine as it's rather rare to set your own env vars compared to starting processes. - If we start using
posix_spawnon macOS, we will have to perform such copy or introduce a global shared lock in the native layer.
@tmds @am11 is my thinking process correct? I would like to verify my assumptions before I create a PR.