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?)

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

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

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

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

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


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

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

(如果能够在运行时获取非 blittable 托管类型的大小和布局信息,那也很好。同样,框架试图让这成为不可能。)

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

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

基于此,我倾向于认为这种 hack 的想法是不可能的——或者至少不值得付出努力。但我很想得到一个涉及#1 或#2 或两者的技术细节的答案。

4

9 回答 9

7

“我想为 C# 实现一个自定义分配器”

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

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

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

于 2012-05-29T13:59:05.983 回答
7

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

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
}

我希望它能在你的道路上帮助你。

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

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);
    }
}
于 2012-12-11T19:08:33.913 回答
5

纯 C# 方法

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

绕道:CLR 中的类

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

警告

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

一个人不会简单地走进魔多

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

有一个秘密的方式

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

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

附录:源代码

块分配器.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);
        }
    }
}

程序.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";
            }
        }
    }
}
于 2012-05-30T02:26:31.647 回答
2

您可以使用 C++ 编写代码并使用 P/Invoke 从 .NET 调用它,也可以使用托管 C++ 编写代码,让您可以从 .NET 语言内部完全访问本机 API。但是,在托管方面,您只能使用托管类型,因此您必须封装非托管对象。

举个简单的例子:Marshal.AllocHGlobal允许你在 Windows 堆上分配内存。返回的句柄在 .NET 中没有多大用处,但在调用需要缓冲区的本机 Windows API 时可能需要。

于 2012-05-29T13:27:37.980 回答
2

这是不可能的。

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

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

于 2012-05-29T13:29:13.827 回答
0

这样的事情是不可能的。您可以在不安全的上下文中访问托管内存,但所述内存仍然是托管的并受制于 GC。

为什么?

简单和安全。

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

于 2012-05-29T13:24:17.983 回答
0

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

于 2012-05-29T13:49:30.350 回答
0

可以完全在 .net 中设计一个值类型分配器,而无需使用任何非托管代码,它可以分配和释放任意数量的值类型实例,而没有任何显着的 GC 压力。诀窍是创建相对少量的数组(可能每种类型一个)来保存实例,然后传递保存相关索引的数组索引的“实例引用”结构。

例如,假设我想要一个“生物”类,它包含 XYZ 位置(float)、XYZ 速度(也float)、滚动/俯仰/偏航(同上)、伤害(浮动)和种类(枚举)。接口“ICreatureReference”将为所有这些属性定义 getter 和 setter。一个典型的实现将是一个CreatureReference具有单个私有字段的结构int _index,以及像这样的属性访问器:

  浮动位置{
    得到 {return Creatures[_index].Position;}
    设置 {Creatures[_index].Position = value;}
  };

系统将保留一个列表,其中列出了哪些阵列插槽已使用和空置(如果需要,它可以使用其中的一个字段Creatures来形成空置插槽的链接列表)。该CreatureReference.Create方法将从 vacant-items 列表中分配一个项目;实例的Dispose方法CreatureReference会将其数组槽添加到 vacant-items 列表中。

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

于 2012-06-01T03:01:15.423 回答
-2

我在不依赖 Emit 的情况下编写了类似的实现,需要 .NET6(System.Runtime.InteropServices.NativeMemory)

用法:

using Native;
using System;

unsafe class Program
{
    class TestA : NativeClass
    {
        public int x;

        public virtual int GetX()
        {
            return x;
        }
    }

    class TestB : TestA
    {
        public int y;

        public override int GetX()
        {
            return base.GetX() * 3;
        }
    }

    public static unsafe void Main(string[] args)
    {
        TestA a = Factory<TestB>.Create();
        a.x = 10;

        int val = a.GetX();
        Console.WriteLine($"val is {val}");
        a.Free();
    }

}

// - - 输出 - -

val is 30

暗示:

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Native
{
    unsafe abstract class NativeClass
    {
        private void* _ptr;
        public void SetPtr(void* ptr)
        {
            if (_ptr != null) throw new Exception("ptr can only be assigned once");
            _ptr = ptr;
        }
        public void Free() { if (_ptr != null) NativeMemory.Free(_ptr); }
    }

    unsafe struct Factory<T> where T : NativeClass, new()
    {
        public static uint size { get; private set; }
        static byte[] header; // vtable *, monitor * and so on

        static readonly Func<IntPtr, T> cast2T;

        /// <summary>
        /// Create object at addr given
        /// </summary>
        /// <param name="ptr"></param>
        /// <returns></returns>
        public static T Create(void * ptr = null)
        {
            if (ptr == null) ptr = NativeMemory.Alloc(size);

            fixed (void* headerPtr = header) Buffer.MemoryCopy(headerPtr, ptr, header.Length, header.Length);

            T ret = cast2T(new IntPtr(ptr));
            ret.SetPtr(ptr);
            return ret;
        }

        public static object GetO_Dummy(object obj)
        {
            return obj;
        }

        static Factory()
        {
            IntPtr tptr = typeof(T).TypeHandle.Value;
            size = (uint)Marshal.ReadInt32(tptr, 4);

            T obj = new T();
            void* pObj = Unsafe.AsPointer(ref obj);

            header = new byte[MinClass.size];
            fixed (void* headerPtr = header)
                Buffer.MemoryCopy((void*)*(long*)pObj, headerPtr, header.Length, header.Length);

            var dummyFunc = GetO_Dummy;
            cast2T = Unsafe.Read<Func<IntPtr, T>>(Unsafe.AsPointer(ref dummyFunc));
        }
    }

    class MinClass
    {
        public static uint size { get; private set; }
        static MinClass()
        {
            IntPtr tptr = typeof(MinClass).TypeHandle.Value;
            size = (uint)Marshal.ReadInt32(tptr, 4);
        }
    }
}

https://github.com/Misaka-Mikoto-Tech/MonoHook/blob/master/Assets/Scripts/Others/NativeClass.cs

于 2021-12-20T17:44:48.347 回答