C#效率優化(1)-- 使用泛型時避免裝箱

Minotauros發表於2018-11-30

  本想接著上一篇詳解泛型接著寫一篇使用泛型時需要注意的一個效能問題,但是後來想著不如將之前的詳解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/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。

相關文章