Hello World
Spiga

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

2009-11-29 21:11 by 老赵, 5250 visits

之前谈到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实现方式有关了具体参见文章下方讨论。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

36 条回复

  1. oec2003
    *.*.*.*
    链接

    oec2003 2009-11-29 21:20:00

    沙发 呵呵

  2. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-11-29 22:25:00

    留个记号明天细看

  3. Fred_Xu
    *.*.*.*
    链接

    Fred_Xu 2009-11-29 22:52:00

    老赵周末也发博文,精神让人佩服啊!留个记号,明天上班认真看!

  4. Will Meng
    *.*.*.*
    链接

    Will Meng 2009-11-30 08:35:00

    系统崩溃了,等装完了,好好研究一下子

  5. 狂人
    *.*.*.*
    链接

    狂人 2009-11-30 10:21:00

    至于为什么是这个结果,那便和CLR实现方式有关了。

    结果在哪……在哪……

  6. 老赵
    admin
    链接

    老赵 2009-11-30 10:27:00

    @狂人
    自己试验一下就看到结果了。

  7. 狂人
    *.*.*.*
    链接

    狂人 2009-11-30 10:33:00

    @卖关赵

    我试了,貌似这种状况下fix不fix都不会变

  8. 老赵
    admin
    链接

    老赵 2009-11-30 10:53:00

    @狂人
    是啊,我一开始猜还以为是fixed不变,另一个变。所以我说,这就要看CLR咋搞了……
    例如,JIT结果如何,Workstation GC还是Server GC……当然这还是我猜的……

  9. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-30 11:15:00

    我不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道不想知道

  10. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 11:50:00

    我猜可能和编译后的东东有关系。。。呃。。。纯猜测,一会儿试一下。。。。

  11. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 11:53:00

    然则,我的猜测还真的对了。。。。。

    好不容易找到这么个崇拜下自己的机会,内牛满面啊。。。。。

  12. 老赵
    admin
    链接

    老赵 2009-11-30 12:15:00

    @Ivony...
    啥叫编译后的东东?

  13. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 12:19:00

    Jeffrey Zhao:
    @Ivony...
    啥叫编译后的东东?




    就是经过编译器做了丑恶的事情之后的东东,可以用Reflector或者IL DASM来看。。。。。

  14. 老赵
    admin
    链接

    老赵 2009-11-30 12:24:00

    @Ivony...
    果然啊,我咋没想到,弱了……不过为啥会编译成这样涅?

    private static unsafe void Mixed()
    {
        SomeClass c1 = new SomeClass();
        SomeClass c2 = new SomeClass();
        fixed (int* ptr1 = &c1.Field)
        {
            PrintAddress("Before GC", (int) ptr1);
            ptr2 = &c2.Field;
        }
        PrintAddress("Before GC (fixed)", (int) ((IntPtr) ptr2));
        GC.Collect(2);
        fixed (int* ptr2 = null)
        {
            ref int pinned ptr2;
            fixed (int* ptr1 = &c1.Field)
            {
                PrintAddress("After GC", (int) ptr1);
                ptr2 = &c2.Field;
            }
            PrintAddress("After GC (fixed)", (int) ((IntPtr) ptr2));
            ptr2 = null;
        }
    }

  15. 老赵
    admin
    链接

    老赵 2009-11-30 12:26:00

    这代码怎么一下子变那么丑恶,又要花时间搞CSS了,草。

  16. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 12:27:00

    Jeffrey Zhao:这代码怎么一下子变那么丑恶,又要花时间搞CSS了,草。




    似乎是博客园的问题,,脑袋那边的回复显示也有问题。。。

  17. 老赵
    admin
    链接

    老赵 2009-11-30 12:34:00

    @Ivony...
    好吧,我再去diff一下了,现在搞博客园样式日趋熟练……

  18. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-30 12:39:00

    @Jeffrey Zhao
    这段代码也没有解释为什么不管有没有fixed,GC前后的地址都没有改变啊。

    另外,在Release模式下,var a = new int[100];这行代码会被优化掉,所以这行代码起不到制造垃圾的作用。

  19. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 12:41:00

    Jeffrey Zhao:
    @Ivony...
    果然啊,我咋没想到,弱了……不过为啥会编译成这样涅?


    先不说别的,老赵贴出来的代码是编译不了的。贴出来的代码看起来很怪,造成这种“视觉效果”很怪的原因是Reflector在分析fixed的时候并不完善,连变量声明前就使用的代码都弄出来了。
    至于fixed的语义……等有空再说,今天抓虫抓得我抓狂了 ToT

  20. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-30 12:45:00

    @RednaxelaFX
    我也认为是Reflector在反编译的时候出问题了。

    大家还是把Reflector的Optimization选项设为none再看看吧,虽然出来的代码仍然不能通过编译,但是至少逻辑和语句的顺序上都没有问题,比老赵贴出来的那段直观多了。

  21. 老赵
    admin
    链接

    老赵 2009-11-30 12:48:00

    幸存者:
    @Jeffrey Zhao
    这段代码也没有解释为什么不管有没有fixed,GC前后的地址都没有改变啊。

    另外,在Release模式下,var a = new int[100];这行代码会被优化掉,所以这行代码起不到制造垃圾的作用。


    本来就没法解释,只能验证啊。
    我比较奇怪的是,既然var a = new int[100]被优化掉了,那为啥第一个实验是有效果的呢?

  22. 老赵
    admin
    链接

    老赵 2009-11-30 12:50:00

    幸存者:
    @RednaxelaFX
    我也认为是Reflector在反编译的时候出问题了。

    大家还是把Reflector的Optimization选项设为none再看看吧,虽然出来的代码仍然不能通过编译,但是至少逻辑和语句的顺序上都没有问题,比老赵贴出来的那段直观多了。


    不管fixed与否,全pin住了?
    private static unsafe void Mixed()
    {
        SomeClass c1;
        SomeClass c2;
        ref int pinned ptr1;
        ref int pinned ptr2;
        ref int pinned ptr1;
        ref int pinned ptr2;
        c1 = new SomeClass();
        c2 = new SomeClass();
        ptr1 = &c1.Field;
        PrintAddress("Before GC", (int) ((IntPtr) ptr1));
        ptr1 = (IntPtr) 0;
        ptr2 = &c2.Field;
        PrintAddress("Before GC (fixed)", (int) ((IntPtr) ptr2));
        GC.Collect(2);
        ptr2 = (IntPtr) 0;
        ptr1 = &c1.Field;
        PrintAddress("After GC", (int) ((IntPtr) ptr1));
        ptr1 = (IntPtr) 0;
        ptr2 = &c2.Field;
        PrintAddress("After GC (fixed)", (int) ((IntPtr) ptr2));
        ptr2 = (IntPtr) 0;
        return;
    }

  23. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 12:55:00

    So,我已经习惯了IL DASM,不过Reflector的好处是导航太赞了,点一个方法就过去了,还有Analyzer也很赞。。。。

    看实现的话,IL DASM还是比较靠谱。。。。


    Reflector出来的代码是不能确保能编译回去的,不过这里这段代码已经足够说明问题了。

  24. 老赵
    admin
    链接

    老赵 2009-11-30 12:59:00

    @Ivony...
    用Reflector看IL不也一样嘛。
    可惜看IL多累啊,我是能看C#就不看IL的。
    话说我《IL无用论》还有最后一篇一直没写,要找机会写写。

  25. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 13:00:00

    呃,,,,Reflector真不靠谱啊,int32&竟然被解释成了ref int。。。。

    还是看IL吧,最重要的就是开头的变量定义:

    int32& pinned ptr1,
    int32& pinned ptr2,
    int32& pinned V_4,
    int32& pinned V_5

  26. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 13:09:00

    简单点说结论就是fixed总是整个函数内有效的。
    复杂点说,C#语法中局部变量在定义且“明确赋值”后才可以被使用,而IL中则所有用到的局部变量都是在函数的一开始就定义好的。那么fixed在IL是作为局部变量的一个特性存在的(pinned)而不是像try、finally那样有区域性,所以就造成了这种结果,fixed总是整个函数内有效的。

    当然,如果C#语法改为fixed int* ptr = &(c1.Field)就会比较直观。。。。

  27. 老赵
    admin
    链接

    老赵 2009-11-30 13:11:00

    @Ivony...
    看来是fixed的场景太少,测试不够,hoho。

  28. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 13:27:00

    Ivony...:
    简单点说结论就是fixed总是整个函数内有效的。
    复杂点说,C#语法中局部变量在定义且“明确赋值”后才可以被使用,而IL中则所有用到的局部变量都是在函数的一开始就定义好的。那么fixed在IL是作为局部变量的一个特性存在的(pinned)而不是像try、finally那样有区域性,所以就造成了这种结果,fixed总是整个函数内有效的。

    当然,如果C#语法改为fixed int* ptr = &(c1.Field)就会比较直观。。。。


    TvT 被催快点抓虫了……
    长话短说:虽然fixed编译出来的局部变量的pinned性质是整个方法体内有效的,但fixed块仍然有局部性:在开头处对指针赋值,在结尾处将同一指针设为null。这样就保证在fixed块内部有pinned语义,而离开fixed块之后撤销pinned语义。

  29. 云中深海
    *.*.*.*
    链接

    云中深海 2009-11-30 13:36:00

    你的文章不错,有空我就来捧场!希望以后有机会多多交流!!!

  30. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 13:41:00

    RednaxelaFX:

    Ivony...:
    简单点说结论就是fixed总是整个函数内有效的。
    复杂点说,C#语法中局部变量在定义且“明确赋值”后才可以被使用,而IL中则所有用到的局部变量都是在函数的一开始就定义好的。那么fixed在IL是作为局部变量的一个特性存在的(pinned)而不是像try、finally那样有区域性,所以就造成了这种结果,fixed总是整个函数内有效的。

    当然,如果C#语法改为fixed int* ptr = &(c1.Field)就会比较直观。。。。


    TvT 被催快点抓虫了……
    长话短说:虽然fixed编译出来的局部变量的pinned性质是整个方法体内有效的,但fixed块仍然有局部性:在开头处对指针赋值,在结尾处将同一指针设为null。这样就保证在fixed块内部有pinned语义,而离开fixed块之后撤销pinned语义。




    呃。。。。。。在我的幻想中,RednaxelaFX应该是直接一拍桌子:“还改什么改,给我重做”这样的大佬才对啊。。。。。


    的确如此,这么说来,也就不是这个问题了。。。。等系统装好了我做更详细的实验

  31. 老赵
    admin
    链接

    老赵 2009-11-30 13:47:00

    @Ivony...
    请问RFX大牛,你现在做的事情是否让你觉得很不甘……

  32. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-30 17:56:00

    Ivony...:
    简单点说结论就是fixed总是整个函数内有效的。
    复杂点说,C#语法中局部变量在定义且“明确赋值”后才可以被使用,而IL中则所有用到的局部变量都是在函数的一开始就定义好的。那么fixed在IL是作为局部变量的一个特性存在的(pinned)而不是像try、finally那样有区域性,所以就造成了这种结果,fixed总是整个函数内有效的。

    当然,如果C#语法改为fixed int* ptr = &(c1.Field)就会比较直观。。。。


    我觉得这仍然没有解释文中的问题。
    虽然fixed指针在整个函数作用域内有效,但是fixed指针指向的地址在局部是变化的。
    注意fixed块结束处C#编译器会加上ptr1 = (IntPtr) 0; 也就是说被pin住的内存地址已经变成了0,也就是null。而GC.Collect()执行的时候c1所在的内存块是没有被pin住的。

  33. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-30 19:12:00

    @幸存者


    是的,RFX已经帮我指出来了。。。。是我不仔细,我一会儿详细的测试下。。。。

  34. 老赵
    admin
    链接

    老赵 2009-11-30 19:16:00

    @Ivony...
    这东西……估计要看汇编,甚至要边看SSCLI边猜CLR的实现才能准确地说明问题吧……

  35. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 20:22:00

    Jeffrey Zhao:
    @Ivony...
    这东西……估计要看汇编,甚至要边看SSCLI边猜CLR的实现才能准确地说明问题吧……


    GC是CLR里最智能最动态的部分了(有没有之一呢……大概没有?),这玩儿光靠猜太困难了。我是一直都没准确弄清CLR的GC到底有哪些东西。它是一个由policy管理、实现了许多GC算法的系统,根据收集到的数据的反馈来调整GC堆的大小、限制和算法。
    靠GC.Collect()去试探肯定得不到什么精确反映实际生产环境的结果,因为干扰了反馈……靠它只能用于了解CLR选定的某个GC算法作用于某一代的堆的效果。在SOS里可以观察GC堆的分配情况,包括GC后堆的碎片化状况,例如用!dumpheap -type Free命令。

  36. 露磬弢
    112.64.0.*
    链接

    露磬弢 2010-04-14 01:00:01

    其实只是C#在编译时做了优化而已,如果博主可能需要编译器生成能够回收效果的代码,可以使用如下代码

    private static unsafe void Mixed()
    {
        int[] a = new int[100];
        SomeClass c1 = new SomeClass();
        SomeClass c2 = new SomeClass();
    
        Console.WriteLine(' ');
        fixed (int* ptr1 = &c1.Field)
        {
            PrintAddress("Before GC", (int)ptr1);
        }
    
        Console.WriteLine(' ');
        fixed (int* ptr2 = &c2.Field)
        {
            PrintAddress("Before GC (fixed)", (int)ptr2);
        }
    
        GC.Collect(2);
        Console.Write(' ');
        fixed (int* ptr3 = &c1.Field)
        {
           PrintAddress("After GC", (int)ptr3);
        }
    
        Console.Write(' ');
        fixed (int* ptr4 = &c2.Field)
        {
            PrintAddress("After GC (fixed)", (int)ptr4);
        }
    }
    

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我