Skip to content

Reduce 99% of allocations when starting processes with derived env vars on Unix #126162

@adamsitnik

Description

@adamsitnik

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:

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=value strings (code)
  • allocating an unmanaged memory to hold utf8 representation (code)
  • encoding string to utf8 (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 to Environment.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_spawn on 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.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions