在前二篇博客中,我分別介紹了二種優化反射的方法:
1. Delegate:委托。
2. CodeDOM:動態代碼生成。
這是二種截然不同的方法,性能的差距也很大。
今天的博客將着重比較它們的優缺點,以及給出它們的使用建議。
用Delegate優化反射的缺點
在評價委托方案時,我認為有必要細分一下委托方案:
1. 強類型委托,例如:Action<TTarget, TValue>
2. 弱類型委托,例如:Action<object, object>
它們的優點分別是:
強類型委托:速度快,已經最接近直接調用的性能,然而它的缺點是 不通用。
弱類型委托:比較通用,且經過一些代碼封裝后,使用方便,但是 封裝后的性能會變差。
用Delegate優化反射的優點
優點有二個:
1. 實現簡單,不管是使用Emit, ExpressionTree還是CreateDelegate,代碼量都不大。
2. 方法通用,使用弱類型委托,我們可以封裝出很容易使用的API,且適用於任何項目。
用CodeDOM優化反射的優點
最大的,也是唯一的優點就是:性能好。
由於生成的是直接調用的代碼,因此最終運行的是直接調用的代碼,所以沒有性能損耗。
另外,代碼生成器可以決定最終生成的代碼質量,代碼生成器越優秀,代碼的性能也會更優秀。
注意:當使用這種技術時,不同人可能會有不同的使用方法,最終可以得到性能不同的結果, (理論上)最壞情況下可能比委托還差。
如果希望借助這種優化方式實現最好的性能,需要做好二件事情:
1. 保證最終生成的代碼質量是最優的。
2. 編譯方式的設計要合理(用好CodeDOM)。
如何保證最終生成的代碼質量是最優的,我給不了建議,需要您自己去思考,
我們接着討論第2點。
如何用好CodeDOM?
雖然采用動態編譯技術,我們可以生成直接調用的代碼來代替反射調用,這樣就不會有任何性能損失。
但是,還有一個問題也是需要考慮的:我該以什么粒度去生成代碼?
1. 是為每個反射調用生成代碼?
2. 還是為每個類型批量生成一段代碼?
3. 還是為一堆類型大批量的生成一批代碼?
由於動態編譯的結果並不能直接調用,我們只能借助委托或者接口的方式去調用,
所以如果每次代碼生成的粒度較小,將會產生大量的程序集,也會消耗較多的編譯器啟動時間,
因此,這並不是高效的做法。高效的做法應該是一次盡可能生成較多的代碼。
除此之外,還有一個問題也要考慮:當需要循環調用編譯結果時,該怎么辦?
對於這類場景,我建議在生成代碼時,把循環過程直接生成出來,最終只用一次調用編譯結果完成整個調用過程。
例如我們可以為數據訪問層生成這樣類似的代碼,把循環、創建實體對象,以及給屬性賦值的所有操作全部包含進來:
public static List<Product> LoadProduct(DbDataReader reader) { List<Product> list = new List<Product>(); while( reader.Read() ) { Product p = new Product(); p.ProductID = (int)reader["ProductID"]; p.ProductName = reader["ProductName"].ToString(); p.CategoryID = (int)reader["CategoryID"]; p.Unit = reader["Unit"].ToString(); p.UnitPrice = (decimal)reader["UnitPrice"]; p.Remark = reader["Remark"].ToString(); p.Quantity = (int)reader["Quantity"]; list.Add(p); } return list; }
如果我們生成了這樣的代碼,最后只需要一次調用,就可以代替以前上百次的委托調用以及緩存查找,鎖的沖突也會減少到最低。
用CodeDOM優化反射的缺點
缺點有三個:
1. 方法不通用,需要針對不同的類型,不同的數據源生成不同的直接調用代碼,因此難以通用化。
2. 復雜性較高,由於是生成直接調用的代碼,且數據類型及格式未知,因此需要周密的考慮各種情況,復雜性也隨之增高。
3. 難以封裝,由於編譯的結果是一個程序集,它並不能直接調用,還需要借助其它的方式來調用,所以難以實現較為通用的封裝。
能不能不使用委托?
既然我們可以在運行時動態生成代碼並編譯它們,達到代替反射的目標,因此也就不需要委托調用的優化方法了。
那么,委托還有意義嗎? 或者說:優化反射時能不能不使用委托?
在上篇博客中,我演示過動態編譯的方法。
由於動態編譯的結果是一個程序集,它本身是不能直接調用,我們需要采用其它的方法去調用它。
那篇博客給大家介紹了二種方法,其中一種方法就是用委托去調用程序集中的方法。
由於那些在運行時生成的代碼是由我們的代碼生成的,方法的簽名我們可以控制,
所以,這時調用 Delegate.CreateDelegate 方法您不會遇到任何麻煩,
因此,通過強類型的委托來調用CodeDOM的編譯結果,這種配合會非常方便。
正是由於這個原因,當您選擇生成static類型的方法時,委托還是必須的,此時委托和CodeDOM將是一種共存關系。
如果您在生成代碼時采用了接口的設計方案,那么委托就沒有必要使用了。
根據反射密集程度選擇優化方法
優化反射,到底是選擇CodeDOM,還是選擇Delegate ?
我認為要按不同的反射密集程度分開討論。
1. 反射密集程度低:例如:一次HTTP請求過程,我們的代碼只需要一二次反射操作,
或者對於桌面程序來說,在響應用戶點擊事件時,使用了幾次反射調用。
在這類場景中,反射的密集程度就可認為是很低的。那么這種情況下該如何優化呢?
我的答案是:優不優化都無所謂,因為反射並不是慢得不能接受。
反射的速度到底有多慢? 我們還是來看一下以前做過的測試吧:
從這張圖片(來源於本系列的第一篇)可以看出,用反射的方式執行屬性賦值操作,就算運行1000000次,也只花了1.2秒! 要知道我的測試機器是3年前買的筆記本電腦,如果換成目前專業的服務器,消耗的時間會更少, 因此,這類反射的優化價值不大。 當然了,如果您願意優化它們那也不是件壞事。
2. 反射密集程度高:例如,數據訪問層的應用中, 當一次加載一個實體列表時,反射次數是分頁數量乘以字段數量,再加上創建實體對象數量。 這個數量很容易達到百次級別,而且一次HTTP請求過程中,可能需要加載多種數據,那么反射次數就很可觀了。 我們經常感覺各種序列化和反序列化程序的執行效率不高,這與反射有着很直接的關系。 不過,我們通常不需要編寫序列化反序列化程序,也只能被迫接受它們的性能了。 因此,對於反射密集程度很高的代碼,如果優化手段不理想,肯定會影響性能。
3. 當處於前二者之間的密集程度。由於這類場景實在是無法定性衡量, 而且不同人對性能敏感程度也不一樣,或者由於不同的應用對性能的要求也不同。
因此,這類場景的范圍只能靠自己去評估了,優化方式也只能是自行選擇了:
1. 關注性能的話,就選擇CodeDOM,
2. 否則就選擇Delegate吧,畢竟這種方法使用簡單。
CodeDOM優化的誤區
1. CodeDOM真能讓程序的性能提升千倍嗎?
根據前面的截圖,我們知道直接調用比反射調用的性能要提升千倍, 因此是不是可以認為采用動態編譯的方法,程序的性能就能提升千倍?
答案是否定的。舉例來說,拿創建實體對象的場景來說,雖然反射調用所花時間和直接調用時間差了千倍, 即使我們用動態編譯代替了反射,但是給屬性賦值前,我們需要為那些屬性獲取數據。 然而,獲取數據的操作極有可能比反射更慢,因此,對於整個過程來說,我們能優化的只是其中的一小部分, 所以,當我們測試整個過程時,性能不會提升到千倍。 性能提升多少倍,取決於反射在整個過程中所花時間的比例。
2. CodeDOM方案一定比Delegate方案快。
答案也是否定的,前面已經解釋過了,如果您為每個反射調用去生成一個方法(委托的思路),那么最后還是需要一個委托或者一個接口來調用, 而且此時還要加上編譯器的啟動時間,最終的性能將比委托更慢。
反射優化的總結
反射優化的根本方法只有一條路:避開反射。
然而,避開的方法可分為二種:
1. 用委托去調用。(繞彎子)
2. 生成直接調用代碼,替代反射調用。(直截了當)
這二種方法都有優缺點,我認為選擇哪種方法應該根據反射場景來決定:
1. 調用目標明確(名稱和類型都是已知):強類型委托方法是較好的選擇。
2. 調用目標不明確,且調用程度密集:動態編譯方法是最好的選擇。
3. 其它情況:可以用弱類型委托,或者不優化。