.NET CLR虚拟机中的逃逸分析

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());
}

通常,循环变量是值类型,在堆栈上分配(在循环的情况下)。 - Lasse V. Karlsen
我的意思是在循环体内分配的变量。我会更新问题以澄清。 - SimonC
你是指变量吗?还是对象?(概念上非常不同) - Marc Gravell
你的 ToString() 是不正确的;应该是 public override ToString() - 强调 override - Marc Gravell
1
是的,我确实指的是对象,而不是变量。我以为对 Java 版本的问题的引用会说明这一点,但我应该使用正确的术语。像往常一样,您在 ToString() 上是正确的,我会修复代码的…… - SimonC
根据2019年12月2日的此评论“...逃逸分析和堆栈分配目前正在运行时团队进行中”. - Branko Dimitrijevic
3个回答

15
如果你是指代码中的对象(new Foo(i);),那么我的理解是:它从未在堆栈上分配,但是它将在第零代中销毁,因此回收非常高效。我不敢说我对CLI的每个黑暗角落都了如指掌,但我没有意识到任何场景会导致C#中管理的引用类型在堆栈上分配(类似于stackalloc这样的东西并不算,并且非常特定)。显然,在C++中你有更多的选择,但那不是一个托管实例。
有趣的是,在MonoTouch/AOT上,它可能会立即被回收,但那不是主要的CLI VM(而且是针对一个非常特定的场景)。
至于变量,它通常会在堆栈上(并在每个循环迭代中重新使用)- 但它可能不会。例如,如果这是一个“迭代器块”,那么所有未删除的局部变量实际上都是编译器生成状态机上的字段。更常见的是,如果变量被“捕获”(进入匿名方法或lambda表达式,两者都形成闭包),则变量会转换为在编译器生成的捕获上下文中的字段,并且每个循环迭代都是独立的(因为foo在循环内部声明)。这意味着它们在堆上是分离的。
至于i(循环变量)-如果它被捕获,那就更有趣了:
  • 在C# 1.2中,捕获不存在,但根据规范,循环变量从技术上讲是每次迭代的
  • 在C# 2.0到4.0中,循环变量是共享的(导致了众所周知的捕获/foreach常见问题)
  • 在C# 5.0及以上版本中,循环变量再次是每次迭代的

只有在变量被捕获时,这才会有所不同,但它改变了变量在捕获上下文中体现方式的语义。


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

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

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

另外,堆栈是一个实现细节 也是一篇不错的阅读材料。


1

虽然 x86 JIT 在“内联”valuetype方面表现良好,但是您的代码片段不会被视为ToString方法,因为它将成为装箱对象上的虚拟调用。 编辑:这可能并非如此,因为您没有覆盖ToString

然而,从我的实验中来看,x64 JIT根本不会执行这个操作。

编辑:

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


1
Foo是一个类,所以我不确定这些内容有多少适用。 - Marc Gravell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接