C# で浅いコピーを行う最速の方法は何だろうか? 浅いコピーを行うには2つの方法があることだけを知っています:
- MemberwiseClone
- 各フィールドを 1 つずつコピーする (手動)
(2)は(1)よりも速いことがわかりました。浅いコピーを行う別の方法があるかどうか疑問に思っていますか?
C# で浅いコピーを行う最速の方法は何だろうか? 浅いコピーを行うには2つの方法があることだけを知っています:
(2)は(1)よりも速いことがわかりました。浅いコピーを行う別の方法があるかどうか疑問に思っていますか?
これは、考えられる解決策が多数あり、それぞれに多くの長所と短所がある複雑な問題です。C# でコピーを作成するさまざまな方法を概説している素晴らしい記事がここにあります。要約する:
手動でクローン
を作成する 面倒ですが、高度な制御が可能です。
MemberwiseClone
のみでクローンを作成すると、浅いコピーが作成されます。つまり、参照型フィールドの場合、元のオブジェクトとそのクローンは同じオブジェクトを参照します。
Reflection を使用したクローン
デフォルトでは浅いコピーが行われ、ディープ コピーを実行するように書き換えることができます。利点: 自動化。短所:反射が遅い。
シリアル化による複製
簡単、自動化。一部の制御を放棄すると、シリアル化が最も遅くなります。
IL を使用してクローンを作成、拡張メソッドを使用してクローンを作成、
より高度なソリューションであり、一般的ではありません。
いくつかの引用から始めたいと思います:
実際、特に複合型の場合、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 型だからこそ可能なMemberwiseClone
agaist 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);
これらの数字をよく見ると、次のことがわかります。
では、なぜこれらすべてが非常に遅いのでしょうか。
私の説明は、GCに関係しているということです。基本的に、実装は、完全な GC の前後でメモリが同じままであるという事実に依存することはできません (メモリのアドレスは、GC 中に変更される可能性があります。これは、シャロー コピー中を含め、いつでも発生する可能性があります)。これは、可能なオプションが 2 つしかないことを意味します。
GCHandle.Alloc
。C++/CLI などを使用するとパフォーマンスが向上することはよく知られています。MemberwiseClone
方法 1 を使用します。これは、固定手順のためにパフォーマンスが低下することを意味します。
(はるかに) 高速な実装
いずれの場合も、アンマネージ コードは型のサイズを推測できないため、データを固定する必要があります。サイズに関する仮定を行うことで、コンパイラは、ループ展開、レジスタ割り当てなどの最適化をより適切に行うことができます (C++ コピー ctor が よりも高速であるようにmemcpy
)。データを固定する必要がないということは、余分なパフォーマンス ヒットが発生しないことを意味します。.NET JIT はアセンブラー向けであるため、理論的には、単純な IL 発行を使用してより高速な実装を作成し、コンパイラーがそれを最適化できるようにする必要があることを意味します。
なぜこれがネイティブ実装よりも高速になるのかを要約すると、
私たちが目指しているのは、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を参照してください。
よくわかりません。浅いコピーの場合、他のもののパフォーマンスを全滅させるMemberwiseClone()
必要があります。CLI では、RCW 以外のタイプは、次のシーケンスでシャロー コピーできる必要があります。
memcpy
元のデータから新しいデータへ。ターゲットはナーサリにあるため、書き込みバリアは必要ありません。SuppressFinalize
それを呼び出し、そのようなフラグがオブジェクト ヘッダーに格納されている場合は、クローンで設定を解除します。CLR 内部チームの誰かが、なぜそうでないのか説明できますか?
なぜ物事を複雑にするのですか?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();
}
これは、動的 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);
}
}
MemberwiseClone は、メンテナンスの必要が少なくなります。デフォルトのプロパティ値を持つことが役立つかどうかはわかりませんが、デフォルト値を持つアイテムを無視できるかどうかはわかりません。