64

C# で浅いコピーを行う最速の方法は何だろうか? 浅いコピーを行うには2つの方法があることだけを知っています:

  1. MemberwiseClone
  2. 各フィールドを 1 つずつコピーする (手動)

(2)は(1)よりも速いことがわかりました。浅いコピーを行う別の方法があるかどうか疑問に思っていますか?

4

8 に答える 8

79

これは、考えられる解決策が多数あり、それぞれに多くの長所と短所がある複雑な問題です。C# でコピーを作成するさまざまな方法を概説している素晴らしい記事がここにあります。要約する:

  1. 手動でクローン
    を作成する 面倒ですが、高度な制御が可能です。

  2. MemberwiseClone
    のみでクローンを作成すると、浅いコピーが作成されます。つまり、参照型フィールドの場合、元のオブジェクトとそのクローンは同じオブジェクトを参照します。

  3. Reflection を使用したクローン
    デフォルトでは浅いコピーが行われ、ディープ コピーを実行するように書き換えることができます。利点: 自動化。短所:反射が遅い。

  4. シリアル化による複製
    簡単、自動化。一部の制御を放棄すると、シリアル化が最も遅くなります。

  5. IL を使用してクローンを作成、拡張メソッドを使用してクローンを作成、
    より高度なソリューションであり、一般的ではありません。

于 2009-06-08T19:28:47.357 に答える
35

いくつかの引用から始めたいと思います:

実際、特に複合型の場合、MemberwiseClone は通常、他のものよりもはるかに優れています。

よくわかりません。MemberwiseClone() は、浅いコピーの場合、他のすべてのパフォーマンスを無効にする必要があります。[...]

理論的には、浅いコピーの最適な実装は C++ コピー コンストラクターです。コンパイル時にサイズを認識し、すべてのフィールドのメンバーごとのクローンを作成します。次善の策は、memcpyまたは同様のものを使用することです。これは基本的にどのように機能するかMemberwiseCloneです。これは、理論的には、パフォーマンスに関して他のすべての可能性を排除する必要があることを意味します。右?

...しかし、どうやらそれは非常に高速ではなく、他のすべてのソリューションを消し去るわけでもありません。一番下に、実際に 2 倍以上高速なソリューションを投稿しました。だから:間違っています。

MemberwiseClone の内部のテスト

単純な blittable 型を使用した小さなテストから始めて、パフォーマンスに関する基本的な仮定を確認してみましょう。

[StructLayout(LayoutKind.Sequential)]
public class ShallowCloneTest
{
    public int Foo;
    public long Bar;

    public ShallowCloneTest Clone()
    {
        return (ShallowCloneTest)base.MemberwiseClone();
    }
}

テストはblittable 型だからこそ可能なMemberwiseCloneagaist rawの性能を確認できるように工夫されています。memcpy

自分でテストするには、安全でないコードでコンパイルし、JIT 抑制を無効にし、リリース モードでコンパイルしてテストします。また、関連するすべての行の後にタイミングを付けました。

実装 1 :

ShallowCloneTest t1 = new ShallowCloneTest() { Bar = 1, Foo = 2 };
Stopwatch sw = Stopwatch.StartNew();
int total = 0;
for (int i = 0; i < 10000000; ++i)
{
    var cloned = t1.Clone();                                    // 0.40s
    total += cloned.Foo;
}

Console.WriteLine("Took {0:0.00}s", sw.Elapsed.TotalSeconds);

基本的に、これらのテストを何度も実行し、アセンブリの出力をチェックして、最適化されていないことを確認するなどしました。最終結果は、この 1 行のコードにかかるおおよその秒数、つまり 0.40 秒であることがわかります。私のPC。これは、 を使用したベースラインMemberwiseCloneです。

実装 2 :

sw = Stopwatch.StartNew();

total = 0;
uint bytes = (uint)Marshal.SizeOf(t1.GetType());
GCHandle handle1 = GCHandle.Alloc(t1, GCHandleType.Pinned);
IntPtr ptr1 = handle1.AddrOfPinnedObject();

for (int i = 0; i < 10000000; ++i)
{
    ShallowCloneTest t2 = new ShallowCloneTest();               // 0.03s
    GCHandle handle2 = GCHandle.Alloc(t2, GCHandleType.Pinned); // 0.75s (+ 'Free' call)
    IntPtr ptr2 = handle2.AddrOfPinnedObject();                 // 0.06s
    memcpy(ptr2, ptr1, new UIntPtr(bytes));                     // 0.17s
    handle2.Free();

    total += t2.Foo;
}

handle1.Free();
Console.WriteLine("Took {0:0.00}s", sw.Elapsed.TotalSeconds);

これらの数字をよく見ると、次のことがわかります。

  • オブジェクトの作成とコピーには約 0.20 秒かかります。通常の状況では、これが最速のコードです。
  • ただし、これを行うには、オブジェクトを固定および固定解除する必要があります。それには0.81秒かかります。

では、なぜこれらすべてが非常に遅いのでしょうか。

私の説明は、GCに関係しているということです。基本的に、実装は、完全な GC の前後でメモリが同じままであるという事実に依存することはできません (メモリのアドレスは、GC 中に変更される可能性があります。これは、シャロー コピー中を含め、いつでも発生する可能性があります)。これは、可能なオプションが 2 つしかないことを意味します。

  1. データを固定してコピーを実行します。これは、これを行う方法の 1 つにすぎないことに注意してくださいGCHandle.Alloc。C++/CLI などを使用するとパフォーマンスが向上することはよく知られています。
  2. フィールドの列挙。これにより、GC 収集間で特別なことを行う必要がなくなり、GC 収集中に GC 機能を使用して、移動したオブジェクトのスタック上のアドレスを変更できます。

MemberwiseClone方法 1 を使用します。これは、固定手順のためにパフォーマンスが低下することを意味します。

(はるかに) 高速な実装

いずれの場合も、アンマネージ コードは型のサイズを推測できないため、データを固定する必要があります。サイズに関する仮定を行うことで、コンパイラは、ループ展開、レジスタ割り当てなどの最適化をより適切に行うことができます (C++ コピー ctor が よりも高速であるようにmemcpy)。データを固定する必要がないということは、余分なパフォーマンス ヒットが発生しないことを意味します。.NET JIT はアセンブラー向けであるため、理論的には、単純な IL 発行を使用してより高速な実装を作成し、コンパイラーがそれを最適化できるようにする必要があることを意味します。

なぜこれがネイティブ実装よりも高速になるのかを要約すると、

  1. オブジェクトを固定する必要はありません。動き回るオブジェクトは GC によって処理されます。実際、これは徹底的に最適化されています。
  2. コピーする構造体のサイズを推測できるため、レジスタの割り当てやループの展開などをより適切に行うことができます。

私たちが目指しているのは、rawmemcpyまたはそれ以上のパフォーマンス: 0.17 秒です。

これを行うには、基本的に を使用してcall、オブジェクトを作成し、一連のcopy命令を実行することはできません。上記の実装に少し似ていますClonerが、重要な違いがいくつかあります (最も重要なのは、呼び出しがDictionaryなく、冗長なCreateDelegate呼び出しがないことです)。ここに行きます:

public static class Cloner<T>
{
    private static Func<T, T> cloner = CreateCloner();

    private static Func<T, T> CreateCloner()
    {
        var cloneMethod = new DynamicMethod("CloneImplementation", typeof(T), new Type[] { typeof(T) }, true);
        var defaultCtor = typeof(T).GetConstructor(new Type[] { });

        var generator = cloneMethod .GetILGenerator();

        var loc1 = generator.DeclareLocal(typeof(T));

        generator.Emit(OpCodes.Newobj, defaultCtor);
        generator.Emit(OpCodes.Stloc, loc1);

        foreach (var field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
            generator.Emit(OpCodes.Ldloc, loc1);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, field);
            generator.Emit(OpCodes.Stfld, field);
        }

        generator.Emit(OpCodes.Ldloc, loc1);
        generator.Emit(OpCodes.Ret);

        return ((Func<T, T>)cloneMethod.CreateDelegate(typeof(Func<T, T>)));
    }

    public static T Clone(T myObject)
    {
        return cloner(myObject);
    }
}

このコードをテストしたところ、結果は 0.16s でした。これは、 よりも約 2.5 倍高速であることを意味しMemberwiseCloneます。

さらに重要なことに、この速度は と同等でありmemcpy、多かれ少なかれ「通常の状況下での最適なソリューション」です。

個人的には、これが最速のソリューションだと思います。最も良い点は、.NET ランタイムが高速になる場合 (SSE 命令の適切なサポートなど)、このソリューションも高速になることです。

編集上の注意: 上記のサンプル コードは、既定のコンストラクターが public であることを前提としています。そうでない場合、呼び出しGetConstructorは null を返します。その場合は、他のGetConstructorシグネチャのいずれかを使用して、保護されたコンストラクターまたはプライベート コンストラクターを取得します。https://docs.microsoft.com/en-us/dotnet/api/system.type.getconstructor?view=netframework-4.8を参照してください。

于 2015-07-20T08:53:59.997 に答える
31

よくわかりません。浅いコピーの場合、他のもののパフォーマンスを全滅させるMemberwiseClone()必要があります。CLI では、RCW 以外のタイプは、次のシーケンスでシャロー コピーできる必要があります。

  • タイプのナーサリにメモリを割り当てます。
  • memcpy元のデータから新しいデータへ。ターゲットはナーサリにあるため、書き込みバリアは必要ありません。
  • オブジェクトにユーザー定義のファイナライザーがある場合は、ファイナライズ保留中のアイテムの GC リストに追加します。
    • ソース オブジェクトがSuppressFinalizeそれを呼び出し、そのようなフラグがオブジェクト ヘッダーに格納されている場合は、クローンで設定を解除します。

CLR 内部チームの誰かが、なぜそうでないのか説明できますか?

于 2009-08-20T21:34:49.757 に答える
17

なぜ物事を複雑にするのですか?MemberwiseClone で十分です。

public class ClassA : ICloneable
{
   public object Clone()
   {
      return this.MemberwiseClone();
   }
}

// let's say you want to copy the value (not reference) of the member of that class.
public class Main()
{
    ClassA myClassB = new ClassA();
    ClassA myClassC = new ClassA();
    myClassB = (ClassA) myClassC.Clone();
}
于 2013-03-08T12:26:37.820 に答える
8

これは、動的 IL 生成を使用して行う方法です。私はそれをオンラインのどこかで見つけました:

public static class Cloner
{
    static Dictionary<Type, Delegate> _cachedIL = new Dictionary<Type, Delegate>();

    public static T Clone<T>(T myObject)
    {
        Delegate myExec = null;

        if (!_cachedIL.TryGetValue(typeof(T), out myExec))
        {
            var dymMethod = new DynamicMethod("DoClone", typeof(T), new Type[] { typeof(T) }, true);
            var cInfo = myObject.GetType().GetConstructor(new Type[] { });

            var generator = dymMethod.GetILGenerator();

            var lbf = generator.DeclareLocal(typeof(T));

            generator.Emit(OpCodes.Newobj, cInfo);
            generator.Emit(OpCodes.Stloc_0);

            foreach (var field in myObject.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                // Load the new object on the eval stack... (currently 1 item on eval stack)
                generator.Emit(OpCodes.Ldloc_0);
                // Load initial object (parameter)          (currently 2 items on eval stack)
                generator.Emit(OpCodes.Ldarg_0);
                // Replace value by field value             (still currently 2 items on eval stack)
                generator.Emit(OpCodes.Ldfld, field);
                // Store the value of the top on the eval stack into the object underneath that value on the value stack.
                //  (0 items on eval stack)
                generator.Emit(OpCodes.Stfld, field);
            }

            // Load new constructed obj on eval stack -> 1 item on stack
            generator.Emit(OpCodes.Ldloc_0);
            // Return constructed object.   --> 0 items on stack
            generator.Emit(OpCodes.Ret);

            myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));

            _cachedIL.Add(typeof(T), myExec);
        }

        return ((Func<T, T>)myExec)(myObject);
    }
}
于 2009-06-08T19:16:53.777 に答える
4

MemberwiseClone は、メンテナンスの必要が少なくなります。デフォルトのプロパティ値を持つことが役立つかどうかはわかりませんが、デフォルト値を持つアイテムを無視できるかどうかはわかりません。

于 2009-06-08T19:24:44.293 に答える