一个符合规范的C#编译器能否优化掉一个本地变量(但未使用),如果它是对象的唯一强引用?

11
以下是相关资源:

请参阅这些相关资源:


换句话说:

在局部变量引用的对象超出其作用域之前(例如,因为该变量被赋值但未再次使用),该对象是否可以被回收,或者该对象是否保证在变量超出作用域之前不会被垃圾回收?

让我解释一下:


void Case_1()
{
    var weakRef = new WeakReference(new object());

    GC.Collect();  // <-- doesn't have to be an explicit call; just assume that
                   //     garbage collection would occur at this point.

    if (weakRef.IsAlive) ...
}

在这个代码示例中,我显然必须考虑到新创建的object可能会被垃圾回收器回收;因此需要使用if语句。
(请注意,我仅使用weakRef来检查新创建的object是否仍然存在。)
void Case_2()
{
    var unusedLocalVar = new object();
    var weakRef = new WeakReference(unusedLocalVar);

    GC.Collect();  // <-- doesn't have to be an explicit call; just assume that
                   //     garbage collection would occur at this point.

    Debug.Assert(weakRef.IsAlive);
}

这个代码示例与之前的不同之处在于,新建的object被一个局部变量(unusedLocalVar)强引用。然而,在创建了弱引用(weakRef)后,这个变量再也没有被使用过。
问题:如果符合规范的C#编译器看到unusedLocalVar只在一个地方被使用,即作为WeakReference构造函数的参数,是否允许将Case_2的前两行优化成Case_1的形式?即Case_2中的断言是否可能失败?
3个回答

12
无论 C# 编译器做了什么,JITter/GC 都可以在方法体内不再存在本地引用时清理它们。请参阅 GC.KeepAlive 文档。
此外,这个 PowerPoint 演示文稿,特别是从第 30 张幻灯片开始,有助于解释 JIT/GC 可以做的事情。

4
请注意,在调试版本中,为了让调试器查看变量,该变量会被明确地保持在作用域的末尾--只有在发布版本中才会出现这种行为。 - Andy Mortimer
@Andy - 有趣的观点。虽然这并不重要,但我猜测这种行为是由JITter控制的? - Damien_The_Unbeliever
4
为了完整起见,这就是为什么在方法的结尾处设置unusedLocalVar=null通常会导致性能下降。 - H H
@Damien_The_Unbeliever: 感谢您提供了 GC.KeepAlive 的提示,我认为 MSDN 文档对我的问题给出了 95% 的答案。-- @Henk Holtermann: 这似乎比 GC.KeepAlive 不安全;聪明的编译器不能将这样的 null 赋值识别为多余的并将其优化掉吗? - stakx - no longer contributing
是的,JIT优化器将删除空赋值。垃圾收集器从JIT编译器获取本地变量的生命周期线索。GC.KeepAlive是这种提示的手动版本,它实际上不会生成任何代码。 - Hans Passant

3
虽然我的问题已经得到了回答,但我想发表一篇相关的信息,我刚在MSDN博客文章"WP7: When does GC Consider a Local Variable as Garbage" by abhinaba中找到了这篇文章:

“例如,如果在作用域内的局部变量是对象的唯一现有引用,但是该局部变量在从过程的当前执行点开始的任何可能的执行延续中都没有被引用,实现可能(但不必)将该对象视为不再使用。”

这就说了一切。上述文章还说,.NET框架(至少在发布模式下)会执行预测分析并释放这些对象,而.NET Compact Framework则不会(出于性能原因)。


-1
一个符合规范的C#编译器是否允许将Case_2的前两行优化为Case_1的形式,如果它看到unusedLocalVar仅在一个地方被使用,即作为WeakReference构造函数的参数?
这两个定义是等效的,因此从一个转换到另一个不是“优化”,因为两者都没有更高的效率。
也就是说,Case_2中的断言有可能失败吗?
是的。生产编译器不太可能保留不必要的引用,因此它将被删除,GC将不会将其视为全局根,并收集该对象。
请注意,垃圾收集器不会按变量和作用域来查看您的程序。这些高级概念在您的代码到达垃圾收集器时早已被编译掉了。GC只看到寄存器、线程堆栈和全局变量。

这两个定义是等效的,因此从一个转换到另一个并不是一种“优化”,因为它们都没有更高的效率。做还是不做赋值是内存管理工作的基础,这直接关系到性能。这就是这个问题的关键,编译器是否足够聪明,在不需要时不进行赋值。 - Seabizkit
“做出分配和不做出分配是内存管理的基础”并不是正确的。您正在混淆源代码的特征与GC实际可见的内容。从GC的角度来看,这里没有局部变量,也没有任何情况下的赋值操作。因此,这不是一种“优化”。C#编译器唯一的限制是它不能合并写入,但在这里根本没有向堆写入内存...” - J D

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