在寫類似Code Generator的功能,遇到一個抉擇點:

若要將DataRow的各欄位逐一映對到資料物件的各欄位上,該使用Reflection還是Hard-Coding?

使用Reflection方式(PropertyInfo)讀寫物件屬性可大幅簡化程式碼複雜度,但需付出效能上的代價。如先前文章所提,隨著CPU的運算能力愈來愈強大,效能代價的負面影響也會愈來愈小。只是在查詢資料過程中,DataRow轉換成資料物件的程序會被連續大量執行,恐怕又得另當別論。例如: 查詢取回10萬筆DataRow,接著就得進行10萬次DataRow轉物件的動作,若每個DataRow有30個欄位,PropertyInfo.SetValue就會被呼叫300萬次,這個量已讓效能差異不容忽視。

於是,我寫了一段簡單的Benchmark,由資料庫查詢取得約兩萬筆資料,每筆有24個欄位。在C#裡宣告有24個同名屬性的類別用以存放查詢一筆結果,分別使用Reflection PropertyInfo.SetValue及ObjectVariable.PropName = (T) row[“PropName”]的Hard-Coding法將DataRow的內容逐一轉成資料物件放入集合,並以Stopwatch計時,反覆跑10次進行測試。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.OracleClient;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
 
namespace ConsoleApplication1
{
    class Program
    {
        static string cs = "Data Source=BOO;User Id=foo;Password=blabubu";
 
        static void Main(string[] args)
        {
            using (var cn = new OracleConnection(cs))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = "SELECT * FROM MY_TABLE";
                DataTable t = new DataTable();
                t.Load(cmd.ExecuteReader());
                cn.Close();
                List<MyCustClass> lst;
                Console.WriteLine("Row Count: {0:N0}", t.Rows.Count);
 
                Stopwatch sw = new Stopwatch();
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("==== Test[{0}] ====", i);
 
                    lst = new List<MyCustClass>();
                    sw.Reset();
                    sw.Start();
                    foreach (DataRow row in t.Rows)
                        lst.Add(MyCustClass.RefConv(row));
                    sw.Stop();
                    Console.WriteLine(" * By Reflection: {0:N0}", sw.ElapsedMilliseconds);
 
                    lst = new List<MyCustClass>();
                    sw.Reset();
                    sw.Start();
                    foreach (DataRow row in t.Rows)
                        lst.Add(MyCustClass.ExplicitConv(row));
                    Console.WriteLine(" * By Hard-Coding: {0:N0}", sw.ElapsedMilliseconds);
                }
            }
            Console.Read();
        }
    }
 
    public class MyCustClass
    {
        public string C1 { get; set; }
        public DateTime C2 { get; set; }
        public string C3 { get; set; }
        public DateTime C4 { get; set; }
        public string C5 { get; set; }
        public string C6 { get; set; }
        public string C7 { get; set; }
        public string C8 { get; set; }
        public string C9 { get; set; }
        public string C10 { get; set; }
        public string C11 { get; set; }
        public DateTime C12 { get; set; }
        public string C13 { get; set; }
        public decimal C14 { get; set; }
        public decimal C15 { get; set; }
        public decimal C16 { get; set; }
        public decimal C17 { get; set; }
        public decimal C18 { get; set; }
        public decimal C19 { get; set; }
        public decimal C20 { get; set; }
        public decimal C21 { get; set; }
        public decimal C22 { get; set; }
        public string C23 { get; set; }
        public string C24 { get; set; }
 
        public static MyCustClass RefConv(DataRow row)
        {
            MyCustClass mcc = new MyCustClass();
            foreach (PropertyInfo pi in mcc.GetType().GetProperties())
                pi.SetValue(mcc, row[pi.Name], null);
            return mcc;
        }
 
        public static MyCustClass ExplicitConv(DataRow row)
        {
            MyCustClass mcc = new MyCustClass();
            mcc.C1 = (string)row["C1"];
            mcc.C2 = (DateTime)row["C2"];
            mcc.C3 = (string)row["C3"];
            mcc.C4 = (DateTime)row["C4"];
            mcc.C5 = (string)row["C5"];
            //...省略...
            mcc.C22 = (decimal)row["C22"];
            mcc.C23 = (string)row["C23"];
            mcc.C24 = (string)row["C24"];
            return mcc;
        }
    }
}

程式結果出爐,直接寫死mcc.C1 = (string)row[“C1”]比起C1 PropertyInfo.SetValue()快了約10倍!!

Row Count: 19,429
==== Test[0] ====
 * By Reflection: 721
 * By Hard-Coding: 60
==== Test[1] ====
 * By Reflection: 788
 * By Hard-Coding: 60
==== Test[2] ====
 * By Reflection: 637
 * By Hard-Coding: 69
==== Test[3] ====
 * By Reflection: 1,077
 * By Hard-Coding: 99
==== Test[4] ====
 * By Reflection: 1,009
 * By Hard-Coding: 75
==== Test[5] ====
 * By Reflection: 652
 * By Hard-Coding: 53
==== Test[6] ====
 * By Reflection: 622
 * By Hard-Coding: 51
==== Test[7] ====
 * By Reflection: 639
 * By Hard-Coding: 47
==== Test[8] ====
 * By Reflection: 667
 * By Hard-Coding: 63
==== Test[9] ====
 * By Reflection: 846
 * By Hard-Coding: 73

最後的結論是,既然透過Code Generator預先產生好資料值設定的程式碼並不會增加太多成本,卻有高達10倍的效能提升,這個測試算是給了肯定的決策佐證。未來在玩自動產生程式碼把戲時,還是別偷雞,乖乖地逐一展開各屬性的映對程式碼爭取效能才是王道。

[2011-07-07更新] Jerry補充了Expression Tree做法的測試,laneser則實做了Delegate.CreateDelegate(見留言),大家一起完成了PropertyInfo、ExpressionTree、Delegate.CreateDelegate測試大三元,依結果來看,排除Hardcoding,重複執行效率由Delegate.CreateDelegate勝出。(laneser提供的另一篇文章也有相似結論)

[2011-07-07更新] iceboundrock再補充了一個很酷的Open Source Library(http://fastreflectionlib.codeplex.com/),直接擴充了PropertyInfo及MethodInfo,加上高速版的FastGetValue() Method,很棒的點子,也非常直覺易用,謝謝iceboundrock的分享。


Comments

# by laneser

public class MyCustClass { public string C1 { get; set; } public DateTime C2 { get; set; } public string C3 { get; set; } public DateTime C4 { get; set; } public string C5 { get; set; } public string C6 { get; set; } public string C7 { get; set; } public string C8 { get; set; } public string C9 { get; set; } public string C10 { get; set; } public string C11 { get; set; } public DateTime C12 { get; set; } public string C13 { get; set; } public decimal C14 { get; set; } public decimal C15 { get; set; } public decimal C16 { get; set; } public decimal C17 { get; set; } public decimal C18 { get; set; } public decimal C19 { get; set; } public decimal C20 { get; set; } public decimal C21 { get; set; } public decimal C22 { get; set; } public string C23 { get; set; } public string C24 { get; set; } private static Dictionary<string, Action<MyCustClass,object>> SetMethods = createSetMethods(); static Action<T, object> CreateSetMethodHelper<T, TParam>(MethodInfo method) { var act = (Action<T, TParam>)Delegate.CreateDelegate(typeof(Action<T, TParam>), method); Action<T, object> ret = (T target, object param) => act(target, (TParam)param); return ret; } private static Dictionary<string, Action<MyCustClass,object>> createSetMethods() { var properties = typeof(MyCustClass).GetProperties(); var r = new Dictionary<string, Action<MyCustClass, object>>(properties.Length); foreach (var p in properties) { var method = p.GetSetMethod(); var genericHelper = typeof(MyCustClass).GetMethod("CreateSetMethodHelper", BindingFlags.Static | BindingFlags.NonPublic); var constructedHelper = genericHelper.MakeGenericMethod(typeof(MyCustClass), method.GetParameters()[0].ParameterType); r[p.Name] = (Action<MyCustClass, object>)constructedHelper.Invoke(null, new object[] {method}); } return r; } public static MyCustClass RefConv(DataRow row) { MyCustClass mcc = new MyCustClass(); foreach (var setfuncpair in SetMethods) setfuncpair.Value(mcc, row[setfuncpair.Key]); return mcc; } public static MyCustClass ExplicitConv(DataRow row) { MyCustClass mcc = new MyCustClass(); mcc.C1 = (string)row["C1"]; mcc.C2 = (DateTime)row["C2"]; mcc.C3 = (string)row["C3"]; mcc.C4 = (DateTime)row["C4"]; mcc.C5 = (string)row["C5"]; mcc.C6 = (string)row["C6"]; mcc.C7 = (string)row["C7"]; mcc.C8 = (string)row["C8"]; mcc.C9 = (string)row["C9"]; mcc.C10 = (string)row["C10"]; mcc.C11 = (string)row["C11"]; mcc.C12 = (DateTime)row["C12"]; mcc.C13 = (string)row["C13"]; mcc.C14 = (decimal)row["C14"]; mcc.C15 = (decimal)row["C15"]; mcc.C16 = (decimal)row["C16"]; mcc.C17 = (decimal)row["C17"]; mcc.C18 = (decimal)row["C18"]; mcc.C19 = (decimal)row["C19"]; mcc.C20 = (decimal)row["C20"]; mcc.C21 = (decimal)row["C21"]; mcc.C22 = (decimal)row["C22"]; mcc.C23 = (string)row["C23"]; mcc.C24 = (string)row["C24"]; return mcc; } } ---- 這樣速度快了不少, ==== Test[0] ==== * By Reflection: 111 * By Hard-Coding: 56 ==== Test[1] ==== * By Reflection: 97 * By Hard-Coding: 64 ==== Test[2] ==== * By Reflection: 104 * By Hard-Coding: 81 ==== Test[3] ==== * By Reflection: 123 * By Hard-Coding: 55 ==== Test[4] ==== * By Reflection: 87 * By Hard-Coding: 57 ==== Test[5] ==== * By Reflection: 103 * By Hard-Coding: 53 ==== Test[6] ==== * By Reflection: 116 * By Hard-Coding: 42 ==== Test[7] ==== * By Reflection: 102 * By Hard-Coding: 61 ==== Test[8] ==== * By Reflection: 108 * By Hard-Coding: 44 ==== Test[9] ==== * By Reflection: 108 * By Hard-Coding: 53 可以參考 http://msmvps.com/blogs/jon_skeet/archive/2008/08/09/making-reflection-fly-and-exploring-delegates.aspx

# by Jeffrey

to laneser, 謝謝分享Delegate.CreateDelegate的做法,再加上Jerry的測試(http://www.dotblogs.com.tw/lastsecret/archive/2011/07/07/31236.aspx) 大家合作完成PropertyInfo + ExpressionTree + Delegate.CreateDelegate 的測試三大元了。看來也跟您提的文章結論相同(http://www.cnblogs.com/artech/archive/2011/03/26/Propertyaccesstest.html) ,三者中CreateDelegate 效能最佳。 又一篇討論與迴響比本文精彩的文章,很高興我是抛磚頭的人!

# by iceboundrock

hi, you may want to check out this project: http://fastreflectionlib.codeplex.com/

# by Jeffrey

to iceboundrock, Awesome! 感謝分享~~

Post a comment