iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 9
2
Software Development

🌊 進階學習 ADO.NET、Dapper、Entity Framework 系列 第 9

【深入Dapper.NET源碼】Strongly Typed Mapping 原理 Part6 : Emit版本

以下代碼是Emit版本,我把C#對應IL部分都寫在註解。

public static class DemoExtension
{
	public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql) where T : new()
	{
		using (var command = cnn.CreateCommand())
		{
			command.CommandText = sql;
			using (var reader = command.ExecuteReader())
			{
				var func = GetTypeDeserializerImpl(typeof(T), reader);

				while (reader.Read())
				{
					var result = func(reader as DbDataReader);
					yield return result is T ? (T)result : default(T);
				}
			}

		}
	}

	private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
	{
		var returnType = type.IsValueType ? typeof(object) : type;

		var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(IDataReader) }, type, true);
		var il = dm.GetILGenerator();

		//C# : User user = new User();
		//IL : 
		//IL_0001:  newobj      
		//IL_0006:  stloc.0         
		var constructor = returnType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0]; //這邊簡化成只會有預設constructor
		il.Emit(OpCodes.Newobj, constructor);
		var returnValueLocal = il.DeclareLocal(type);
		il.Emit(OpCodes.Stloc, returnValueLocal); //User user = new User();

		// C# : 
		//object value = default(object);
		// IL :
		//IL_0007: ldnull
		//IL_0008:  stloc.1     // value	
		var valueLoacl = il.DeclareLocal(typeof(object));
		il.Emit(OpCodes.Ldnull);
		il.Emit(OpCodes.Stloc, valueLoacl);
		
		
		int index = startBound;
		var getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public)
						.Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int))
						.Select(p => p.GetGetMethod()).First();

		foreach (var p in type.GetProperties())
		{
			//C# : value = P_0[0];
			//IL:
			//IL_0009:  ldarg.0			
			//IL_000A: ldc.i4.0
			//IL_000B: callvirt System.Data.IDataRecord.get_Item
			//IL_0010:  stloc.1     // value				
			il.Emit(OpCodes.Ldarg_0); //取得reader參數
			EmitInt32(il, index);
			il.Emit(OpCodes.Callvirt, getItem);
			il.Emit(OpCodes.Stloc, valueLoacl);


			//C#: if (!(value is DBNull)) user.Name = (string)value;
			//IL:
			// IL_0011:  ldloc.1     // value
			// IL_0012:  isinst      System.DBNull
			// IL_0017:  ldnull      
			// IL_0018:  cgt.un      
			// IL_001A:  ldc.i4.0   
			// IL_001B:  ceq         
			// IL_001D:  stloc.2    
			// IL_001E:  ldloc.2     
			// IL_001F:  brfalse.s   IL_002E
			// IL_0021:  ldloc.0     // user
			// IL_0022:  ldloc.1     // value
			// IL_0023:  castclass   System.String
			// IL_0028:  callvirt    UserQuery+User.set_Name			
			il.Emit(OpCodes.Ldloc, valueLoacl);
			il.Emit(OpCodes.Isinst, typeof(System.DBNull));
			il.Emit(OpCodes.Ldnull);
			
			var tmpLoacl = il.DeclareLocal(typeof(int));
			il.Emit(OpCodes.Cgt_Un);
			il.Emit(OpCodes.Ldc_I4_0);
			il.Emit(OpCodes.Ceq);
			
			il.Emit(OpCodes.Stloc,tmpLoacl);
			il.Emit(OpCodes.Ldloc,tmpLoacl);
			
			
			var labelFalse = il.DefineLabel();
			il.Emit(OpCodes.Brfalse_S,labelFalse);
			il.Emit(OpCodes.Ldloc, returnValueLocal);
			il.Emit(OpCodes.Ldloc, valueLoacl);
			if (p.PropertyType.IsValueType)
				il.Emit(OpCodes.Unbox_Any, p.PropertyType);
			else
				il.Emit(OpCodes.Castclass, p.PropertyType);
			il.Emit(OpCodes.Callvirt, p.SetMethod);
			
			il.MarkLabel(labelFalse);

			index++;
		}

		// IL_0053:  ldloc.0     // user
		// IL_0054:  stloc.s     04  //不需要
		// IL_0056:  br.s        IL_0058
		// IL_0058:  ldloc.s     04  //不需要
		// IL_005A:  ret         
		il.Emit(OpCodes.Ldloc, returnValueLocal);
		il.Emit(OpCodes.Ret);

		var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(IDataReader), returnType);
		return (Func<IDataReader, object>)dm.CreateDelegate(funcType);
	}

	private static void EmitInt32(ILGenerator il, int value)
	{
		switch (value)
		{
			case -1: il.Emit(OpCodes.Ldc_I4_M1); break;
			case 0: il.Emit(OpCodes.Ldc_I4_0); break;
			case 1: il.Emit(OpCodes.Ldc_I4_1); break;
			case 2: il.Emit(OpCodes.Ldc_I4_2); break;
			case 3: il.Emit(OpCodes.Ldc_I4_3); break;
			case 4: il.Emit(OpCodes.Ldc_I4_4); break;
			case 5: il.Emit(OpCodes.Ldc_I4_5); break;
			case 6: il.Emit(OpCodes.Ldc_I4_6); break;
			case 7: il.Emit(OpCodes.Ldc_I4_7); break;
			case 8: il.Emit(OpCodes.Ldc_I4_8); break;
			default:
				if (value >= -128 && value <= 127)
				{
					il.Emit(OpCodes.Ldc_I4_S, (sbyte)value);
				}
				else
				{
					il.Emit(OpCodes.Ldc_I4, value);
				}
				break;
		}
	}
}

這邊Emit的細節概念非常的多,這邊無法全部都講解,先挑出重要概念講解

Emit Label

在Emit if/else需要使用Label定位,告知編譯器條件為true/false時要跳到哪個位子,舉例 : 「boolean轉整數」,假設要簡單將Boolean轉換成Int,C#代碼可以用「如果是True返回1否則返回0」邏輯來寫:

	public static int BoolToInt(bool input) => input ? 1 : 0;

當轉成Emit寫法的時候,需要以下邏輯 :

  1. 考慮Label動態定位問題
  2. 先要建立好Label讓Brtrue_S知道符合條件時要去哪個Label位子 (注意,這時候Label位子還沒確定)
  3. 繼續按順序由上而下建立IL
  4. 等到了符合條件要運行區塊的前一行,使用MarkLabel方法標記Label的位子

最後寫出的C# Emit代碼 :

public class Program
{
	public static void Main(string[] args)
	{
		var func = CreateFunc();
		Console.WriteLine(func(true)); //1
		Console.WriteLine(func(false)); //0
	}

	static Func<bool, int> CreateFunc()
	{
		var dm = new DynamicMethod("Test" + Guid.NewGuid().ToString(), typeof(int), new[] { typeof(bool) });

		var il = dm.GetILGenerator();
		var labelTrue = il.DefineLabel();

		il.Emit(OpCodes.Ldarg_0);
		il.Emit(OpCodes.Brtrue_S, labelTrue);
		il.Emit(OpCodes.Ldc_I4_0);
		il.Emit(OpCodes.Ret);
		il.MarkLabel(labelTrue);
		il.Emit(OpCodes.Ldc_I4_1);
		il.Emit(OpCodes.Ret);

		var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(bool), typeof(int));
		return (Func<bool, int>)dm.CreateDelegate(funcType);
	}
}

這邊可以發現Emit版本
優點 :

  1. 能做更多細節的操作
  2. 因為細節顆粒度小,可以優化的效率更好

缺點 :

  1. 難以Debug
  2. 可讀性差
  3. 代碼量變大、複雜度增加

接著來看Dapper作者的建議,現在一般專案當中沒有必要使用Emit,使用Expression + Func/Action已經可以解決大部分動態方法的需求,尤其是Expression支援Block等方法情況。連結 c# - What's faster: expression trees or manually emitting IL

20190927163441.png

話雖如此,但有一些厲害的開源專案就是使用Emit管理細節,如果想看懂它們,就需要基礎的Emit IL概念


上一篇
【深入Dapper.NET源碼】Strongly Typed Mapping 原理 Part5 : Emit IL反建立C#代碼
下一篇
【深入Dapper.NET源碼】Dapper 效率快關鍵之一 : Cache 緩存原理
系列文
🌊 進階學習 ADO.NET、Dapper、Entity Framework 30

1 則留言

1
石頭
iT邦研究生 3 級 ‧ 2019-09-24 09:57:04

感謝 暐翰 大 分享這篇文章
對於想要了解Emit的人很有幫助

暐翰 iT邦大師 1 級‧ 2019-09-24 10:00:06 檢舉

謝謝 石頭 大大 /images/emoticon/emoticon58.gif

我要留言

立即登入留言