類與結構體性能對比測試——以封裝網路心跳包為例

1.背景

接上篇文章深入淺出C#結構體——封裝乙太網心跳包的結構為例,使用結構體性能不佳,而且也說明了原因。本篇文章詳細描述了以類來封裝網路心跳包的優缺點,結果大大提升了解析性能。

2.用類來封裝乙太網心跳包的優缺點

2.1.優點

  • 可以在類里直接new byte[],即直接實例位元組數組,然後寫初始化方法或者構造函數中直接對傳進來的快取進行拷貝賦值;
  • 無需裝箱拆箱;
  • 類屬於引用類型,無需像結構體進行值拷貝,底層直接就是智慧指針;
  • 智慧指針指向同一片記憶體,省記憶體空間;
  • 可以在類里寫很多方便的方法,這也就是面向對象,面向領域的基石,方便以後擴展;

2.2.缺點

  • 存在堆里,讀取性能會比棧稍差(現在PC端的計算速度很快,基本可忽略不計);
  • 雖然類也屬於GC的託管資源,但是GC什麼時候進行自動回收不可控制,需要實現IDisposable介面,用完該類,手動對該類進行釋放動作;

使用類的實際性能怎樣,我們用測試數據說話,後面會放上與結構體測試的性能對比數據。

3.網路心跳包封裝類

這裡全部都命名成了位元組數組,包括 public byte[] type=new byte[1];因為如果是byte type類型,我不知道如何去釋放這一值類型,怕到時候引起記憶體泄露等問題。然後在構造函數裡面將快取buf拷貝到了類的各個屬性中,就是這麼簡單。

    public class TcpHeartPacketClass: BaseDisposable      {          private bool _disposed; //表示是否已經被回收          public TcpHeartPacketClass(byte[] buf)          {              Buffer.BlockCopy(buf, 0, head, 0, 4);              type[0] = buf[4];              Buffer.BlockCopy(buf, 4, length, 0, 2);              Buffer.BlockCopy(buf, 6, Mac, 0, 6);              Buffer.BlockCopy(buf, 12, data, 0, 104);              Buffer.BlockCopy(buf, 116, tail, 0, 4);          }          protected override void Dispose(bool disposing)          {              if (!_disposed) //如果還沒有被回收              {                  if (disposing) //如果需要回收一些託管資源                  {                      //TODO:回收託管資源,調用IDisposable的Dispose()方法就可以                    }                  //TODO:回收非託管資源,把之設置為null,等待CLR調用析構函數的時候回收                  head = null;                  type = null;                  length = null;                  Mac = null;                  data = null;                  tail = null;                    _disposed = true;                }              base.Dispose(disposing);//再調用父類的垃圾回收邏輯          }            public byte[] head=new byte[4];            public byte[] type=new byte[1];            public byte[] length = new byte[2];            public byte[] Mac = new byte[6];            public byte[] data = new byte[104];//數據體            public byte[] tail = new byte[4];      }  

4.實現IDisposable介面

用完類之後,為了主動去釋放類,我封裝了一個釋放基類BaseDisposable。詳見程式碼注釋,有不明白的地方可以在評論區提問,我會詳細作答。

    public class BaseDisposable : IDisposable      {          ~BaseDisposable()          {              //垃圾回收器將調用該方法,因此參數需要為false。              Dispose(false);          }            /// <summary>          /// 是否已經調用了 Dispose(bool disposing)方法。          ///     應該定義成 private 的,這樣可以使基類和子類互不影響。          /// </summary>          private bool disposed = false;            /// <summary>          /// 所有回收工作都由該方法完成。          ///     子類應重寫(override)該方法。          /// </summary>          /// <param name="disposing"></param>          protected virtual void Dispose(bool disposing)          {              // 避免重複調用 Dispose 。              if (!disposed) return;                // 適應多執行緒環境,避免產生執行緒錯誤。              lock (this)              {                  if (disposing)                  {                      // ------------------------------------------------                      // 在此處寫釋放託管資源的程式碼                      // (1) 有 Dispose() 方法的,調用其 Dispose() 方法。                      // (2) 沒有 Dispose() 方法的,將其設為 null。                      // 例如:                      //     xxDataTable.Dispose();                      //     xxDataAdapter.Dispose();                      //     xxString = null;                      // ------------------------------------------------                  }                    // ------------------------------------------------                  // 在此處寫釋放非託管資源                  // 例如:                  //     文件句柄等                  // ------------------------------------------------                  disposed = true;              }          }            /// <summary>          /// 該方法由程式調用,在調用該方法之後對象將被終結。          ///     該方法定義在IDisposable介面中。          /// </summary>          public void Dispose()          {              //因為是由程式調用該方法的,因此參數為true。              Dispose(true);              //因為我們不希望垃圾回收器再次終結對象,因此需要從終結列表中去除該對象。              GC.SuppressFinalize(this);          }            /// <summary>          /// 調用 Dispose() 方法,回收資源。          /// </summary>          public void Close()          {              Dispose();          }      }  

5.應用層調用

    DateTime packetClassStart = DateTime.Now;        TcpHeartPacketClass tcpHeartPacketClass = neTcpHeartPacketClass(ReceviveBuff);        DateTime packetClassEnd = DateTime.Now;      TimeSpan toClassTs = packetClassEnd.Subtra(packetClassStart);      try      {      tcpHeartPacketClass.head[0] = 0x11;        LoggerHelper.Info("類中的包頭:" + BitConverteToString(tcpHeartPacketClass.head));      Console.WriteLine("類中的包頭:{0}", BitConverteToString(tcpHeartPacketClass.head));        LoggerHelper.Info("類中的包類型:" tcpHeartPacketClass.type.ToString());      Console.WriteLine("類中的包類型:{0}"tcpHeartPacketClass.type.ToString());        LoggerHelper.Info("類中的包長度:" + BitConverteToString(tcpHeartPacketClass.length));      Console.WriteLine("類中的包長度:{0}", BitConverteToString(tcpHeartPacketClass.length));        LoggerHelper.Info("類中的MAC地址:" + BitConverteToString(tcpHeartPacketClass.Mac));      Console.WriteLine("類中的MAC地址:{0}", BitConverteToString(tcpHeartPacketClass.Mac));        LoggerHelper.Info("類中的註冊包內容:" + BitConverteToString(tcpHeartPacketClass.data));      Console.WriteLine("類中的註冊包內容:{0}"BitConverter.ToString(tcpHeartPacketClass.data));        LoggerHelper.Info("類中的包尾:" + BitConverteToString(tcpHeartPacketClass.tail));      Console.WriteLine("類中的包尾:{0}", BitConverteToString(tcpHeartPacketClass.tail));        Console.WriteLine("位元組數組類中分割總共花費{0}msn"toClassTs.TotalMilliseconds);      }      finally      {          IDisposable disposable = tcpHeartPacketClass as IDisposable;          if (disposable != null)              disposable.Dispose();      }  

6.Dispose()方法生效的測試

在ty…finally塊執行完Dispose()方法之後,再去給類的某個屬性賦值,我們看是否報錯,如果報錯賦值給空對象則證明釋放成功。

    finally      {          IDisposable disposable = tcpHeartPacketClass        IDisposable;          if (disposable != null)              disposable.Dispose();      }      tcpHeartPacketClass.head[0] = 0x12;  

如下報錯,翻譯過來意思就是對象引用沒有對應的實例,也就是被我們給釋放掉了。

7.測試性能對比

通過上圖可以看到,上面的類解析的是微秒級別的,而文章深入淺出C#結構體——封裝乙太網心跳包的結構為例解析的是幾十微秒級別的,差了差不多5到10倍的性能。

由此可見,在這種應用場景下,使用類來封裝網路心跳包比結構體封裝更合理。

8.綜上,在C#里,結構體主要作用有如下兩點:

  • 數據長度很短,構造16位元組以下的新類型,而且結構體內的子類型必須是值類型,不然沒意義,其目的是為了適應棧上的高效讀取;
  • 為了兼容一些來自C跟C++的庫;
    避開以上兩點,我認為在C#新開發的應用程式中,可以完全的用類來取代結構體(僅代表個人觀點)。

版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

本文鏈接:https://www.cnblogs.com/JerryMouseLi/p/12610332.html