代码改变世界

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

  Jeffrey Zhao  阅读(5779)  评论(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实现方式有关了具体参见文章下方讨论。

编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· SQL Server 2025 AI相关能力初探
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示