Как экземпляр класса С# в неуправляемой памяти? (Возможное?)

ОБНОВЛЕНИЕ: теперь есть принятый ответ, который "работает". Вы никогда не должны когда-либо использовать его. Когда-либо.


Сначала позвольте мне предисловие к моему вопросу, заявив, что я разработчик игр. Там есть законная, если очень необычная причина, связанная с производительностью, желающая сделать это.


Скажем, у меня есть класс С# следующим образом:

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? Если это невозможно сделать в С#, можно ли это сделать в IL? (Или, может быть, С++/CLI?)

В принципе: есть ли способ - как бы хаки - не превратить какой-то полностью произвольный указатель в ссылку на объект. И - не допустить, чтобы CLR взорвался - прокляните последствия.

(Еще один способ поставить вопрос: "Я хочу реализовать пользовательский распределитель для С#" )

Это приводит к следующему вопросу: что делает сборщик мусора (если это необходимо для реализации), когда сталкивается с ссылкой, указывающей вне управляемой памяти?

И, связанный с этим, что произойдет, если Foo имеет ссылку как поле участника? Что, если он указал на управляемую память? Что делать, если он только указывал на другие объекты, выделенные в неуправляемой памяти?

Наконец, если это невозможно: почему?


Обновление: Ниже приведены "недостающие части":

# 1: Как преобразовать IntPtr в ссылку на объект? Возможно, это будет возможно хотя и непроверяемый IL (см. Комментарии). До сих пор мне не повезло. Структура, по-видимому, чрезвычайно тщательна, чтобы это не происходило.

(Также было бы неплохо иметь возможность получать информацию о размере и макете для не-blittable управляемых типов во время выполнения. Опять же, структура пытается сделать это невозможным.)

# 2: Предполагая, что проблема может быть решена - что делает GC, когда она встречает ссылку на объект, которая указывает вне кучи GC? Это крушение? Антон Тихий, в своем ответе, догадывается, что так оно и будет. Учитывая, насколько осторожна структура, чтобы предотвратить # 1, это кажется вероятным. Что-то, что подтверждает это, было бы неплохо.

(Альтернативно ссылка на объект может указывать на закрепленную память внутри кучи GC. Разве это имеет значение?)

Исходя из этого, я склонен думать, что эта идея для взлома невозможна или, по крайней мере, не стоит усилий. Но мне было бы интересно получить ответ, который входит в технические детали №1 или №2 или обоих.

Ответ 1

Я экспериментировал с созданием классов в неуправляемой памяти. Это возможно, но у меня есть проблема, которую я в настоящее время не могу решить - вы не можете назначать объекты для полей ссылочного типа -see edit внизу, так что вы можете иметь только поля структуры в своем обычном класс. Это зло:

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);
    }
}

Ответ 2

"Я хочу реализовать пользовательский распределитель для С#"

GC находится в ядре CLR. Только Microsoft (или команда Mono в случае Mono) может заменить ее, при большой стоимости усилий в области развития. GC, находящаяся в ядре CLR, возится с GC или управляемой кучей, приведет к краху CLR - быстро, если вам очень повезет.

Что делает сборщик мусора (при необходимости, конкретный для реализации), когда сталкивается с ссылкой, которая указывает за пределы управляемой памяти?

Он разбивается по конкретному пути реализации;)

Ответ 3

Чистый подход С#

Итак, есть несколько вариантов. Самый простой способ - использовать new/delete в небезопасном контексте для структур. Во-вторых, использовать встроенные службы Маршаллинга для работы с неуправляемой памятью (код для этого показан ниже). Тем не менее, оба они касаются структур (хотя, я думаю, последний метод очень близок к тому, что вы хотите). Мой код имеет ограничение в том, что вы должны придерживаться структур во всем и использовать IntPtrs для ссылок (используя ChunkAllocator.ConvertPointerToStructure для получения данных и ChunkAllocator.StoreStructure для хранения измененных данных). Это, безусловно, громоздко, поэтому вам лучше всего захотеть, если вы используете мой подход. Однако, если вы имеете дело только с типами значений, этот подход является достаточным.

Объяснение: классы в среде CLR

Классы имеют 8-байтовый "префикс" в выделенной памяти. Четыре байта для индекса синхронизации для многопоточности, а четыре байта предназначены для идентификации их типа (в основном, таблицы виртуальных методов и отражения во время выполнения). Это затрудняет работу с неамунизируемой памятью, поскольку они являются специфичными для CLR, и поскольку индекс синхронизации может меняться во время выполнения. См. здесь для получения подробной информации о создании объекта времени выполнения и здесь для обзора макета памяти для ссылочного типа. Также проверьте CLR через С# для более подробного объяснения.

Предостережение

Как обычно, все бывает так просто, как да/нет. Реальная сложность ссылочных типов связана с тем, как сборщик мусора собирает память во время сбора мусора. Если вы можете как-то убедиться, что сбор мусора не произойдет или что он не повлияет на данные, о которых идет речь (см. фиксированное ключевое слово), тогда вы можете превратить произвольный указатель в ссылку на объект (просто смещать указатель на 8 байтов, а затем интерпретировать эти данные как структуру с теми же полями и макетом памяти, возможно использовать StructLayoutAttribute). Я бы экспериментировал с не виртуальными методами, чтобы убедиться, что они работают; они должны (особенно если вы поместите их в структуру), но виртуальные методы не идут из-за таблицы виртуальных методов, которую вы должны отбросить.

Один не просто идет в Мордор

Проще говоря, это означает, что управляемые ссылочные типы (классы) не могут быть выделены в неуправляемой памяти. Вы можете использовать управляемые ссылочные типы в С++, но они будут подвержены сбору мусора... и процесс и код более болезненны, чем подход, основанный на 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";
            }
        }
    }
}

Ответ 4

Вы можете написать код на С++ и вызвать его из .NET с помощью P/Invoke или вы можете написать код в управляемом С++, который дает вам полный доступ к собственному API с языка .NET. Однако на управляемой стороне вы можете работать только с управляемыми типами, поэтому вам придется инкапсулировать неуправляемые объекты.

Чтобы дать простой пример: Marshal.AllocHGlobal позволяет выделять память на кучу Windows. Возвращенный дескриптор не очень полезен в .NET, но может потребоваться при вызове собственного Windows API, требующего наличия буфера.

Ответ 5

Это невозможно.

Однако вы можете использовать управляемую структуру и создать указатель этого типа структуры. Этот указатель может указывать в любом месте (в том числе в неуправляемую память).

Вопрос в том, почему вы хотите иметь класс в неуправляемой памяти? В любом случае вы не получите GC. Вы можете просто использовать указатель-к-структуре.

Ответ 6

Ничего подобного не возможно. Вы можете получить доступ к управляемой памяти в небезопасном контексте, но эта память все еще управляется и подчиняется GC.

Почему?

Простота и безопасность.

Но теперь, когда я думаю об этом, я думаю, что вы можете смешивать управляемые и неуправляемые с С++/CLI. Но я не уверен в этом, потому что я никогда не работал с С++/CLI.

Ответ 7

Я не знаю способ удержания экземпляра класса С# в неуправляемой куче, даже в С++/CLI.

Ответ 8

Можно создать универсальный распределитель значений в пределах .net без использования неуправляемого кода, который может выделять и освобождать произвольное количество экземпляров типа значения без какого-либо значительного давления в ГК. Трюк состоит в том, чтобы создать относительно небольшое количество массивов (возможно, для каждого типа) для хранения экземпляров, а затем передать "структуры ссылок на экземпляр", которые содержат индексы массивов соответствующего индекса.

Предположим, например, что я хочу иметь класс "существа", который содержит позиции XYZ (float), скорость XYZ (также float), roll/pitch/yaw (ditto), damage (float), и вид (перечисление). Интерфейс "ICreatureReference" будет определять геттеры и сеттеры для всех этих свойств. Типичной реализацией будет struct CreatureReference с одним частным полем int _index, а также обладатели свойств, такие как:

  float Position {
    get {return Creatures[_index].Position;} 
    set {Creatures[_index].Position = value;}
  };

Система сохранит список используемых слотов массива и освободится (при желании можно использовать одно из полей внутри Creatures, чтобы сформировать связанный список свободных слотов). Метод CreatureReference.Create выделил бы элемент из списка вакантных позиций; метод Dispose экземпляра CreatureReference добавит свой слот массива в список вакантных позиций.

Этот подход заканчивается тем, что требует раздражающего количества кода шаблона, но он может быть достаточно эффективным и избегать давления в ГК. Самые большие проблемы, вероятно, в том, что (1) он делает structs более похожими на ссылочные типы, чем structs, и (2) он требует абсолютной дисциплины с вызовом IDispose, так как неиспользуемые слоты массива никогда не будут восстановлены. Еще одна неприятная причуда заключается в том, что не удастся использовать средства определения свойств для значений типа "только для чтения" типа CreatureReference, хотя разработчики свойств не будут пытаться мутировать любые поля экземпляра CreatureReference, к которому они применяются. Использование интерфейса ICreatureReference может избежать этой трудности, но нужно быть осторожным, чтобы объявлять места хранения общих типов, ограниченных ICreatureReference, вместо объявления мест хранения ICreatureReference.