When does a local variable inside a function *actu

2020-03-26 00:55发布

Just curious about this. Following are two code snippets for the same function:

void MyFunc1()
{
    int i = 10;
    object obj = null;

    if(something) return;
}

And the other one is...

void MyFunc1()
{
    if(something) return;

    int i = 10;
    object obj = null;
}

Now does the second one has the benefit of NOT allocating the variables when something is true? OR the local stack variables (in current scope) are always allocated as soon as the function is called and moving the return statement to the top has no effect?

A link to dotnetperls.com article says "When you call a method in your C# program, the runtime allocates a separate memory region to store all the local variable slots. This memory is allocated on the stack even if you do not access the variables in the function call."

UPDATED
Here is a comparison of the IL code for these two functions. Func2 refers to second snipped. It seems like the variable in both the cases are allocated at the beginning, though in case of Func2() they are initialized later on. So no benefit as such I guess.

ILCode in ILDisassembler

2条回答
迷人小祖宗
2楼-- · 2020-03-26 01:30

Peter Duniho's answer is correct. I want to draw attention to the more fundamental problem in your question:

does the second one have the benefit of NOT allocating the variables when something is true?

Why ought that to be a benefit? Your presumption is that allocating the space for a local variable has a cost, that not doing so has a benefit and that this benefit is somehow worth obtaining. Analyzing the actual cost of local variables is very, very difficult; the presumption that there is a clear benefit in avoiding an allocation conditionally is not warranted.

To address your specific question:

The local stack variables (in current scope) are always allocated as soon as the function is called and moving the return statement to the top has no effect?

I can't answer such a complicated question easily. Let's break it down into much simpler questions:

Variables are storage locations. What are the lifetimes of the storage locations associated with local variables?

Storage locations for "ordinary" local variables -- and formal parameters of lambdas, methods, and so on -- have short, predictable lifetimes. None of them live before the method is entered, and none of them live after the method terminates, either normally or exceptionally. The C# language specification clearly calls out that local variable lifetimes are permitted to be shorter at runtime than you might think if doing so does not cause an observable change to a single-threaded program.

Storage locations for "unusual" local variables -- outer variables of lambdas, local variables in iterator blocks, local variables in async methods, and so on -- have lifetimes which are difficult to analyze at compile time or at run time, and are therefore moved to the garbage-collected heap, which uses GC policy to determine the lifetimes of the variables. There is no requirement that such variables ever be cleaned up; their storage lifetime can be extended arbitrarily at the whim of the C# compiler or the runtime.

Can a local which is unused be optimized away entirely?

Yes. If the C# compiler or the runtime can determine that removing the local from the program entirely has no observable effect in a single-threaded program, then it may do so at its whim. Essentially this is reducing its lifetime to zero.

How are storage locations for "ordinary" locals allocated?

This is an implementation detail, but typically there are two techniques. Either space is reserved on the stack, or the local is enregistered.

How does the runtime determine whether a local is enregistered or put on the stack?

This is an implementation detail of the jitter's optimizer. There are many factors, such as:

  • whether the address of the local could possibly be taken; registers have no address
  • whether the local is passed as a parameter to another method
  • whether the local is a parameter of the current method
  • what the calling conventions are of all the methods involved
  • the size of the local
  • and many, many more factors

Suppose we consider only the ordinary locals which are put on the stack. Is it the case that storage locations for all such locals are allocated when a method is entered?

Again, this is an implementation detail, but typically the answer is yes.

So a "stack local" that is used conditionally would not be allocated off the stack conditionally? Rather, its stack location would always be allocated.

Typically, yes.

What are the performance tradeoffs inherent in that decision?

Suppose we have two locals, A and B, and one is used conditionally and the other is used unconditionally. Which is faster:

  • Add two units to the current stack pointer
  • Initialize the two new stack slots to zero

or

  • Add one unit to the current stack pointer
  • Initialize the new stack slot to zero
  • If the condition is met, add one unit to the current stack pointer and initialize the new stack slot to zero

Keep in mind that "add one" and "add two" have the same cost.

This scheme is not cheaper if the variable B is unused, and has twice the cost if it is used. That's not a win.

But what about space? The conditional scheme uses either one or two units of stack space but the unconditional scheme uses two regardless.

Correct. Stack space is cheap. Or, more accurately, the million bytes of stack space you get per thread is insanely expensive, and that expense is paid up front, when you allocate the thread. Most programs never use anywhere close to a million bytes of stack space; trying to optimize use of that space is like spending an hour deciding whether to pay $5.01 for a latte vs $5.02 when you have a million dollars in the bank; it's not worth it.

Suppose 100% of the stack-based locals are allocated conditionally. Could the jitter put the addition to the stack pointer after the conditional code?

In theory, yes. Whether the jitter actually makes this optimization -- an optimization which saves literally less than a billionth of a second -- I don't know. Keep in mind that any code the jitter runs to make the decision to save that billionth of a second is code that takes far more than a billionth of a second. Again, it makes no sense to spend hours worrying about pennies; time is money.

And of course, how realistic is it that the billionth of a second you save will be the common path? Most method calls do something, not return immediately.

Also, keep in mind that the stack pointer is going to have to move for all the temporary value slots that aren't enregistered, regardless of whether those slots have names or not. How many scenarios are there where the condition that determines whether or not the method returns itself has no subexpression which touches the stack? Because that's the condition you're actually proposing that gets optimized. This seems like a vanishingly small set of scenarios, in which you get a vanishingly small benefit. If I were writing an optimizer I would spend exactly zero percent of my valuable time on solving this problem, when there are far juicier low-hanging fruit scenarios that I could be optimizing for.

Suppose there are two locals that are each allocated conditionally under different conditions. Are there additional costs imposed by a conditional allocation scheme other than possibly doing two stack pointer moves instead of one or zero?

Yes. In the straightforward scheme where you move the stack pointer two slots and say "stack pointer is A, stack pointer + 1 is B", you now have a consistent-throughout-the-method way to characterize the variables A and B. If you conditionally move the stack pointer then sometimes the stack pointer is A, sometimes it is B, and sometimes it is neither. That greatly complicates all the code that uses A and B.

What if the locals are enregistered?

Then this becomes a problem in register scheduling; I refer you to the extensive literature on this subject. I am far from an expert in it.

查看更多
再贱就再见
3楼-- · 2020-03-26 01:40

The only way to know for sure when this happens for your program, when you run it, is to look at the code the JIT compiler emits when you run your program. None of us can even answer the specific question with authority (well, I guess someone who wrote the CLR could, provided they knew which version of the CLR you're using, and possible some other details about configuration and your actual program code).

Any allocation on the stack of a local variable is strictly "implementation detail". And the CLS doesn't promise us any specific implementation.

Some locals never wind up on the stack per se, normally due to being stored in a register, but it would be legal for the runtime to use heap space instead, as long as it preserves the normal lifetime semantics of a local vaiable.

See also Eric Lippert's excellent series The Stack Is An Implementation Detail

查看更多
登录 后发表回答