標籤:
Attribute是.NET平台上提供的一種元編程能力,可以通過標記的方式來修飾各種成員。無論是組件設計,語言之間互連,還是最普通的架構使用,現在已經都離不開Attribute了。迫於Attribute的功能的重要性(Kent Beck認為NUnit比早期JUnit設計的好,一個主要方面便是利用了Attribute),Java語言也在5.0版本中引入了與Attribute類似的Annotation概念。不過Attribute說到底也是一種反射操作,平時正常使用不會帶來問題,但是密集的調用還是對效能有一定影響的。這次我們就來總結看看我們究竟可以如何迴避Attribute操作的一些效能問題。
假設我們有一個Attribute,它定義在一個類型上:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]public class TestAttribute : Attribute{ public TestAttribute(string prop) { this.Prop = prop; } public TestAttribute() { } public string Prop { get; set; }}[Test("Hello World")][Test(Prop = "Hello World")]public class SomeClass { }
那麼,如果我們需要獲得SomeClass類型上所標記的TestAttribute,我們一般會使用Type對象的GetCustomAttributes方法。那麼在其中又發生了什麼呢?
通過.NET Reflector來追蹤其中實現,會發現這些邏輯最終是由CustomAttribute的GetCustomAttributes方法完成的,感興趣的朋友們可以找到那個最複雜的重載。由於實現有些複雜,我沒有看懂完整的邏輯,但從關鍵的代碼上可以看出,它其實是使用了Activator.CreateInstance方法建立對象,並且使用反射對Attribute對象的屬性進行設定。於是我便打算瞭解一下這些反射操作占整個GetCustomAttributes方法的多大比重:
CodeTimer.Time("GetCustomAttributes", 1000 * 100, () =>{ var attributes = typeof(SomeClass).GetCustomAttributes(typeof(TestAttribute), true);});CodeTimer.Time("Reflection", 1000 * 100, () =>{ var a1 = (TestAttribute)Activator.CreateInstance(typeof(TestAttribute), "Hello World"); var a2 = (TestAttribute)Activator.CreateInstance(typeof(TestAttribute)); typeof(TestAttribute).GetProperty("Prop").SetValue(a2, "Hello World", null);});
結果如下:
GetCustomAttributes Time Elapsed: 2,091ms CPU Cycles: 5,032,765,488 Gen 0: 43 Gen 1: 0 Gen 2: 0Reflection Time Elapsed: 527ms CPU Cycles: 1,269,399,624 Gen 0: 40 Gen 1: 0 Gen 2: 0
可以看出,雖然GetCustomAttributes方法中使用了反射進行對象的建立和屬性設定,但是它的大部分開銷還是用於擷取一些中繼資料的,它們佔據了3/4的時間,而反射的開銷其實只佔了1/4左右。這就有些令人奇怪了,既然是靜態中繼資料,為什麼.NET Framework不對這些資料進行緩衝,而是每次再去取一次呢?即便是我們不應該緩衝最後得到的Attribute對象,但是用於構造對象的“資訊”是完全可以緩衝下來的。
事實上.NET Framework事實上已經給出了足夠的資訊,那便是CustomAttributeData的GetCustomAttributes方法,它返回的是IList<CustomAttributeData>對象,其中包含了構造Attribute所需要的全部資訊。換句話說,我完全可以根據一個CustomAttributeData來“快速構建”Attribute對象:
public class AttributeFactory{ public AttributeFactory(CustomAttributeData data) { this.Data = data; var ctorInvoker = new ConstructorInvoker(data.Constructor); var ctorArgs = data.ConstructorArguments.Select(a => a.Value).ToArray(); this.m_attributeCreator = () => ctorInvoker.Invoke(ctorArgs); this.m_propertySetters = new List<Action<object>>(); foreach (var arg in data.NamedArguments) { var property = (PropertyInfo)arg.MemberInfo; var propertyAccessor = new PropertyAccessor(property); var value = arg.TypedValue.Value; this.m_propertySetters.Add(o => propertyAccessor.SetValue(o, value)); } } public CustomAttributeData Data { get; private set; } private Func<object> m_attributeCreator; private List<Action<object>> m_propertySetters; public Attribute Create() { var attribute = this.m_attributeCreator(); foreach (var setter in this.m_propertySetters) { setter(attribute); } return (Attribute)attribute; }}
AttributeFactory利用了FastReflectionLib,將ConstructorInfo和PropertyInfo封裝成效能很高的ConstructorInvoker和PropertyAccessor對象,這樣使用起來便有數量級的效能提高。我們再來進行一番測試:
var factories = CustomAttributeData.GetCustomAttributes(typeof(SomeClass)) .Where(d => d.Constructor.DeclaringType == typeof(TestAttribute)) .Select(d => new AttributeFactory(d)).ToList();CodeTimer.Time("GetCustomAttributes", 1000 * 100, () =>{ var attributes = typeof(SomeClass).GetCustomAttributes(typeof(TestAttribute), true);});CodeTimer.Time("AttributeFactory", 1000 * 100, () => factories.ForEach(f => f.Create()));
結果如下:
GetCustomAttributes Time Elapsed: 2,131ms CPU Cycles: 5,136,848,904 Gen 0: 43 Gen 1: 43 Gen 2: 0Attribute Factory Time Elapsed: 18ms CPU Cycles: 44,235,564 Gen 0: 4 Gen 1: 4 Gen 2: 0
在這裡,我們先獲得SomeClass中所有定義過的CustomAttributeData對象,然後根據其Constructor的類型來判斷哪些是用於構造TestAttribute對象的,然後用它們來構造AttributeFactory。在實際使用過程中,AttributeFactory執行個體可以緩衝下來,並反覆使用。這樣的話,我們即可以每次得到新的Attribute對象,又可以避免GetCustomAttributes方法所帶來的莫名其妙的開銷。
事實上,我們完全可以利用這個方法,來實現一個效能更高的GetCustomAttributesEx方法,它的行為可以和.NET內建的GetCustomAttributes完全一致,但是效能可以快上無數——可能是100倍。不過,這個方法雖然不難編寫,但比較麻煩。因為CustomAttributeData只能用於獲得“直接定義”在某個成員上的資料,而實際情況是,我們往往還必鬚根據某個Attribute上標記的AttributeUsage的AllowMultiple和Inherited屬性來決定是否要遍曆整個繼承鏈。只有這般,我們才能百分之百地重現GetCustomAttribute方法的行為。
不過我們在這裡有個優勢,那便是“靜態”。一旦“靜態”,我們便可以為某個特定的情境,用“肉眼”判斷出特定的處理方式,這樣便不需要一個非常通用的GetCustomAttributeEx方法了。例如在實際使用過程中,我們可以可以發現某個Attribute的Inherited屬性為false,那麼我們便可以免去遍曆繼承鏈的麻煩。
最後還有兩點可能值得一提:
除了Type,Assembly等成員內建的GetCustomAttributes方法之外,Attribute類也有些靜態GetCustomAttributes方法可用於擷取Attribute對象。但是,通過.NET Reflector,我們可以發現,Attribute類中的靜態方法,最終還是委託給各自的執行個體方法,因此不會有效能提高。唯一區別對待的是ParameterInfo——不過我沒搞懂為什麼那麼複雜,感興趣的朋友可以自行探索一番。
如果僅僅是判斷一個成員是否定義了某個特定類型的Attribute對象,那麼可以使用Attribute.IsDefined靜態方法。它的效能比GetCustomAttributes後再判斷數組的Length要高效許多倍。不過個人認為這點倒並不是非常重要,因為這原本就是個靜態資訊,即便是我們使用較慢的GetCustomAttributes方法來進行判斷,也可以把最終的true或false結果進行緩衝,這自然也不會有效能問題了。
我們之所以要反覆調用GetCustomAttributes方法,就是因為每次得到的Attribute對象都是建立的,因此在某些情境下可能無法緩衝它們。不過現在已經有了現在更快的做法,在這方面自然也就不會有太大問題了。
Attribute操作的效能最佳化方式