-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: TryForSufficientStack method to support stackalloc usage #24963
Comments
|
FYI, there already is a public RuntimeHelpers.TryEnsureSufficientExecutionStack() exposed in .NET Core. |
Then I think the method proposed here should be added as its overload. |
Then probably complementary |
If the proposed EnsureSufficientExecutionStack is added what the typical usage is going to look like? Note that stackalloc should be only used for relatively small buffers (say up to 1kB). Array pool is more appropriate above this limit. The framework is using stackalloc in number of places and the typical pattern looks like this: https://github.com/dotnet/coreclr/blob/efebb38f3c18425c57f94ff910a50e038d13c848/src/mscorlib/shared/System/Text/ValueStringBuilder.cs Would it be better to work on making this pattern easier instead? |
For example, I was thinking about various scenarios where some temporary data collections have to be calculated. Instead of using an array or even ArrayPool, we could utilize stack with almost no time overhead:
But currently every usage of I mean, I know it is quite a big size guaranteed already by |
So what would your code example look like if you had EnsureSufficientExecutionStack API? |
Large stack buffers do have overhead: The stack memory has to be faulted in (one page at a time typically), and then it tends to be stuck in the working set. |
Something like:
Isn't the same for faulting in memory from ArrayPool? I mean, if Additionally, the point is that even with small buffers of 1 kB the |
I do not think I would want to use this pattern because of it makes me duplicate the code.
No - if the buffer is reused on multiple threads, or different callers on the same thread. There needs to be best practice for allocating large buffers. If half of the folks are going to do it via ArrayPool and other half with large stack allocs, everybody loses at the end. |
Ok, this was a poorly prepared example. I was thinking more about something like 'generic temporary collection factory' without repeating any processing logic:
However, this currently does not compile with "A result of a stackalloc expression of type 'Span' cannot be used in this context because it may be exposed outside of the containing method" error. Is it supposed to be ok? Because https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.2/span-safety.md says that "A stackalloc expression is an rvalue that is safe-to-escape to the top-level scope of the method (but not from the entire method itself)".
Here I agree fully and I see your point. What still bothers me is the fact that |
Right, it does not compile. Also, missing in your example is code to return the buffer to the pool once you are done. The pattern that works today looks like this: https://github.com/dotnet/coreclr/blob/efebb38f3c18425c57f94ff910a50e038d13c848/src/mscorlib/shared/System/Text/ValueStringBuilder.cs Or another example: https://github.com/dotnet/corefx/pull/26942/files#diff-a07e741d448a2771beaea007ab99a266 |
Though its likely unsensible to do on a different thread than the one you are running on (unless in a Wait?); how about some utility apis for querying stack size instead? So currently there is a public Thread (ParameterizedThreadStart start, int maxStackSize); But you can't query it back to find out what it was set to; so maybe something like the following would be better? public partial class Thread
{
public int TotalStackSize { get; }
public int AvailableStackSize { get; }
public int UsedStackSize { get; }
} Its also less ambiguous with guarantees that |
Or perhaps in namespace System.Diagnostics.ThreadInfo
{
public static class ThreadInfoExtensions
{
public static int TotalStackSize(this Thread thread);
public static int AvailableStackSize(this Thread thread);
public static int UsedStackSize(this Thread thread);
}
} |
Moving it away from "mainstream" APIs like |
It does not make sense for these methods to take |
Would it be more convenient if the user didn't have to compute the required size in bytes, they would just specify the type and the count of items of that type to allocate? So instead of: RuntimeHelpers.TryEnsureSufficientExecutionStack(list.Count * PUT_HERE_YOUR_STRUCT_SIZE) You would have: RuntimeHelpers.TryEnsureSufficientExecutionStack<SomeStruct>(list.Count) Though being able to specify the size in bytes is probably still useful, so this could be just a convenience overload (possibly added later than the one proposed here). |
I like this idea. |
I do appreciate this idea, but my intuition suggests that it's a lot less useful than it seems on its face. Thinking about when you'd need a really big buffer (like, bigger than a 4K OS page) that could legally fit on the stack, allocating that buffer on the stack feels like it could very often cause a page fault when trying to return to the caller. Since we're probably talking about situations where these scratch buffers are of input-dependent lengths, then it seems to follow that larger buffers will correlate with more time spent in the method, which means that I see a potential for a bit of a "perfect storm":
I'd imagine that someone could produce a contrived microbenchmark that shows this as being even worse than unconditionally allocating on the heap; more likely, each method will have its own threshold where it starts making sense to use Furthermore, it feels like as soon as we write a
I only bring up the above because several examples in the thread use Finally, what are some actual situations that we're worried about here?
Again, though, I do appreciate the idea, and the sample in the original issue description seems to suggest that it's easy to implement. I just wanted to make sure to share my thoughts on it (it's been on my mind a ton since @svick linked it from dotnet/roslyn#25118), even if all it does is inspire a "Remarks" section in the docs. |
Completely agree with @airbreather . Similar discussion at dotnet/csharplang#1344 |
I really like the direction of this discussion. My primary reasons for this proposal were twofold:
At the moment, I fully agree with the arguments presented by you. However, the fact that such I like @jkotas idea of |
RuntimeHelpers.TryEnsureSufficientExecutionStack(
size: list.Count * STRUCT_SIZE, calledMethods: 10) |
There are a couple of issues that feed into this. One is that the stack size varies widely on platforms now (for instance 2mb on windows and a default of 8mb on Ubuntu). Also I am not sure about allocating large chunks of stack until we get the "don't zero the stack". Without that its faster to pull from the array pool because zeroing a large array will cost you more and as said it will need to be faulted in anyway. |
@svick looking through the code, the rule-of-thumb used by On the one hand, that certainly feels like it would be safe enough, but on the other hand, is it maybe a bit too safe? If I'm a leaf method that's 50KiB away from overflowing the stack (which might actually start happening because of callers over-eagerly stack-allocating because of this guard), and this guard stops me from allocating 400B, then I'm not happy.
@kkokosa Without further context, if all you're given is a question phrased like "how much can I Anyway, without that context, isn't the best answer still going to be something like that? "Probably something around 1 KiB"? The helper utility would only help to remove the "stack burned so far" part, not the "stack burned after the It's almost like the more useful helper would be to just give a utility method that answers "how many more bytes can I push onto the stack before I hit the guard page?" (edit: or was it the page that comes immediately "before" the true guard page?) and just leave it up to the caller to interpret that according to their specific knowledge of what they intend to do in the future. |
In PowerShell repo we got first experience of using Span and const stackallocTheshold = 120;
Span<int> array;
int size = GetArraySize();
if (size < stackallocTheshold)
{
array = stackalloc int[size];
}
else
{
array = new int[size]
} Based on the discussion we would have to implement: const RedLineStackallocTheshold = 1Kb; // ???
const StackallocTheshold = 120;
Span<int> array;
int size = GetArraySize();
if (size < StackallocTheshold && AvailableStackSize() > RedLineStackallocTheshold)
{
array = stackalloc int[size];
}
else
{
array = new int[size]
}
Fallback to heap can be interesting proposal for the discussion. |
Maybe the method should return the number of bytes left. That way the caller can apply any logic they like. This would simplify the API as well. No need for a size parameter and comparison. |
#25423 is a better proposal solving the same problem. |
From @kkokosa on February 8, 2018 12:8
Due to changes in C# 7.2 and Span, more and more
stackalloc
usages may become popular like:Span<byte> span = stackalloc byte[1000];
However, this call will end up with unrecoverable
StackOverflowException
if there is not enough stack space left. We can do nothing in such situation which makes this approach not useful at some point. It is now just completely unreliable to guess whatstackalloc
size may end up with such a catastrophe.@jkotas pointed out in dotnet/corefx#14675 that
RuntimeHelpers.EnsureSufficientExecutionStack
is a reliable solution for handling stack overflow in general but as MSDN says, this method "ensures that the remaining stack space is large enough to execute the average .NET Framework function". However, probing for average .NET framework function is not very helpful asstackalloc
makes it not average for sure.I propose to add a new helper method which gives at least some clue whether our
stackalloc
may end up withStackOverflowException
:public static bool RuntimeHelpers.TryForSufficientStack(long size)
I believe returning
bool
instead of throwing an exception (likeInsufficientExecutionStackException
from above method) is better becausestackalloc
is most probably used in hot paths already and adding exception handling there is rather undesirable.As far as I understand this method seems to be quite simple in terms of implementation as all necessary data are there already. My naive implementation proposal:
PS. I am not sure whether stack guard size should be taken into consideration here or not...
Copied from original issue: dotnet/coreclr#16277
The text was updated successfully, but these errors were encountered: