如何在非托管内存中实例化一个C#类?(可能吗?)

13

更新:现在有一个被接受的答案可以“工作”。你永远不应该使用它。永远。


首先,我要说明我的问题,我是一名游戏开发人员。想要这样做有一个合理的 - 尽管非常不寻常 - 与性能相关的原因。


假设我有一个像这样的 C# 类:

class Foo
{
    public int a, b, c;
    public void MyMethod(int d) { a = d; b = d; c = a + b; }
}

没什么花里胡哨的。注意它是一个只包含值类型的引用类型。

在托管代码中,我想要像这样的东西:

Foo foo;
foo = Voodoo.NewInUnmanagedMemory<Foo>(); // <- ???
foo.MyMethod(1);

如果可以的话,函数NewInUnmanagedMemory会是什么样子?如果在C#中无法实现,那么在IL中可以吗?(或者可能是C++/CLI?)基本上:是否有一种方法 - 无论多么粗糙 - 可以将一些完全任意的指针转换为对象引用。并且 - 短视CLR爆炸 - 不管后果如何。
(另一种表达我的问题的方式是:“我想为C#实现自定义分配器”)
这引出了一个后续问题:当面对指向托管内存之外的引用时,垃圾收集器会执行什么操作(如果需要,可以进行特定于实现的实现)?
与此相关的是,如果Foo具有引用作为成员字段会发生什么情况?如果它指向托管内存呢?如果它只指向在未管理内存中分配的其他对象呢?
最后,如果这是不可能的:为什么?

更新:以下是目前“遗漏的部分”:

#1:如何将IntPtr转换为对象引用?虽然可能通过不可验证的IL(请参见注释),但到目前为止我还没有成功。该框架似乎非常小心地防止这种情况发生。

(在运行时获取非平凡托管类型的大小和布局信息也很好,但是,框架试图使这不可能。)

#2:假设问题一得以解决——当GC遇到指向GC堆之外的对象引用时会发生什么?它会崩溃吗?Anton Tykhyy在他的答案中猜测它会崩溃。考虑到框架对于#1非常小心谨慎,这似乎是有可能的。如果有证据证实这一点,那将是很好的。

(或者,对象引用可以指向GC堆内的固定内存。这会有所不同吗?)

基于此,我倾向于认为这个黑客想法是不可能的,或者至少不值得花费精力。但我很想得到一个涉及#1或#2或两者的技术细节的答案。


4
你能解释一下你提到的“与绩效相关的原因”是什么吗? - svick
2
@AndrewRussell “Rewind” 是指如何实现?非托管内存在哪里使用? - Anton Tykhyy
2
我明白了。是的,按照您所写的,您需要一个新的GC来实现这一点。然而,可能有其他方法可以达到相同的效果。您考虑了哪些字节级操作?您的游戏对象是否不可变? - Anton Tykhyy
1
@AndrewRussell,如果你想保留历史记录,使用不可变对象并不是一种hack,而是正确的解决方案(或者至少是其中之一)。 - svick
@AndrewRussell,三年后,你有写出什么东西吗?我在Untiy3d上的第一次尝试非常有前途,因此我正在研究如何在性能关键情况下避免GC,同时仍需要进行内存分配。 - sebas
显示剩余11条评论
8个回答

8

我一直在尝试在非托管内存中创建类。这是可能的,但有一个问题我目前还无法解决 - 请参见底部编辑内容,因此您的自定义类中只能有结构体字段而不能有引用类型字段。

这很糟糕:
using System;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

public class Voodoo<T> where T : class
{
    static readonly IntPtr tptr;
    static readonly int tsize;
    static readonly byte[] zero;

    public static T NewInUnmanagedMemory()
    {
        IntPtr handle = Marshal.AllocHGlobal(tsize);
        Marshal.Copy(zero, 0, handle, tsize);
        IntPtr ptr = handle+4;
        Marshal.WriteIntPtr(ptr, tptr);
        return GetO(ptr);
    }

    public static void FreeUnmanagedInstance(T obj)
    {
        IntPtr ptr = GetPtr(obj);
        IntPtr handle = ptr-4;
        Marshal.FreeHGlobal(handle);
    }

    delegate T GetO_d(IntPtr ptr);
    static readonly GetO_d GetO;
    delegate IntPtr GetPtr_d(T obj);
    static readonly GetPtr_d GetPtr;
    static Voodoo()
    {
        Type t = typeof(T);
        tptr = t.TypeHandle.Value;
        tsize = Marshal.ReadInt32(tptr, 4);
        zero = new byte[tsize];

        DynamicMethod m = new DynamicMethod("GetO", typeof(T), new[]{typeof(IntPtr)}, typeof(Voodoo<T>), true);
        var il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ret);
        GetO = m.CreateDelegate(typeof(GetO_d)) as GetO_d;

        m = new DynamicMethod("GetPtr", typeof(IntPtr), new[]{typeof(T)}, typeof(Voodoo<T>), true);
        il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ret);
        GetPtr = m.CreateDelegate(typeof(GetPtr_d)) as GetPtr_d;
    }
}

如果你在意内存泄漏,当你使用完你的类之后,应该总是调用FreeUnmanagedInstance。 如果你想要更复杂的解决方案,你可以尝试这个:

using System;
using System.Reflection.Emit;
using System.Runtime.InteropServices;


public class ObjectHandle<T> : IDisposable where T : class
{
    bool freed;
    readonly IntPtr handle;
    readonly T value;
    readonly IntPtr tptr;

    public ObjectHandle() : this(typeof(T))
    {

    }

    public ObjectHandle(Type t)
    {
        tptr = t.TypeHandle.Value;
        int size = Marshal.ReadInt32(tptr, 4);//base instance size
        handle = Marshal.AllocHGlobal(size);
        byte[] zero = new byte[size];
        Marshal.Copy(zero, 0, handle, size);//zero memory
        IntPtr ptr = handle+4;
        Marshal.WriteIntPtr(ptr, tptr);//write type ptr
        value = GetO(ptr);//convert to reference
    }

    public T Value{
        get{
            return value;
        }
    }

    public bool Valid{
        get{
            return Marshal.ReadIntPtr(handle, 4) == tptr;
        }
    }

    public void Dispose()
    {
        if(!freed)
        {
            Marshal.FreeHGlobal(handle);
            freed = true;
            GC.SuppressFinalize(this);
        }
    }

    ~ObjectHandle()
    {
        Dispose();
    }

    delegate T GetO_d(IntPtr ptr);
    static readonly GetO_d GetO;
    static ObjectHandle()
    {
        DynamicMethod m = new DynamicMethod("GetO", typeof(T), new[]{typeof(IntPtr)}, typeof(ObjectHandle<T>), true);
        var il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ret);
        GetO = m.CreateDelegate(typeof(GetO_d)) as GetO_d;
    }
}

/*Usage*/
using(var handle = new ObjectHandle<MyClass>())
{
    //do some work
}

我希望这能帮助你在IT技术方面更进一步。 < p >编辑:找到了解决引用类型字段的方法:< /p>
class MyClass
{
    private IntPtr a_ptr;
    public object a{
        get{
            return Voodoo<object>.GetO(a_ptr);
        }
        set{
            a_ptr = Voodoo<object>.GetPtr(value);
        }
    }
    public int b;
    public int c;
}

编辑:更好的解决方案是使用ObjectContainer<object>而不是object等。

public struct ObjectContainer<T> where T : class
{
    private readonly T val;

    public ObjectContainer(T obj)
    {
        val = obj;
    }

    public T Value{
        get{
            return val;
        }
    }

    public static implicit operator T(ObjectContainer<T> @ref)
    {
        return @ref.val;
    }

    public static implicit operator ObjectContainer<T>(T obj)
    {
        return new ObjectContainer<T>(obj);
    }

    public override string ToString()
    {
        return val.ToString();
    }

    public override int GetHashCode()
    {
        return val.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        return val.Equals(obj);
    }
}

1
邪恶?这几乎是撒旦的边缘。回答被接受了 - 恭喜 :) 任何人都不应该使用这段代码! - Andrew Russell
所以...我正在编写一个使用非托管内存的内存管理器,只是为了好玩,而我一直在努力寻找一种将类映射到预分配内存的方法。嗯,你的ObjectHandle类完美地解决了这个问题!现在我必须理解你在这里做了什么 :D - sebas
1
@sebas tptr 是指向内部运行时类型信息(称为方法表)的指针,其中包含有关类型的一些信息,包括基本实例大小,该大小位于偏移量4处。 - IS4
1
@sebas 可能是因为它是CLR内部使用的信息,因此可能会受到更改,并且与平台和实现有关。 - IS4
1
@sebas 前四个字节是同步块,其中包含一些与锁定等相关的信息。接下来的四个字节是类型指针,但由于优化,实际对象指针指向后者;因此我首先将句柄加上4个字节,然后从中获取对象引用。 - IS4
显示剩余8条评论

7
我希望为C#实现一个自定义分配器。
GC是CLR的核心。只有微软(或Mono团队,如果使用Mono)可以替换它,并付出巨大的开发成本。由于GC是CLR的核心,乱搞GC或托管堆将很快导致CLR崩溃——如果你非常幸运。
当面对指向托管内存之外的引用时,垃圾回收器会以特定于实现的方式崩溃 ;)

+1 如果您实际回答了我的问题。虽然我只是想得到更详细的确认(特别是由于您在其中放置了“;)”),但这是否归因于直觉而导致它崩溃?还是您已经尝试过它/经历过它/看到类似的情况/有参考资料? - Andrew Russell
1
这是一种直觉,虽然我没有尝试过,但这是一种有根据的直觉。看看旧的SSCLI代码,每当一个方法使用托管指针时,有多少先决条件、契约等等。如果你只是随意地插入一个随机指针,它几乎肯定会破坏某些东西——而且它不一定是显而易见的,它可能只在负载下,在多处理器系统中等等表现出来。 - Anton Tykhyy
我想我会暂时搁置探究SSCLI代码的计划 - 或许作为最后的手段。不过你提醒了我一个有趣的事情:字符串驻留(String interning)。这已经超出了我的CLR知识范围 - 但是驻留的字符串是否存储在GC内存中?如果不是,那么或许可以为指向GC堆之外的引用设置先例?(对于我可怕的黑客来说是一线希望?;) - Andrew Russell
据我所知,内部化字符串仍然以正常方式分配在托管堆上。SSCLI代码似乎证实了这个想法。顺便说一句,SSCLI代码在许多关键位置上与实际的Microsoft CLR代码不太接近,因此它只能用于指针和大致了解可能涉及的内容。 - Anton Tykhyy

5

纯C#方法

所以,有几个选择。最简单的方法是在不安全的上下文中使用new/delete来处理结构体。其次是使用内置的Marshalling服务来处理非托管内存(此代码可见于下面)。但是,这两种方法都处理结构体(尽管我认为后一种方法非常接近您想要的方法)。我的代码有一个限制,即必须始终坚持使用结构体,并使用IntPtrs作为引用(使用ChunkAllocator.ConvertPointerToStructure获取数据和ChunkAllocator.StoreStructure存储更改后的数据)。显然,这很麻烦,因此如果您使用我的方法,则最好非常需要性能。但是,如果您只处理值类型,这种方法就足够了。

插曲:CLR中的类

类在分配内存时有8个字节的“前缀”。其中四个字节用于多线程同步索引,另外四个字节用于标识它们的类型(基本上是虚拟方法表和运行时反射)。这使得处理非托管内存变得困难,因为这些是CLR特定的,并且由于同步索引可以在运行时更改。有关运行时对象创建的详细信息,请参见此处,有关引用类型的内存布局概述,请参见此处。还可以查看CLR via C#以获得更深入的解释。

注意事项

通常情况下,事情很少像是简单的“是”或“否”那样简单。引用类型的真正复杂性与垃圾收集器在垃圾收集期间如何压缩已分配内存有关。如果您可以以某种方式确保垃圾收集不会发生或不会影响相关数据(请参见fixed keyword),则可以将任意指针转换为对象引用(只需将指针偏移8个字节,然后将该数据解释为具有相同字段和内存布局的结构体;可能要使用StructLayoutAttribute以确保)。我建议尝试非虚拟方法,看看它们是否有效;它们应该有效(特别是如果将它们放在结构体上),但由于必须丢弃虚拟方法表,因此虚拟方法无法使用。

一个人不可能简单地走进魔多

简单来说,这意味着托管引用类型(类)不能在非托管内存中分配。你可以在C++中使用托管引用类型,但那些会受到垃圾回收的影响...而且过程和代码比基于struct的方法更加繁琐。那我们现在怎么办呢?当然是回到起点了。

有一个秘密方法

我们可以自己处理内存分配,就像勇闯蜘蛛的巢穴一样。不幸的是,这就是我们分道扬镳的地方,因为我对此并不是很了解。我会给你提供一个链接链接 - 也许实际上是链接链接。这相当复杂,令人质疑:是否有其他优化方法可尝试?缓存一致性和优越算法是一种方法,谨慎应用P/Invoke来进行性能关键代码也是一种方法。您还可以为关键方法/类应用前面提到的仅结构内存分配。

祝好运,并告诉我们如果您找到更好的替代方案。

附录:源代码

ChunkAllocator.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace MemAllocLib
{
    public sealed class ChunkAllocator : IDisposable
    {
        IntPtr m_chunkStart;
        int m_offset;//offset from already allocated memory
        readonly int m_size;

        public ChunkAllocator(int memorySize = 1024)
        {
            if (memorySize < 1)
                throw new ArgumentOutOfRangeException("memorySize must be positive");

            m_size = memorySize;
            m_chunkStart = Marshal.AllocHGlobal(memorySize);
        }
        ~ChunkAllocator()
        {
            Dispose();
        }

        public IntPtr Allocate<T>() where T : struct
        {
            int reqBytes = Marshal.SizeOf(typeof(T));//not highly performant
            return Allocate<T>(reqBytes);
        }

        public IntPtr Allocate<T>(int reqBytes) where T : struct
        {
            if (m_chunkStart == IntPtr.Zero)
                throw new ObjectDisposedException("ChunkAllocator");
            if (m_offset + reqBytes > m_size)
                throw new OutOfMemoryException("Too many bytes allocated: " + reqBytes + " needed, but only " + (m_size - m_offset) + " bytes available");

            T created = default(T);
            Marshal.StructureToPtr(created, m_chunkStart + m_offset, false);
            m_offset += reqBytes;

            return m_chunkStart + (m_offset - reqBytes);
        }

        public void Dispose()
        {
            if (m_chunkStart != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(m_chunkStart);
                m_offset = 0;
                m_chunkStart = IntPtr.Zero;
            }
        }

        public void ReleaseAllMemory()
        {
            m_offset = 0;
        }

        public int AllocatedMemory
        {
            get { return m_offset; }
        }

        public int AvailableMemory
        {
            get { return m_size - m_offset; }
        }

        public int TotalMemory
        {
            get { return m_size; }
        }

        public static T ConvertPointerToStruct<T>(IntPtr ptr) where T : struct
        {
            return (T)Marshal.PtrToStructure(ptr, typeof(T));
        }

        public static void StoreStructure<T>(IntPtr ptr, T data) where T : struct
        {
            Marshal.StructureToPtr(data, ptr, false);
        }
    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MemoryAllocation
{
    class Program
    {
        static void Main(string[] args)
        {
            using (MemAllocLib.ChunkAllocator chunk = new MemAllocLib.ChunkAllocator())
            {
                Console.WriteLine(">> Simple data test");
                SimpleDataTest(chunk);

                Console.WriteLine();

                Console.WriteLine(">> Complex data test");
                ComplexDataTest(chunk);
            }

            Console.ReadLine();
        }

        private static void SimpleDataTest(MemAllocLib.ChunkAllocator chunk)
        {
            IntPtr ptr = chunk.Allocate<System.Int32>();

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr) == 0, "Data not initialized properly");
            System.Diagnostics.Debug.Assert(chunk.AllocatedMemory == sizeof(Int32), "Data not allocated properly");

            int data = MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr);
            data = 10;
            MemAllocLib.ChunkAllocator.StoreStructure(ptr, data);

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr) == 10, "Data not set properly");

            Console.WriteLine("All tests passed");
        }

        private static void ComplexDataTest(MemAllocLib.ChunkAllocator chunk)
        {
            IntPtr ptr = chunk.Allocate<Person>();

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Age == 0, "Data age not initialized properly");
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Name == null, "Data name not initialized properly");
            System.Diagnostics.Debug.Assert(chunk.AllocatedMemory == System.Runtime.InteropServices.Marshal.SizeOf(typeof(Person)) + sizeof(Int32), "Data not allocated properly");

            Person data = MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr);
            data.Name = "Bob";
            data.Age = 20;
            MemAllocLib.ChunkAllocator.StoreStructure(ptr, data);

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Age == 20, "Data age not set properly");
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Name == "Bob", "Data name not set properly");

            Console.WriteLine("All tests passed");
        }

        struct Person
        {
            public string Name;
            public int Age;

            public Person(string name, int age)
            {
                Name = name;
                Age = age;
            }

            public override string ToString()
            {
                if (string.IsNullOrWhiteSpace(Name))
                    return "Age is " + Age;
                return Name + " is " + Age + " years old";
            }
        }
    }
}

我认为你的回答值得点赞,因为它涉及了很多努力。然而,它仍然没有真正回答我的问题。特别地,我应该说想使用非托管内存的原因是希望 GC 可以忽略指向 GC 堆之外的“拼凑在一起”的引用。没有其他理由将数据放入非托管内存 - 因此编组肯定是不可取的。 - Andrew Russell
在这种情况下,不行,除非你可以完全避免垃圾回收。或者你可以简单地复制一个巨大的内存块(作为字节数组)。 - GGulati

2
您可以使用P/Invoke从.NET调用C++代码,也可以编写托管的C++代码,从而在.NET语言中完全访问本机API。 但是,在托管端,您只能使用托管类型,因此必须封装不受管控的对象。
举个简单的例子:Marshal.AllocHGlobal 允许您在Windows堆上分配内存。 返回的句柄在.NET中没有太多用处,但在调用需要缓冲区的本机Windows API时可能会用到。

虽然这并没有回答我的问题。但是我想在非托管内存中使用托管类型 ;) - Andrew Russell

2

这是不可能的。

但是你可以使用一个托管结构体并创建该结构体类型的指针。该指针可以指向任何地方(包括非托管内存)。

问题是,为什么要在非托管内存中拥有一个类?你无论如何都无法获得GC功能。你只需使用指向结构体的指针即可。


结构体带有按值传递的语义,我想避免这种情况。 - Andrew Russell
不是结构体而是结构体指针。结构体指针的行为与C ++中的大致相同。你同意吗? - usr
抱歉 - 我误读了你的回答。你是完全正确的。我需要考虑一下这个替代方案的可行性。你可能有点眼光... - Andrew Russell
一个结构体必须声明为 unsafe 才能包含指向其他结构体的指针,参见 http://msdn.microsoft.com/en-us/library/aa664771。不安全的结构体基本上与 C++ 结构体相同,但不支持继承。所有操作指针的代码也必须声明为 unsafe - Anton Tykhyy
@AntonTykhyy 的确。当我有机会尝试这个想法时,我将尝试回答的第一个问题是“我需要输入多少次 unsafe?” ;) - Andrew Russell

0

这是不可能的。您可以在不安全的上下文中访问托管内存,但该内存仍然是托管的,并受GC控制。

为什么?

简单和安全。

但是现在我想起来了,我认为您可以使用C++/CLI混合托管和非托管代码。但我不确定,因为我从未使用过C++/CLI。


我并不是在寻找“为什么”的答案,而是需要一个技术性的解释。 - Andrew Russell
这实际上是最常引用的原因之一:http://blogs.msdn.com/b/ericlippert/archive/2009/06/22/why-doesn-t-c-implement-top-level-methods.aspx - Euphoric
我想说的是:我已经知道这是一个坏主意,以及为什么它是一个坏主意。我知道这是一种hack。我想问的是,是否有人知道如果我将本质上是一种(巧妙制作的、具体的)破坏引入到CLR的内存模型中,CLR会如何爆炸。以及如何引入这样的破坏。 - Andrew Russell
@AndrewRussell:你不能“破坏”托管内存(虽然从未托管的代码中可以,但在那个世界里你可以做任何事情)。但是,有一些方法可以从.NET访问未托管内存,允许您进行自己的内存处理,包括引入任何想要引入的“破坏”。在.NET中,这将是不透明对象,如IntPtr等。 - Martin Liversage

0

我不知道如何在非托管堆中保存C#类实例,即使是在C++/CLI中也不行。


您可以将托管对象固定在托管堆上,以便非托管代码可以访问它。否则,托管垃圾回收可能会移动该对象,导致非托管代码出现“悬空”指针。不存在固定非托管对象的情况。 - Martin Liversage
@Shahar 我本来想发表评论的,但我认为Martin已经更好地解释了。你在回答中所说的并没有意义。 - Andrew Russell
我删除了我的回答,以防止人们犯错。我会更多地了解固定功能,以确保我理解这个主题... - Shahar

0
可以在.NET中完全设计一个值类型分配器,而不使用任何非托管代码,它可以分配和释放任意数量的值类型实例,而不会产生任何重要的GC压力。诀窍是创建相对较少的数组(可能为每种类型创建一个数组)来保存实例,然后传递“实例引用”结构,其中包含所需索引的数组索引。
例如,假设我想要一个“生物”类,它包含XYZ位置(float),XYZ速度(也是float),滚动/俯仰/偏航(同上),伤害(float)和种类(枚举)。接口“ICreatureReference”将为所有这些属性定义getter和setter。典型的实现将是一个结构体CreatureReference,具有单个私有字段int _index,以及像下面这样的属性访问器:
系统会保留一个已用和空闲数组槽位列表(如果需要,可以使用 Creatures 中的一个字段来形成空闲槽位的链接列表)。CreatureReference.Create 方法会从空闲项列表中分配一个项目;CreatureReference 实例的 Dispose 方法会将其数组槽位添加到空闲项列表中。

这种方法最终需要大量的样板代码,但它可以相当高效并避免GC压力。最大的问题可能是(1)它使structs的行为更像引用类型而不是structs,以及(2)它需要绝对的纪律来调用IDispose,因为未处理的数组插槽永远不会被回收。另一个令人烦恼的怪癖是,即使属性设置器不会尝试改变应用于它们的CreatureReference实例的任何字段,但无法使用只读值为CreatureReference类型的属性设置器。使用接口ICreatureReference可以避免这个困难,但必须小心,只声明泛型类型的存储位置约束为ICreatureReference,而不是声明存储位置为ICreatureReference


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