我在阅读“通过 C# 实现 CLR”时,在阅读“事物在运行时的关系”和“原始类型、引用类型和值类型”时,我有点困惑。 如果我从 Main 调用了以下代码 -
void DoSomething(int x)
{
int m = x/2;
int n = SomeMethod1(m);
n = (n * 2) + x;
int k = SomeMethod2(n);
m += (k*3);
}
该代码没有任何用处,但我只是想了解局部整数变量和内存分配的行为。
我了解 m、n 和 k 驻留在堆栈中? 现在,对于最后一行(我忽略了变量 n),必须修改“m”的值。所以堆栈上的所有其他东西都必须被弹出并更新'm'的值?与堆上装箱和拆箱的开销相比,这可以忽略不计吗?如果堆叠高度更高,它会变得重要吗?
P.S:我将 GC 排除在讨论之外(用于装箱和拆箱),这绝对是一种开销,而且这只是为了尝试理解行为,可能/可能不是实际场景。
最佳答案
不,没有人说 m、n 或 k 应该在堆栈上。最有可能的是,它们会在寄存器中,并且由于您甚至没有使用相互依赖性,它们甚至不必同时存在。 JIT 编译器非常擅长运行时优化,堆栈几乎不用于方法参数(或其引用)以外的任何东西。并且 GC 在寄存器或堆栈中都没有权力 - 堆栈的释放方式与 native 代码中的方式相同。只有堆是 GC 的一个域。
这是一个常见的误解,因为在 IL(C# 编译成的中间语言)中,实际上 一切都使用堆栈。但是,这不是在您的机器上执行的内容 - IL 代码由 JIT 编译器再次编译。当然,JIT 编译器也可以自由地处理堆栈上的所有事情,但那是愚蠢的。简单示例时间:
x + y
将编译成这个 IL(伪代码):
push y
push x
op_Add
所以对于一个简单的整数加法,需要进行一次方法调用和两次入栈。不是很贵,但如果你想做任何严肃的计算,你就有麻烦了。
但是,JIT 编译器会产生更像这样的东西:
add ax, bx
它在这方面非常聪明,所以它实际上经常将寄存器用于方法参数——如果它是安全的话。
但是请注意,所有这些都只是一个实现细节——在本例中是性能优化。整数可能很容易存在于堆上(事实上,如果它是装箱的或存在于堆上的另一个对象的一部分)。
因此,显然,最快的是 CPU 本身支持的那些 - 如上例所示,将两个寄存器加在一起。非常快。
使用栈通常还是很快的。并不是说分配给堆很昂贵——事实上,堆和堆栈分配的成本大致相同。伤害的是释放 - 堆需要进行垃圾收集和压缩,而堆栈只需要更改单个指针。
哦,您误解了访问堆栈的方式。的确,有基本的 push
和 pop
指令,但是,这些不是唯一的方式。您可以直接寻址堆栈的内存,就像寻址任何其他内存一样。事实上,这就是为什么您可以看到堆栈指针(如 ESP)的全部原因。
关于c# - 原始类型不变性和堆栈?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25781939/