本想接著上一篇詳解泛型接著寫一篇使用泛型時需要註意的一個性能問題,但是後來想著不如將之前的詳解XX系列更正為現在的效率優化XX系列,記錄在工作時遇到的一些性能優化的經驗和技巧,如果有什麼不足,還請大家多多指出; 在使用集合時,通常為了防止裝箱操作而選擇List<T>、Dictionary<TKey, ...
本想接著上一篇詳解泛型接著寫一篇使用泛型時需要註意的一個性能問題,但是後來想著不如將之前的詳解XX系列更正為現在的效率優化XX系列,記錄在工作時遇到的一些性能優化的經驗和技巧,如果有什麼不足,還請大家多多指出;
在使用集合時,通常為了防止裝箱操作而選擇List<T>、Dictionary<TKey, TValue>等泛型集合,但是在使用過程中如果使用不當,依然會產生大量的裝箱操作;
首先,將值類型的實例當做引用類型來使用時,即會產生裝箱,例如:
int num = 10; object obj = num; IEquatable<int> iEquatable = num;
其次,對於自定義結構,在正常使用時,通常需要註意一些誤裝箱的操作:
public struct MyStruct { public int MyNum; }
對該結構MyStruct的實例調用基類Object中的方法時,都會進行裝箱操作,對於靜態方法(Equals、ReferenceEquals)很好理解,對於實例方法,在CLR調用實例方法時,實際上會把調用這個方法的對象當作第一個參數傳入實例方法,而基類Object中的實例方法都會將Object類型的對象作為第一個參數,因此也會發生裝箱,這其中的實例方法包括GetType和虛方法Equals、GetHashCode、ToString;
其中,GetType方法本身就是通過堆記憶體中與實例數據一起存儲的方法表指針來獲取實例類型信息的,對於值類型實例,本身就沒有這個開銷成員,此處應使用typeof()運算符代替避免裝箱;
三個虛方法可以通過在MyStruct中重寫來防止裝箱操作;但是對於Equals方法,有一些需要區別註意的地方:
在調用值類型基類ValueType中的ValueType.Equals(object obj)方法進行比較操作時,會對當前實例和實參obj進行裝箱,共兩次裝箱(抽象基類ValueType依然是類類型);在MyStruct中重寫了該方法MyStruct.Equals(object obj),在調用myStruct1.Equals(myStruct2)時,依然會對myStruct2進行裝箱,共一次裝箱,此時我們可以在MyStruct中聲明一個Equals的重載方法,參數類型同樣為MyStruct,同時對==和!=運算符進行重載:
public struct MyStruct { public int MyNum; public override bool Equals(object obj) //調用時會對實參進行裝箱 { if (!(obj is MyStruct)) { return false; } MyStruct other = (MyStruct)obj; //拆箱 return this.MyNum == other.MyNum; } public bool Equals(MyStruct other) //重載Equals方法,避免裝箱 { return this.MyNum == other.MyNum; } public static bool operator ==(MyStruct left, MyStruct right) //比較時通常採用==運算符 { return left.Equals(right); } public static bool operator !=(MyStruct left, MyStruct right) { return !(left == right); } }
此時,在調用myStruct1.Equals(myStruct2)、myStruct1 == myStruct2、myStruct1 != myStruct2時都不再產生裝箱操作;
但是,在使用泛型方法時,例如對於以下的方法,重載方法並不會生效:
static bool MyFunc<T>(T obj1, T obj2) { return obj1.Equals(obj2); }
查看其生成的IL代碼可以清楚的知道不生效的原因:
其中預設對obj2進行了box指令調用,而對於obj1,在調用callvir指令時加入了首碼constrained指令,則會判斷obj1的類型定義中是否存在Equals方法的重寫,如果有則調用重寫方法,如果沒有,則裝箱後調用基類ValueType中的虛方法;前面MyStruct的定義中重寫了Equals方法,因此會調用該重寫方法,此時只觸發一次對obj2的裝箱,但依然不是我們想要的;
為了避免這個問題,我們需要在MyStruct的定義中實現IEquatable<T>介面,併在這個泛型方法的聲明中添加約束:
public struct MyStruct : IEquatable<MyStruct> { public int MyNum; public override bool Equals(object obj) { if (!(obj is MyStruct)) { return false; } MyStruct other = (MyStruct)obj; return this.MyNum == other.MyNum; } public bool Equals(MyStruct other) //實現IEquatable<T>介面中的方法 { return this.MyNum == other.MyNum; } public static bool operator ==(MyStruct left, MyStruct right) { return left.Equals(right); } public static bool operator !=(MyStruct left, MyStruct right) { return !(left == right); } }
static bool MyFunc<T>(T obj1, T obj2) where T : IEquatable<T> { return obj1.Equals(obj2); }
此時,查看其IL代碼,可以發現沒有了box指令,避免了裝箱操作:
對泛型集合List<Mystruct>使用一些內含比較的實例方法時,也會遇到上面的裝箱問題,解決方法同樣是實現IEquatable<T>介面;以常用的Contains方法舉例:
List<MyStruct>中的Contains方法中會調用泛型抽象類EqualityComparer<T>.Default的實例來進行比較,而在抽象類EqualityComparer<T>中,會根據類型參數T實例化對應的具體類實例,具體可查看EqualityComparer<T>.CreateComparer()中的實例生成邏輯,其中,會根據T是否實現了IEquatable<T>介面而實例化不同的類的實例:
internal class GenericEqualityComparer<T>: EqualityComparer<T> where T: IEquatable<T>
internal class ObjectEqualityComparer<T>: EqualityComparer<T>
這兩個類的具體實現這裡不再贅述;
基於上面的理解,對於值類型,實現基類的虛方法和IEquatable<T>介面對於避免裝箱十分有必要;
如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的認可是我寫作的最大動力!
作者:Minotauros
出處:https://www.cnblogs.com/minotauros/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。