17

CLR 编译器/JIT 是否执行了任何转义分析?例如,在 Java 中,循环变量和分配在循环中的对象似乎不会转义循环被分配在堆栈上而不是堆上(请参阅Java 中的转义分析)。

为了澄清,在下面的示例中,编译器是否会优化堆分配,foo因为它永远不会逃脱循环。

class Foo 
{ 
   int number;
   Foo(int number) { this.number = number; }
   public override string ToString() { return number.ToString(); }
}

for (int i = 0; i < 10000000; i++)
{
   Foo foo = new Foo(i);
   Console.WriteLine(foo.ToString());
}
4

3 回答 3

15

如果您的意思是对象new Foo(i);),那么我的理解是不:这永远不会在堆栈上分配;但是,它会在零代中死亡,因此收集起来非常有效。我不自称知道 CLI 的每个黑暗和潮湿的角落,但我不知道C#中的任何场景会导致在堆栈上分配托管引用类型(诸如stackalloc不真正计数之类的事情,并且非常具体)。显然,在 C++ 中,您有更多选择,但它不是托管实例。

有趣的是,在 MonoTouch/AOT 上,它可能会立即被收集,但这不是主要的 CLI VM(并且是针对非常特定的场景)。

至于变量-通常在堆栈上(并在每次循环迭代中重复使用) - 但它可能不是. 例如,如果这是一个“迭代器块”,那么所有未删除的局部变量实际上都是编译器生成的状态机上的字段。更常见的是,如果变量被“捕获”(到匿名方法或 lambda 表达式中,两者都形成闭包),那么变量会被转换为编译器生成的捕获上下文中的字段,并且每次循环迭代都是独立的(因为foo在循环内声明)。这意味着每个在堆上都是独立的。

至于i(循环变量) - 如果被捕获,它会变得更加有趣

  • 在 C# 1.2 中不存在捕获,但根据规范,循环变量在技术上是每次迭代
  • 在 C# 2.0 到 4.0 中,循环变量是共享的(导致臭名昭著的 capture/foreach 常见问题)
  • 在 C# 5.0 及更高版本中,循环变量再次为每次迭代

这只会在变量被捕获时产生影响,但会改变它在捕获上下文中的表现方式语义

于 2011-11-22T07:43:20.750 回答
6

值类型可能会在堆栈上分配(并非总是如此),但对于引用类型的实例则并非如此。实际上:

特别是,引用类型实例的存储位置总是被视为长期存在的,即使它们被证明是短暂的。因此它们总是在堆上。

(Eric Lippert:关于值类型的真相

《堆栈是一个实现细节》也是一本很好的读物。

于 2011-11-22T07:26:30.787 回答
1

虽然 x86 JIT 擅长“内联”值类型,但您的代码段将不符合条件,因为该ToString方法将是对装箱对象的虚拟调用。编辑:情况可能并非如此,因为您没有覆盖ToString.

然而,从我的实验来看,x64 JIT 根本没有这样做。

编辑:

如果可能,请在 x86 和 x64 上测试您的代码。

于 2011-11-22T07:03:03.023 回答