對於深拷貝,通常的方法是將對象進行序列化,然后再反序化成為另一個對象。例如在stackoverflow上有這樣的解決辦法:https://stackoverflow.com/questions/78536/deep-cloning-objects/78612#78612。這種序列化的方式,對深拷貝來講,無疑是一個性能殺手。
今天大家介紹一個深拷貝的框架 DeepCopy,github地址:https://github.com/ReubenBond/DeepCopy,它是從orleans框架改編過來的,實現邏輯非常簡單。
框架的實現原理是通過IL代碼生成字段拷貝的方法。IL的優點是可以繞過C#的語法規則,例如:訪問私有對象以及給readonly
字段賦值等。
在介紹框架前,先介紹一下IL相關的工具。
IL工具
即使您不是第一次使用IL,這也不是一件容易的事情,無法確認什么樣IL代碼才能達到預期的結果。這是工具來幫助您的地方。可以先用C#編寫代碼,然后將它復制到LINQPad中,運行並打開輸出中的IL選項卡。
使用像JetBrains的dotPeek這樣的反編譯/反匯編程序也是一個不錯選擇。您可以將編譯的程序集在dotPeek中打開它來顯示IL。
最后,ReSharper是不可或缺的工具。ReSharper帶有一個方便的IL查看器。
這些工具可以幫助您如何解決IL產生的問題,您也可以訪問官方文檔。
DeepCopy
DeepCopy本質上它只提供了一個方法:
public static T Copy<T>(T original);
DeepCopy調用示例代碼:
List<string> original = new List<string>(2);
original.Add("A");
original.Add("B");
var result = DeepCopier.Copy(original);
實現原理
Copy
方法將遞歸傳遞對象中的每個字段復制到相同類型的新實例中。首先要處理的是對同一個對象的多次引用,如果用戶提供了一個包含自身引用的對象,那么結果也會包含對自身的引用。這意味着我們需要執行引用跟蹤。這點很容易做到:我們維護一個Dictionary<object, object>
從原始對象到拷貝對象的映射。我們的主要方法Copy<T>(T orig)
將調用上下文的方法來檢查字典中拷貝的對象是否存在:
public static T Copy<T>(T original, CopyContext context)
{
/* TODO: implementation */
}
拷貝流程大致如下:
- 如果傳入是
null
,則返回null
; - 如果傳入的對象已經拷貝過,則返回其拷貝過的對象;
- 如果傳入是“不可變的對象”,則直接返回傳入對象;
- 如果傳入是一個數組,則將每個元素復制到一個新數組中並將其返回;
- 創建一個新的傳入類型實例,遞歸地將每個字段從傳入對象復制到拷貝對象並返回。
對“不可變對象”的定義很簡單:類型是一個基原類型、Enum
、String
、Guid
、DateTime
...,或者使用特殊[Immutable]
標記的類型。更詳細的不可變類型可以參考源代碼,CopyPolicy.cs。
除了上面的最后一步,其它的事情都很簡單。最后一步,遞歸復制每個字段,可以使用反射來獲取和設置字段值。反射是一個性能殺手,所以使用IL來實現這一步。
IL代碼實現
DeepCopy
中的主要IL代碼在CopierGenerator.cs類的CreateCopier<T>(Type type)
方法中。讓我們一步步揭秘:
首先創建一個DynamicMethod
對象,它將保存創建的IL代碼。在創建DynamicMethod
對象時,必須告訴它簽名是什么,在這里,它是一個通用的委托類型delegate T DeepCopyDelegate<T>(T original, CopyContext context)
。
var dynamicMethod = new DynamicMethod(
type.Name + "DeepCopier",
typeof(T), // 委托返回的類型
new[] {typeof(T), typeof(CopyContext)}, // 委托的參數類型。
typeof(CopierGenerator).Module,
true);
var il = dynamicMethod.GetILGenerator();
IL將會變得相當復雜,因為它需要處理不可變的類型和值類型,接下來讓我一點一點地說明。
// 定義一個變量來保存返回的結果。
il.DeclareLocal(type);
接下來,需要初始化傳入類型的新實例到局部變量。有三種情況需要考慮,每種情況對應下面代碼中的一個塊:
- 該類型是一個值類型(結構)。使用
default(T)
表達式來初始化它。 - 該類型有一個無參數的構造函數。通過調用
new T()
初始化它。 - 該類型沒有無參數的構造函數。在這種情況下,我們借助 .Net 框架來解決,調用
FormatterServices.GetUninitializedObject(type)
。
// 構造結果對象實例。
var constructorInfo = type.GetConstructor(Type.EmptyTypes);
if (type.IsValueType)
{
// 值類型可以直接初始化。
// C#: result = default(T);
il.Emit(OpCodes.Ldloca_S, (byte)0);
il.Emit(OpCodes.Initobj, type);
}
else if (constructorInfo != null)
{
// 如果存在默認構造函數,則直接使用默認的參數。
// C#: result = new T();
il.Emit(OpCodes.Newobj, constructorInfo);
il.Emit(OpCodes.Stloc_0);
}
else
{
// 如果沒有默認構造函數的存在,使用GetUninitializedObject創建實例。
// C#: result = (T)FormatterServices.GetUninitializedObject(type);
il.Emit(OpCodes.Ldtoken, type);
il.Emit(OpCodes.Call, DeepCopier.MethodInfos.GetTypeFromHandle);
il.Emit(OpCodes.Call, this.methodInfos.GetUninitializedObject);
il.Emit(OpCodes.Castclass, type);
il.Emit(OpCodes.Stloc_0);
}
在本地創建一個用於保存結果的變量,它是傳入類型的新實例。在我們做任何事情之前,我們必須記錄新創建對象的引用。將每個參數按順序推入堆棧,並使用OpCodes.Call
來調用context.RecordObject(original, result)
。使用OpCodes.Call
來調用CopyContext.RecordObject
方法,因為CopyContext
是一個sealed
類,否則會使用OpCodes.Callvirt
。
// 值類型的實例不會存在多次引用的問題,
// 所以只在上下文中記錄引用類型。
if (!type.IsValueType)
{
// 記錄對象引用。
// C#: context.RecordObject(original, result);
il.Emit(OpCodes.Ldarg_1); // 參數:context
il.Emit(OpCodes.Ldarg_0); // 參數數:original
il.Emit(OpCodes.Ldloc_0); // 本地用來保存結果的變量
il.Emit(OpCodes.Call, this.methodInfos.RecordObject);
}
枚舉對象上的每一個字段並生成代碼,將字段的值復制到結果變量中。過程如下:
// 復制每一個字段的值。
foreach (var field in this.copyPolicy.GetCopyableFields(type))
{
// 加載結果對象的引用。
if (type.IsValueType)
{
// 值類型需要通過地址來加載,而不是復制到堆棧上。
il.Emit(OpCodes.Ldloca_S, (byte)0);
}
else
{
il.Emit(OpCodes.Ldloc_0);
}
// 加載原始對象字段的值。
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, field);
// 如果是不可變類型則直接賦值,否則需要深拷貝字段。
if (!this.copyPolicy.IsShallowCopyable(field.FieldType))
{
// 復制字段使用泛型方法 DeepCopy.Copy<T>(T original, CopyContext context)
// C#: Copy<T>(field)
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Call, this.methodInfos.CopyInner.MakeGenericMethod(field.FieldType));
}
// 將復制的值賦給結果對象的字段。
il.Emit(OpCodes.Stfld, field);
}
返回結果並通過CreateDelegate
構建委托,下一步可以直接使用。
// C#: return result;
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ret);
return dynamicMethod.CreateDelegate(typeof(DeepCopyDelegate<T>)) as DeepCopyDelegate<T>;
性能
框架性能怎么樣呢,分別比較通過手寫代碼、DeepCopy、二進制序列化和Json.Net序列化來實現對象的深拷貝,然后通過 Benchmark測試一下它們之間的性能。
實現方式 | Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|---|
手寫代碼 | CodeCopy | 7.874 ns | 0.0941 ns | 0.0880 ns | 0.0203 | 64 B |
DeepCopy | DeepCopy | 114.510 ns | 0.4071 ns | 0.3608 ns | 0.0203 | 64 B |
二進制序列化 | BinarySerialize | 46,912.139 ns | 156.4497 ns | 138.6886 ns | 3.4180 | 10827 B |
Json.Net序列化 | JsonSerialize | 8,942.457 ns | 97.0560 ns | 90.7862 ns | 1.6479 | 5208 B |
雖然DeepCopy與手寫代碼來說性能相差很大(差異很大的根本原因是反復查找是否存在自引用),但是與二進制序列化、Json序列化來說,性能不在一個等級上。
總結
這是框架的內部邏輯,當然還有一些細節被遺漏了,例如:數組中的特殊處理DeepCopier.cs;
當然還有很多需要優化的細節,大家可以在github上提出您的寶貴意見。
參考內容:
作者:Sweet Tang
本文地址:http://www.cnblogs.com/tdfblog/p/DeepCopy-By-IL.html
歡迎轉載,請在明顯位置給出出處及鏈接。