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

And*_*ell 12 .net c# clr garbage-collection unmanaged-memory

更新:现在有一个被接受的答案"有效".你应该永远,永远,永远,永远使用它.曾经.


首先让我先说明我是一名游戏开发者.想要这样做有一个合法的 - 如果非常不寻常 - 与性能相关的原因.


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

class Foo
{
    public int a, b, c;
    public void MyMethod(int d) { a = d; b = d; c = a + b; }
}
Run Code Online (Sandbox Code Playgroud)

没有什么花哨.请注意,它是仅包含值类型的引用类型.

在托管代码中,我希望有这样的东西:

Foo foo;
foo = Voodoo.NewInUnmanagedMemory<Foo>(); // <- ???
foo.MyMethod(1);
Run Code Online (Sandbox Code Playgroud)

这个功能NewInUnmanagedMemory会是什么样的?如果无法在C#中完成,可以在IL中完成吗?(或者可能是C++/CLI?)

基本上:有没有办法 - 无论多么苛刻 - 将一些完全任意的指针转换为对象引用.并且 - 没有使CLR爆炸 - 该死的后果.

(提出问题的另一种方法是:"我想为C#实现自定义分配器")

这导致了后续问题:当面对指向托管内存之外的引用时,垃圾收集器会做什么(特定于实现,如果需要)?

并且,与此相关,如果Foo将引用作为成员字段会发生什么?如果它指向托管内存怎么办?如果它只指向非托管内存中分配的其他对象怎么办?

最后,如果这是不可能的:为什么?


更新:到目前为止,这是"缺失的部分":

#1:如何将IntPtr对象转换为对象引用?虽然无法验证的IL可能是可能的(见评论).到目前为止,我没有运气.该框架似乎非常小心,以防止这种情况发生.

(能够在运行时获取非blittable托管类型的大小和布局信息也很好.同样,框架试图使这变得不可能.)

#2:假设问题可以解决 - 当GC遇到指向GC堆外部的对象引用时,它会做什么?它崩溃了吗?Anton Tykhyy 在他的回答中猜测它会.考虑到框架对于防止#1的谨慎程度,它似乎很可能.确认这一点的东西会很好.

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

基于此,我倾向于认为这种黑客攻击的想法是不可能的 - 或者至少不值得努力.但我有兴趣得到#1或#2或两者的技术细节的答案.

Ant*_*hyy 7

"我想为C#实现自定义分配器"

GC是CLR的核心.只有Microsoft(或Mono的Mono团队)才能取代它,但开发工作成本很高.GC是CLR的核心,搞乱GC或托管堆会使CLR崩溃 - 如果你非常幸运的话,很快就会崩溃.

当面对指向托管内存之外的引用时,垃圾收集器会做什么(特定于实现,如果需要)?

它以特定于实现的方式崩溃;)


Ill*_*ack 6

我一直在尝试在非托管内存中创建类.这是可能的,但有一个我目前无法解决的问题 - 您无法将对象分配给引用类型字段 - 请在底部进行编辑,这样您的自定义类中只能包含结构字段. 这是邪恶的:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您关心内存泄漏,那么在完成课程后应始终调用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
}
Run Code Online (Sandbox Code Playgroud)

我希望它会帮助你走上正轨.

编辑:找到引用类型字段的解决方案:

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;
}
Run Code Online (Sandbox Code Playgroud)

编辑:更好的解决方案.只需使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)


GGu*_*ati 5

纯 C# 方法

所以,有几个选择。最简单的方法是在结构体的不安全上下文中使用 new/delete。第二种是使用内置的编组服务来处理非托管内存(代码如下)。但是,这两种方法都处理结构(尽管我认为后一种方法非常接近您想要的)。我的代码有一个限制,因为您必须始终坚持结构并使用 IntPtrs 进行引用(使用 ChunkAllocator.ConvertPointerToStructure 获取数据,使用 ChunkAllocator.StoreStructure 存储更改的数据)。这显然很麻烦,所以如果你使用我的方法,你最好真的想要性能。但是,如果你正在处理值类型,这种方式已经足够。

绕道:CLR 中的类

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

一个警告

像往常一样,事情很少像是/否那样简单。引用类型的真正复杂性与垃圾收集器在垃圾收集期间如何压缩分配的内存有关。如果您能以某种方式确保垃圾收集不会发生或不会影响相关数据(请参阅fixed 关键字),那么您可以将任意指针转换为对象引用(只需将指针偏移 8 个字节,然后将该数据解释为具有相同字段和内存布局的结构;也许可以使用StructLayoutAttribute来确定)。我会试验非虚拟方法,看看它们是否有效;他们应该(特别是如果你把它们放在结构上)但是由于你必须丢弃虚方法表,虚方法是不行的。

一个人不只是走进魔多

简单地说,这意味着不能在非托管内存中分配托管引用类型(类)。您可以在 C++ 中使用托管引用类型,但这些类型会受到垃圾回收的影响……并且该过程和代码比struct基于 - 的方法更痛苦。这让我们何去何从?当然,回到我们开始的地方。

有一种秘密方式

我们可以自己勇敢地进行Shelob 的 Lair内存分配。不幸的是,这是我们必须分开的地方,因为我对此并不了解。我将为您提供一个链接2 -也许34的现状。这相当复杂,并引出了一个问题:您还可以尝试其他优化吗?缓存一致性和高级算法是一种方法,对于性能关键代码的 P/Invoke 的明智应用也是一种方法。您还可以为关键方法/类应用上述仅结构的内存分配。

祝你好运,如果你找到更好的选择,请告诉我们。

附录:源代码

块分配器

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

程序.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";
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)