web-dev-qa-db-ja.com

C#で浅いコピーを行う最も速い方法

C#で浅いコピーを行う最も速い方法は何ですか?浅いコピーを行う方法は2つしかありません。

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

(2)は(1)より速いことがわかりました。浅いコピーを行う別の方法があるのだろうか?

54
tep

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

  1. 手動で複製する
    面倒ですが、高度な制御。

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

  3. リフレクションでクローン
    デフォルトでは浅いコピーですが、ディープコピーを行うために書き換えることができます。利点:自動化されています。欠点:反射が遅い。

  4. シリアル化でクローン
    簡単、自動化。いくらかの制御をあきらめ、シリアル化が最も遅くなります。

  5. ILを使用したクローン、拡張メソッドを使用したクローン
    より一般的な方法ではなく、より高度なソリューション。

72
Nick Stamas

よくわかりません。 MemberwiseClone() should 全滅浅いコピーに対するその他のパフォーマンス。 CLIでは、RCW以外のタイプは、次のシーケンスによって浅くコピーできる必要があります。

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

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

28
Sam Harwell

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

実際、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();
    }
}

このテストは、MemberwiseClone agaist raw memcpyのパフォーマンスをチェックできるように考案されています。これはblittable型なので可能です。

自分でテストするには、安全でないコードでコンパイルし、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.4秒であることがわかったことです。私の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. データを固定してコピーを実行します。ご了承ください GCHandle.Allocはこれを行う方法の1つにすぎません。C++/CLIなどのパフォーマンスが向上することはよく知られています。
  2. フィールドを列挙します。これにより、GC収集の間に特別なことをする必要がなくなり、GC収集中にGC機能を使用して、移動したオブジェクトのスタックのアドレスを変更できます。

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

(はるかに)高速な実装

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

では、なぜこれがネイティブ実装よりも高速になるのかをまとめると?

  1. オブジェクトを固定する必要はありません。動き回るオブジェクトはGCによって処理されます。実際、これは容赦なく最適化されています。
  2. コピーする構造のサイズについての仮定を立てることができるため、レジスタの割り当て、ループの展開などが改善されます。

私たちが目指しているのは、未加工のmemcpy以上のパフォーマンス: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.16秒になりました。これは、MemberwiseCloneよりも約2.5倍高速であることを意味します。

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

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

27
atlaste

なぜ物事が複雑になるのですか? 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();
}
14
jun estevez

これは、動的な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);
    }
}
8
eulerfx

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

理由は、コピーを手動で作成する場合、型のコンストラクターの1つを呼び出す必要がありますが、メンバー単位のクローンを使用する必要があるため、メモリブロックをコピーするだけだと思います。これらのタイプには非常に高価な構成アクションがあるため、メンバーごとのクローンが絶対に最良の方法です。

私はそのようなタイプを書きました:{string A = Guid.NewGuid()。ToString()}、私はmemberwiseクローンが新しいインスタンスを作成し、メンバーを手動で割り当てるよりも速いことを発見しました。

以下のコードの結果:

手動コピー:00:00:00.0017099

MemberwiseClone:00:00:00.0009911

namespace MoeCard.TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Program p = new Program() { AAA = Guid.NewGuid().ToString(), BBB = 123 };
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < 10000; i++)
            {
                p.Copy1();
            }
            sw.Stop();
            Console.WriteLine("Manual Copy:" + sw.Elapsed);

            sw.Restart();
            for (int i = 0; i < 10000; i++)
            {
                p.Copy2();
            }
            sw.Stop();
            Console.WriteLine("MemberwiseClone:" + sw.Elapsed);
            Console.ReadLine();
        }

        public string AAA;

        public int BBB;

        public Class1 CCC = new Class1();

        public Program Copy1()
        {
            return new Program() { AAA = AAA, BBB = BBB, CCC = CCC };
        }
        public Program Copy2()
        {
            return this.MemberwiseClone() as Program;
        }

        public class Class1
        {
            public DateTime Date = DateTime.Now;
        }
    }

}

最後に、ここでコードを提供します。

    #region 数据克隆
    /// <summary>
    /// 依据不同类型所存储的克隆句柄集合
    /// </summary>
    private static readonly Dictionary<Type, Func<object, object>> CloneHandlers = new Dictionary<Type, Func<object, object>>();

    /// <summary>
    /// 根据指定的实例,克隆一份新的实例
    /// </summary>
    /// <param name="source">待克隆的实例</param>
    /// <returns>被克隆的新的实例</returns>
    public static object CloneInstance(object source)
    {
        if (source == null)
        {
            return null;
        }
        Func<object, object> handler = TryGetOrAdd(CloneHandlers, source.GetType(), CreateCloneHandler);
        return handler(source);
    }

    /// <summary>
    /// 根据指定的类型,创建对应的克隆句柄
    /// </summary>
    /// <param name="type">数据类型</param>
    /// <returns>数据克隆句柄</returns>
    private static Func<object, object> CreateCloneHandler(Type type)
    {
        return Delegate.CreateDelegate(typeof(Func<object, object>), new Func<object, object>(CloneAs<object>).Method.GetGenericMethodDefinition().MakeGenericMethod(type)) as Func<object, object>;
    }

    /// <summary>
    /// 克隆一个类
    /// </summary>
    /// <typeparam name="TValue"></typeparam>
    /// <param name="value"></param>
    /// <returns></returns>
    private static object CloneAs<TValue>(object value)
    {
        return Copier<TValue>.Clone((TValue)value);
    }
    /// <summary>
    /// 生成一份指定数据的克隆体
    /// </summary>
    /// <typeparam name="TValue">数据的类型</typeparam>
    /// <param name="value">需要克隆的值</param>
    /// <returns>克隆后的数据</returns>
    public static TValue Clone<TValue>(TValue value)
    {
        if (value == null)
        {
            return value;
        }
        return Copier<TValue>.Clone(value);
    }

    /// <summary>
    /// 辅助类,完成数据克隆
    /// </summary>
    /// <typeparam name="TValue">数据类型</typeparam>
    private static class Copier<TValue>
    {
        /// <summary>
        /// 用于克隆的句柄
        /// </summary>
        internal static readonly Func<TValue, TValue> Clone;

        /// <summary>
        /// 初始化
        /// </summary>
        static Copier()
        {
            MethodFactory<Func<TValue, TValue>> method = MethodFactory.Create<Func<TValue, TValue>>();
            Type type = typeof(TValue);
            if (type == typeof(object))
            {
                method.LoadArg(0).Return();
                return;
            }
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Object:
                    if (type.IsClass)
                    {
                        method.LoadArg(0).Call(Reflector.GetMethod(typeof(object), "MemberwiseClone")).Cast(typeof(object), typeof(TValue)).Return();
                    }
                    else
                    {
                        method.LoadArg(0).Return();
                    }
                    break;
                default:
                    method.LoadArg(0).Return();
                    break;
            }
            Clone = method.Delegation;
        }

    }
    #endregion
5
dexiang

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

4
Eric Schneider

これは、リフレクションを使用してMemberwiseCloneにアクセスし、必要以上にリフレクションを使用しないようにデリゲートをキャッシュする小さなヘルパークラスです。

public static class CloneUtil<T>
{
    private static readonly Func<T, object> clone;

    static CloneUtil()
    {
        var cloneMethod = typeof(T).GetMethod("MemberwiseClone", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        clone = (Func<T, object>)cloneMethod.CreateDelegate(typeof(Func<T, object>));
    }

    public static T ShallowClone(T obj) => (T)clone(obj);
}

public static class CloneUtil
{
    public static T ShallowClone<T>(this T obj) => CloneUtil<T>.ShallowClone(obj);
}

次のように呼び出すことができます。

Person b = a.ShallowClone();
1
Tim Pohlmann