代码改变世界

验证fixed关键字效果的小实验

2009-11-29 21:11  Jeffrey Zhao  阅读(5777)  评论(35编辑  收藏  举报

之前谈到String连接操作的性能,其中会涉及到unsafe操作,而unsafe操作必然会涉及到指针,于是fixed关键字也应运而生。fixed关键字是用来pin住一个引用地址的,因为我们知道CLR的垃圾收集器会改变某些对象的地址,因此在改变地址之后指向那些对象的引用就要随之改变。这种改变是对于程序员来说是无意识的,因此在指针操作中是不允许的。否则,我们之前已经保留下的地址,在GC后就无法找到我们所需要的对象。现在就来我们就来做一个小实验,验证fixed关键字的效果。

当然,这个实验很简单,简单地可能会让您笑话。首先我们来准备一个SomeClass类:

public class SomeClass
{
    public int Field;
}

然后准备一段代码:

private static unsafe void GCOutOfFixedBlock()
{
    var a = new int[100];
    var c = new SomeClass();

    fixed (int* ptr = &c.Field)
    {
        PrintAddress("Before GC", (int)ptr);
    }

    GC.Collect(2);

    fixed (int* ptr = &c.Field)
    {
        PrintAddress("After GC", (int)ptr);
    }
}

private static void PrintAddress(string name, int address)
{
    Console.Write(name + ": 0x");
    Console.WriteLine(address.ToString("X"));
}

在GCOutOfFixedBlock方法中,我们首先分配一个长度为100的int数组,然后新建一个SomeClass对象。新建数组的目的在于制造“垃圾”,目的是在调用GC.Collect方法时改变SomeClass对象在堆中的位置。由于垃圾回收发生在fixed代码块之外,这样我们前后两次打印出的值便是不同的:

Before GC: 0x1A058C0
After GC: 0x1975DF4

值得注意的是,这段代码必须在Release模式下进行编译,让CLR执行代码时进行优化,这样CLR便会在垃圾回收时发现a数组已经是垃圾了(因为后面的代码不会用它),于是会将其回收——否则便无法看出地址改变的效果来。那么,我们重写一段代码:

private static unsafe void GCInsideFixedBlock()
{
    var a = new int[100];
    var c = new SomeClass();

    fixed (int* ptr = &c.Field)
    {
        PrintAddress("Before GC", (int)ptr);
        GC.Collect(2);
    }

    fixed (int* ptr = &c.Field)
    {
        PrintAddress("After GC", (int)ptr);
    }
}

结果如下:

Before GC: 0x1B558C0
After GC: 0x1B558C0

由于GC发生在fixed代码块内部,因此c对象被pin在堆上了,于是GC前后c对象的地址没变,这就是fixed的作用。那么,下面这段代码运行结果是什么呢?

private static unsafe void Mixed()
{
    var a = new int[100];
    var c1 = new SomeClass();
    var c2 = new SomeClass();

    fixed (int* ptr1 = &c1.Field)
    {
        PrintAddress("Before GC", (int)ptr1);
    }

    fixed (int* ptr2 = &c2.Field)
    {
        PrintAddress("Before GC (fixed)", (int)ptr2);
        GC.Collect(2);
    }

    fixed (int* ptr1 = &c1.Field)
    {
        PrintAddress("After GC", (int)ptr1);
    }

    fixed (int* ptr2 = &c2.Field)
    {
        PrintAddress("After GC (fixed)", (int)ptr2);
    }
}

至于为什么是这个结果,那便和CLR实现方式有关了具体参见文章下方讨论。