它真的是JIT优化中的错误还是我错过了什么?

时间:2012-08-29 15:04:28

标签: c# jit

[TestFixture]
public class Tests
{
    private class Relay
    {
        public Action Do { get; set; }
    }

    [Test]
    public void OptimizerStrangeness()
    {
        var relay = new Relay();
        var indicator = 0;
        relay.Do = () => indicator++;
        var weak = new WeakReference(relay);

        GC.Collect();

        var relayNew = weak.Target as Relay;
        if (relayNew == null) Assert.Fail();
        relayNew.Do();
        Assert.AreEqual(1, indicator);
    }
}

此代码仅在Assert.Fail()行的发布模式下失败,尽管事实relay变量仍然在范围内,因此我们仍然对该实例有强引用,因此WeakReference必须尚未死亡。 / p> UPD:澄清一点:我意识到它可以'优化掉'。但取决于此优化indicator变量将具有01值,即我们实际上有明显的行为变化。

UPD2:来自C#语言规范,section 3.9

  

如果对象或其任何部分无法通过任何可能的继续执行来访问,除了运行   析构函数,该对象被认为不再使用,它​​变成了   有资格获得破坏。 C#编译器和垃圾收集器   可以选择分析代码以确定对对象的引用   可能会在将来使用。例如,如果是局部变量   范围是对象的唯一现有引用,但是本地引用   变量永远不会在任何可能的延续中被引用   从程序中的当前执行点执行,   垃圾收集器可以(但不是必须)将对象视为否   使用时间更长。

从技术上讲,这个对象可以并且将通过继续执行来访问,因此不能被视为“不再使用”(实际上C#规范没有提及弱引用,因为它是CLR的一部分而不是编译器 - 编译器输出很好)。将尝试搜索有关CLR / JIT的内存管理信息。

UPD3:这是关于CLR内存管理的some info - “释放内存”部分:

  

......每个应用程序都有一组根。每个根指的是一个   托管堆上的对象或设置为null。应用程序的根源   包括全局和静态对象指针,局部变量和   线程堆栈上的引用对象参数和CPU寄存器。   垃圾收集器可以访问活动根列表   实时(JIT)编译器和运行时维护。使用此列表,   它检查应用程序的根,并在此过程中创建一个图形   包含从根可以访问的所有对象。

有问题的变量肯定是局部变量,因此它是可达的。如上所述,这个提及非常快/含糊,所以我很高兴看到更具体的信息。

UPD4:来自.NET Framework的来源:

    // This method DOES NOT DO ANYTHING in and of itself.  It's used to
    // prevent a finalizable object from losing any outstanding references 
    // a touch too early.  The JIT is very aggressive about keeping an 
    // object's lifetime to as small a window as possible, to the point
    // where a 'this' pointer isn't considered live in an instance method 
    // unless you read a value from the instance.  So for finalizable
    // objects that store a handle or pointer and provide a finalizer that
    // cleans them up, this can cause subtle ----s with the finalizer
    // thread.  This isn't just about handles - it can happen with just 
    // about any finalizable resource.
    // 
    // Users should insert a call to this method near the end of a 
    // method where they must keep an object alive for the duration of that
    // method, up until this method is called.  Here is an example: 
    //
    // "...all you really need is one object with a Finalize method, and a
    // second object with a Close/Dispose/Done method.  Such as the following
    // contrived example: 
    //
    // class Foo { 
    //    Stream stream = ...; 
    //    protected void Finalize() { stream.Close(); }
    //    void Problem() { stream.MethodThatSpansGCs(); } 
    //    static void Main() { new Foo().Problem(); }
    // }
    //
    // 
    // In this code, Foo will be finalized in the middle of
    // stream.MethodThatSpansGCs, thus closing a stream still in use." 
    // 
    // If we insert a call to GC.KeepAlive(this) at the end of Problem(), then
    // Foo doesn't get finalized and the stream says open. 
    [System.Security.SecuritySafeCritical]  // auto-generated
    [ResourceExposure(ResourceScope.None)]
    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] 
    public static extern void KeepAlive(Object obj);

如果您有兴趣,请参阅here了解我的调查详情。

1 个答案:

答案 0 :(得分:8)

即使变量在范围内,如果在将来的代码路径中不再访问它,运行时也可以自由地收集它。这就是为什么在调试具有JIT优化的程序集时,可以为您提供有关变量值被优化的消息,即使它当前在范围内。

3.9 Automatic Memory Management上的第2项。

具体而言,

  

例如,如果作用域中的局部变量是对象的唯一现有引用,但该过程中当前执行点的任何可能的继续执行中都不会引用该局部变量,则垃圾收集器可能(但不要求)将对象视为不再使用。

这是至关重要的一点;该对象被认为是不可访问的,因为对该对象的所有强引用(只有一个)是不可达的。请记住,C#规范将包含有关语言的信息以及有关如何执行已编译代码的信息。还要记住,范围不能定义可达性。正如规范所述,如果编译器和运行时可以确定变量在任何未来的代码路径中都不存在(意味着它们根本不会被引用或仅在被确定为无法访问的路径中引用,例如if(false)) ,那么该变量被认为是无法访问的,并且不算作强引用。

虽然规范的特定部分没有明确说明WeakReference,但它并不需要。就编译器而言,只有一个局部变量指向该值。

WeakReference只是另一个以对象作为参数的类;从编译器的角度来看;它没有理由相信(或以某种方式做出假设)关于该类是否持有它所通过的引用。考虑一下我是否有一个这样的类代替了:

public class MyClass
{
    public MyClass(object foo)
    {
        Console.WriteLine(foo);
    }
}

在我的代码中,我这样做了:

var relay = new Relay();
...
var myClass = new MyClass(relay);

我没有对我分配给relay的值引入任何新的强引用,因为MyClass不会保留该引用。事实上WeakReference是一个“特殊”类,旨在为您提供对计数为强引用的对象的引用,就编译器而言是无关紧要的

可达性不是由范围定义的;它是由所讨论的变量(非值)是否存在于任何可能的未来代码路径中来定义的。由于relay后来在函数中不以任何形式出现,因此变量(以及它对对象的引用)被认为是不可达的并且有资格收集。这就是DisableOptimizations标志在程序集级别存在的原因,因此运行时知道(除其他事项外)要等到变量超出范围之后才有资格进行收集,以便调试器可以访问它。 / p>