函数内的局部变量何时*实际*被分配

Posted

技术标签:

【中文标题】函数内的局部变量何时*实际*被分配【英文标题】:When does a local variable inside a function *actually* gets allocated 【发布时间】:2015-05-30 01:20:13 【问题描述】:

只是对此感到好奇。以下是同一功能的两个代码 sn-ps:

void MyFunc1()

    int i = 10;
    object obj = null;

    if(something) return;

另外一个是……

void MyFunc1()

    if(something) return;

    int i = 10;
    object obj = null;

现在第二个有没有在 something 为真时不分配变量的好处?或者总是在调用函数后立即分配本地堆栈变量(在当前范围内)并且将 return 语句移到顶部没有效果?

A link to dotnetperls.com article 说 “当你在 C# 程序中调用一个方法时,运行时会分配一个单独的内存区域来存储所有的局部变量槽。即使你不访问函数调用中的变量。”

更新 这是这两个函数的IL代码的比较。 Func2 指的是第二次剪断。似乎这两种情况下的变量都是在开始时分配的,尽管在 Func2() 的情况下它们稍后会被初始化。所以我猜没有任何好处。

【问题讨论】:

看看生成的IL code怎么样? IL 与它无关; IL 不是运行的。 jitted 代码是运行的。 那篇文章中含糊不清或具有误导性的陈述数量非常多。 【参考方案1】:

Peter Duniho 的回答是正确的。我想提请注意您问题中更根本的问题:

something 为真时,第二个是否有不分配变量的好处?

为什么这应该是一个好处?您的假设是为局部变量分配空间有成本,不这样做有好处,而且这种好处在某种程度上值得获得。分析局部变量的实际成本非常非常困难;假设有条件地避免分配有明显的好处是没有根据的。

解决您的具体问题:

总是在调用函数时立即分配局部堆栈变量(在当前范围内),并且将 return 语句移到顶部没有效果?

我不能轻易回答这么复杂的问题。让我们把它分解成更简单的问题:

变量是存储位置。与局部变量关联的存储位置的生命周期是多少?

“普通”局部变量(以及 lambda 表达式、方法等的形式参数)的存储位置具有较短且可预测的生命周期。他们都没有在方法被输入之前,也没有人在方法终止之后,无论是正常的还是异常的。 C# 语言规范明确指出,如果这样做不会导致单线程程序发生可观察到的变化,则允许局部变量的生命周期在运行时

“不寻常”局部变量的存储位置——lambda 的外部变量、迭代器块中的局部变量、异步方法中的局部变量等等——具有难以在编译时或运行时分析的生命周期,因此被移动到垃圾收集堆,它使用 GC 策略来确定变量的生命周期。没有要求永远清理这些变量;它们的存储寿命可以在 C# 编译器或运行时随心所欲地任意延长

可以完全优化未使用的本地吗?

是的。如果 C# 编译器或运行时可以确定从程序中完全删除本地在单线程程序中没有明显的影响,那么它可能会随心所欲地这样做。从本质上讲,这将其生命周期减少到零。

“普通”本地人的存储位置是如何分配的?

这是一个实现细节,但通常有两种技术。要么在堆栈上保留空间,要么注册本地空间。

运行时如何确定本地是注册还是入栈?

这是抖动优化器的实现细节。有很多因素,比如:

本地地址是否可能被占用;寄存器没有地址 本地是否作为参数传递给另一个方法 local是否是当前方法的参数 所涉及的所有方法的调用约定是什么 本地大小 还有很多很多的因素

假设我们只考虑放入堆栈的普通局部变量。是不是在输入一个方法的时候就分配了所有这些locals的存储位置?

同样,这是一个实现细节,但通常答案是肯定的。

因此,有条件使用的“本地堆栈”不会有条件地从堆栈分配?相反,它的堆栈位置将始终被分配。

通常是的。

该决定固有的性能权衡是什么?

假设我们有两个局部变量 A 和 B,一个是有条件使用的,另一个是无条件使用的。哪个更快:

向当前堆栈指针添加两个单元 将两个新的堆栈槽初始化为零

向当前堆栈指针添加一个单元 将新堆栈槽初始化为零 如果满足条件,则在当前堆栈指针上加一个单元,并将新堆栈槽初始化为零

请记住,“加一”和“加二”的成本相同。

如果变量 B 未使用,则该方案便宜,如果使用它,则成本两倍。这不是胜利。

但是空间呢?条件方案使用一个或两个堆栈空间单位,但无条件方案使用两个。

正确。堆栈空间很便宜。或者,更准确地说,每个线程获得的百万字节堆栈空间非常昂贵,并且在分配线程时预先支付了这笔费用。大多数程序从不使用接近一百万字节的堆栈空间。试图优化该空间的使用就像花一个小时决定是否花 5.01 美元买拿铁咖啡,而银行里有 100 万美元时花 5.02 美元;不值得。

假设 100% 的基于堆栈的局部变量是有条件分配的。抖动会不会把栈指针的加法放在条件代码后面?

理论上,是的。抖动是否真的进行了这种优化——实际上节省了不到十亿分之一秒的优化——我不知道。请记住,抖动运行以决定节省十亿分之一秒的任何代码都是花费超过十亿分之一秒的代码。同样,花几个小时担心一分钱是没有意义的。时间就是金钱。

当然,您节省的十亿分之一秒将成为共同路径,这有多现实?大多数方法调用做某事,而不是立即返回

另外,请记住,对于所有未注册的 临时值槽,无论这些槽是否有 名称,堆栈指针都必须移动> 与否。有多少情况下,确定方法是否返回本身的条件没有触及堆栈的子表达式?因为那是你实际提出的条件得到优化。这似乎是极少的一组场景,您从中获得的收益微乎其微。如果我正在编写一个优化器,我会花费我 0% 的宝贵时间来解决这个问题,而此时我可以针对许多更容易实现的场景进行优化。

假设有两个局部变量,每个局部变量在不同的条件下有条件地分配。除了可能进行两次堆栈指针移动而不是一次或零次之外,条件分配方案是否会带来额外的成本?

是的。在将堆栈指针移动两个槽并说“堆栈指针是 A,堆栈指针 + 1 是 B”的简单方案中,您现在有一个一致的方法来表征变量 A 和 B。如果您有条件地移动堆栈指针,有时堆栈指针是 A,有时是 B,有时两者都不是。这使得使用 A 和 B 的所有代码变得非常复杂。

如果本地人注册了怎么办?

那么这就变成了寄存器调度的问题;我建议您参考有关此主题的大量文献。我远不是这方面的专家。

【讨论】:

【参考方案2】:

确定您的程序何时发生这种情况的唯一方法,何时运行它,是查看JIT编译器发出的代码何时你运行你的程序。我们甚至都不能权威地回答具体问题(好吧,我猜写 CLR 的人可以,只要他们知道您使用的 CLR 的版本,以及可能的有关配置和实际程序代码的其他一些细节)。

本地变量堆栈上的任何分配都是严格的“实现细节”。而且 CLS 并没有向我们承诺任何具体的实现。

一些本地人从不本身在堆栈上结束,通常是由于存储在寄存器中,但运行时使用堆空间代替是合法的,只要它保留局部变量的正常生命周期语义。

另见 Eric Lippert 的优秀系列The Stack Is An Implementation Detail

【讨论】:

以上是关于函数内的局部变量何时*实际*被分配的主要内容,如果未能解决你的问题,请参考以下文章

go强大的垃圾回收机制。

java语言中,类的成员变量分配在哪个内存区?

C语言中的 局部变量,存储在啥地方?

一次定义2个int类型局部变量,这两个局部变量的地址为啥相差12个字节?

C_局部变量&全局变量

Python全栈之路----函数----局部变量