Description
Background:
It is always interesting to see the 2 points of view on a problem like this: "roslyn can optimize, and everyone would benefit" vs "We need a language guarantee because compiler optimisations are flaky, and sometimes can not make the assumption we would want to force them to (eg: nobody would argue that hints to force in-lining are not important). "
Variations of all the "what things can safely be left on the stack to make an operation non-allocating" set of discussions seems to be the primary place this occurs, and it often gets bogged down in escape analysis (eg: we can't do the above (stack alloc for local functions) for lambdas because we can't force a complaint implementation).
This is especially true when many of the things we take as part of the language itself are merely an implementation detail (eg: async x.ConfigureAwait(false) ).
Originally posted by @AartBluestoke in dotnet/csharplang#1862 (comment)
This is also partially a language request (as we would need either a new keyword, or attributes on local variables; some way to hint to the compiler that we'd like to "memory-inline" this 'new'). This issue is also vague about if the actual allocation is on the stack or in the local memory pool.
Feature request:
In cooperation from the runtime, request that objects be optimistically stored to the local memory pool.
for now this is a new keyword (compiled into an attribute) on a local variable
locallalloc var localList = new List<int>();
This is a request that the entire object for a local variable, be stored either on the the stack, or in the local memory pool. This isn't a recursive request, just for that specific 'new' call.
Once an object has been allocated locally it can be passed into any function. The issue is how to deal with "escaping" stack references.
"escaping" in this context is defined as copying the reference to any heap allocated location.
class c{
static IEnumerable sv;
{
localalloc var localList = new List<int>();
var x=localList ; // non-allocating, because x is a local
var doStuff(x); // the *function call* itself is not allocation, although any escaping assignment internal to would need to be.
sv = localList; // this is an escaping assignment, so is an expensive allocation and object relocation;
var doOtherStuff(x); // has to pass the newly relocated memory location, not the original.
This is where the "Optimistic" part of this feature comes in. In a pay-for-play manner, escaping stack references are heap allocated on demand.
I think there are 4 places this could be done:
-
promptly on object escape; as part of the execution of the line of code x=y.
-- perhaps done by memory trap, but i'm not sure how to achieve this without increasing the cost of many different things -
on return from the function when the local memory would be unwound potentially significantly later on return from the function with localalloc.
-- This could be done via keeping a bitmap of localalloc objects that have 'escaped', and the 'escapees' and prior to function return requesting the GC relocate the object. this would hook into the concurrent mark+sweep component -
on the next GC pass
-- on return from a function that uses locallalloc, (and an object escaped?) the local memory section is not considered free until after the next GC gen0 pass. GC Gen 0 also considers any preserved local memory for object promotion. -
keep a separate section of the heap as a stack for the purpose of holding localAlloc objects - this would minimise the 'stack overflow' problem, as expanding the 'localAlloc memspace' could be done as part of a standard GC operation.
-- i think this would be hard to implement in a backwards compatible manner, as the generated IL would have to change. -
no language, just runtime optimisation
If this turns out to be cheap enough, then this could be a pure runtime issue, essentially making a new GC gen of -1 where all objects "known" to not survive can be optimistically allocated. I would propose that in this case all local variables optimistically assumed to be localloc and never escape.
On a gen0 allocation those variables that escape are flagged, and the functions flagged for rejit (yes i know we currently never rejit, but here i want to; there are other use cases for this, optimising based on inlining of children recursively, devirtualization after rejit and code elimination within child calls, etc), removing the localAlloc hint.
--
Should this be over at csharplang first, as they would say 'can't do this without runtime assistance'?