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(コメントを参照)でも可能かもしれません。これまでのところ、私はこれで運がありませんでした。フレームワークは、これが起こらないように非常に注意を払っているようです。

(blittable でないマネージド型のサイズとレイアウト情報を実行時に取得できると便利です。繰り返しますが、フレームワークはこれを不可能にしようとします。)

#2:問題 1 を解決できると仮定すると、GC ヒープの外部を指すオブジェクト参照に遭遇した場合、GC はどうしますか? クラッシュしますか?Anton Tykhyy は、彼の答えで、そうなると推測しています。フレームワークが#1を防ぐためにどれほど注意を払っているかを考えると、そうなる可能性が高いようです. これを確認する何かがいいでしょう。

(または、オブジェクト参照が GC ヒープ内の固定メモリを指すこともできます。違いはありますか?)

これに基づいて、私はこのハッキングのアイデアは不可能であると考える傾向があります-または少なくとも努力する価値はありません. しかし、#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 を使用することです。2 つ目は、組み込みのマーシャリング サービスを使用してアンマネージ メモリを処理することです (このコードは以下に表示されています)。ただし、これらは両方とも構造体を処理します (ただし、後者の方法は必要なものに非常に近いと思います)。私のコードには、構造全体に固執し、参照に IntPtrs を使用する必要があるという制限があります (ChunkAllocator.ConvertPointerToStructure を使用してデータを取得し、ChunkAllocator.StoreStructure を使用して変更されたデータを保存します)。これは明らかに面倒なので、私のアプローチを使用する場合は、本当にパフォーマンスが必要です。ただし、値型のみを扱う場合は、このアプローチで十分です。

回り道: CLR のクラス

クラスには、割り当てられたメモリに 8 バイトの「プレフィックス」があります。4 バイトはマルチスレッドの同期インデックス用で、4 バイトはそれらのタイプ (基本的には仮想メソッド テーブルとランタイム リフレクション) を識別するためのものです。これらは CLR 固有であり、実行時に同期インデックスが変更される可能性があるため、これによりアンマネージ メモリの処理が難しくなります。実行時オブジェクト作成の詳細についてはこちらを、参照型のメモリ レイアウトの概要についてはこちらを参照してください。より詳細な説明については、C# による CLRも参照してください。

警告

いつものように、イエス/ノーのような単純なことはめったにありません。参照型の実際の複雑さは、ガベージ コレクション中にガベージ コレクターが割り当てられたメモリを圧縮する方法に関係しています。ガベージ コレクションが発生しないこと、または問題のデータに影響を与えないことを何らかの方法で確認できる場合 ( fixed キーワードを参照)、任意のポインターをオブジェクト参照に変えることができます (ポインターを 8 バイトだけオフセットし、次に、そのデータを同じフィールドとメモリ レイアウトを持つ構造体として解釈します (おそらく確実にStructLayoutAttributeを使用します)。非仮想メソッドを試して、それらが機能するかどうかを確認します。それらは(特に構造体に配置する場合)必要がありますが、仮想メソッドテーブルは破棄する必要があるため、仮想メソッドは使用できません。

モルドールに足を踏み入れるだけではない

簡単に言えば、これはマネージド参照型 (クラス) をアンマネージド メモリに割り当てることができないことを意味します。structC++ でマネージ参照型を使用することもできますが、それらはガベージ コレクションの対象となります...そして、プロセスとコードは、ベースのアプローチよりも面倒です。それは私たちをどこに残しますか?もちろん、出発点に戻ります。

秘密の方法があります

自分たちでShelob's Lair のメモリ割り当てに立ち向かうことができました。残念ながら、私はそこまで詳しくないので、ここで私たちの道は分かれなければなりません。リンクを 1 つまたは2つ、実際には3 つまたは4 つ提供します。これはかなり複雑で、次のような疑問が生じます。他に試すことができる最適化はありますか? キャッシュの一貫性と優れたアルゴリズムは、パフォーマンスが重要なコードに対する P/Invoke の賢明な適用と同様に、1 つのアプローチです。前述の構造体のみのメモリ割り当てを主要なメソッド/クラスに適用することもできます。

頑張ってください。より優れた代替案が見つかったらお知らせください。

付録: ソースコード

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";
            }
        }
    }
}
于 2012-05-30T02:26:31.647 に答える
2

C++ でコードを記述し、P/Invoke を使用して .NET から呼び出すことができます。または、.NET 言語内からネイティブ API に完全にアクセスできるマネージド C++ でコードを記述できます。ただし、マネージド側ではマネージド型しか操作できないため、アンマネージド オブジェクトをカプセル化する必要があります。

簡単な例を挙げると、 Marshal.AllocHGlobalを使用すると、Windows ヒープにメモリを割り当てることができます。返されるハンドルは、.NET ではあまり使用されませんが、バッファーを必要とするネイティブ Windows API を呼び出すときに必要になる場合があります。

于 2012-05-29T13:27:37.980 に答える
2

これは不可能です。

ただし、マネージ構造体を使用して、この構造体型のポインターを作成できます。このポインターは、どこでも (アンマネージ メモリを含む) を指すことができます。

問題は、なぜクラスをアンマネージ メモリに配置する必要があるのか​​ということです。とにかくGC機能は得られません。構造体へのポインターを使用するだけです。

于 2012-05-29T13:29:13.827 に答える
0

C ++ / CLIでも、アンマネージヒープにC#クラスインスタンスを保持する方法がわかりません。

于 2012-05-29T13:49:30.350 に答える
0

アンマネージ コードを使用せずに、値型アロケーターを .net 内で完全に設計することができます。これにより、大きな GC プレッシャーなしで、任意の数の値型インスタンスを割り当てて解放できます。秘訣は、インスタンスを保持するために比較的少数の配列 (おそらくタイプごとに 1 つ) を作成し、問題のインデックスの配列インデックスを保持する「インスタンス参照」構造体を渡すことです。

たとえば、XYZ 位置 ( float)、XYZ 速度 (またfloat)、ロール/ピッチ/ヨー (同上)、ダメージ (フロート)、および種類 (列挙) を保持する「クリーチャー」クラスが必要であるとします。インターフェイス「ICreatureReference」は、これらすべてのプロパティのゲッターとセッターを定義します。典型的な実装はCreatureReference、単一のプライベート フィールドを持つ構造体と、次のint _indexようなプロパティ アクセサーです。

  フロート位置 {
    get {return Creatures[_index].Position;}
    set {Creatures[_index].Position = value;}
  };

システムは、どのアレイ スロットが使用され、空いているかのリストを保持します (必要に応じて、その中のフィールドの 1 つを使用してCreatures、空いているスロットのリンクされたリストを形成できます)。このCreatureReference.Createメソッドは、空きアイテム リストからアイテムを割り当てます。インスタンスのDisposeメソッドは、CreatureReferenceその配列スロットを空アイテム リストに追加します。

このアプローチは、面倒な量のボイラープレート コードを必要とすることになりますが、かなり効率的であり、GC プレッシャーを回避できます。の最大の問題は、おそらく、(1)structsよりも参照型のように振る舞うことstructs、(2) の呼び出しIDisposeに絶対的な規律が必要であることです。もう 1 つの面倒な癖は、プロパティ セッターが適用されるインスタンスCreatureReferenceのフィールドを変更しようとしない場合でも、type の読み取り専用値にプロパティ セッターを使用できないことです。CreatureReferenceインターフェイスを使用すると、この問題を回避できますが、 の格納場所を宣言するのではなく、 にICreatureReference制約されたジェネリック型の格納場所のみを宣言するように注意する必要があります。ICreatureReferenceICreatureReference

于 2012-06-01T03:01:15.423 に答える
0

そのようなことは不可能です。安全でないコンテキストで管理されたメモリにアクセスできますが、そのメモリは引き続き管理され、GC の対象となります。

なんで?

シンプルさとセキュリティ。

しかし、今考えてみると、マネージドとアンマネージドを C++/CLI で混在させることができると思います。しかし、私は C++/CLI を使用したことがないため、それについてはよくわかりません。

于 2012-05-29T13:24:17.983 に答える