.NET性能优化-使用结构体替代类

前言

我们知道在C#和Java明显的一个区别就是C#可以自定义值类型,也就是今天的主角struct,我们有了更加方便的class为什么微软还加入了struct呢?这其实就是今天要谈到的一个优化性能的Tips使用结构体替代类
那么使用结构体替代类有什么好处呢?在什么样的场景需要使用结构体来替代类呢?今天的文章为大家一一解答。
注意:本文全部都以x64位平台为例

现实的案例

举一个现实系统的例子,大家都知道机票购票的流程,开始选择起抵城市和机场(这是航线),然后根据自己的需要日期和时间,挑一个自己喜欢的航班和舱位,然后付款。
3e536a3281cc9a29ced6694c5121f91a.png

内存占用

那么全国大约49航司,8000多个航线,平均每个航线有20个航班,每个航班平均有10组舱位价格(经济舱、头等还有不同的折扣权益),一般OTA(Online Travel Agency:在线旅游平台)允许预订一年内的机票。也就是说平台可能有8000*20*10*365=~5亿的价格数据(以上数据均来源网络,实际中的数据量不方便透露)。
OTA平台为了能让你更快的搜索想要的航班,会将热门的航线价格数据从数据库拿出来缓存在内存中(内存比单独网络和磁盘传输快的多得多,详情见下图),就取20%也大约有1亿数据在内存中。

操作速度
执行指令1/1,000,000,000 秒 = 1 纳秒
从一级缓存读取数据0.5 纳秒
分支预测失败5 纳秒
从二级缓存读取数据7 纳秒
使用Mutex加锁和解锁25 纳秒
从主存(RAM内存)中读取数据100 纳秒
在1Gbps速率的网络上发送2Kbyte的数据20,000 纳秒
从内存中读取1MB的数据250,000 纳秒
磁头移动到新的位置(代指机械硬盘)8,000,000 纳秒
从磁盘中读取1MB的数据20,000,000 纳秒
发送一个数据包从美国到欧洲然后回来150 毫秒 = 150,000,000 纳秒

假设我们有如下一个类,类里面有这些属性(现实中要复杂的多,而且会分航线、日期等各个维度存储,而且不同航班有不同的售卖规则,这里演示方便忽略),那么这1亿数据缓存在内存中需要多少空间呢?

public class FlightPriceClass
{/// <summary>/// 航司二字码 如 中国国际航空股份有限公司:CA/// </summary>public string Airline { get; set; }/// <summary>/// 起始机场三字码 如 上海虹桥国际机场:SHA/// </summary>public string Start { get; set; }/// <summary>/// 抵达机场三字码 如 北京首都国际机场:PEK/// </summary>public string End { get; set; }/// <summary>/// 航班号 如 CA0001/// </summary>public string FlightNo { get; set; }/// <summary>/// 舱位代码 如 Y/// </summary>public string Cabin { get; set; }/// <summary>/// 价格 单位:元/// </summary>public decimal Price { get; set; }/// <summary>/// 起飞日期 如 2017-01-01/// </summary>public DateOnly DepDate { get; set; }/// <summary>/// 起飞时间 如 08:00/// </summary>public TimeOnly DepTime { get; set; }/// <summary>/// 抵达日期 如 2017-01-01/// </summary>public DateOnly ArrDate { get; set; }/// <summary>/// 抵达时间 如 08:00/// </summary>public TimeOnly ArrTime { get; set; }
}

我们可以写一个Benchmark,来看看100W的数据需要多少空间,然后在推导出1亿的数据

// 随机预先生成100W的数据 避免计算逻辑导致结果不准确
public static readonly FlightPriceClass[] FlightPrices = Enumerable.Range(0,100_0000).Select(index =>new FlightPriceClass{Airline = $"C{(char)(index % 26 + 'A')}",Start = $"SH{(char)(index % 26 + 'A')}",End = $"PE{(char)(index % 26 + 'A')}",FlightNo = $"{index % 1000:0000}",Cabin = $"{(char)(index % 26 + 'A')}",Price = index % 1000,DepDate = DateOnly.FromDateTime(BaseTime.AddHours(index)),DepTime = TimeOnly.FromDateTime(BaseTime.AddHours(index)),ArrDate = DateOnly.FromDateTime(BaseTime.AddHours(3 + index)),ArrTime = TimeOnly.FromDateTime(BaseTime.AddHours(3 + index)),}).ToArray();// 使用类来存储
[Benchmakr]
public FlightPriceClass[] GetClassStore()
{var arrays = new FlightPriceClass[FlightPrices.Length];for (int i = 0; i < FlightPrices.Length; i++){var item = FlightPrices[i];arrays[i] = new FlightPriceClass{Airline = item.Airline,Start = item.Start,End = item.End,FlightNo = item.FlightNo,Cabin = item.Cabin,Price = item.Price,DepDate = item.DepDate,DepTime = item.DepTime,ArrDate = item.ArrDate,ArrTime = item.ArrTime};}return arrays;
}

来看看最终的结果,图片如下所示。
b2a1dbaadff1fbbd5956c62fe66516eb.png
从上面的图可以看出来100W数据大约需要107MB的内存存储,那么一个占用对象大约就是112byte了,那么一亿的对象就是约等于10.4GB。这个大小已经比较大了,那么还有没有更多的方案可以减少一些内存占用呢?有小伙伴就说了一些方案。

  • 可以用int来编号字符串

  • 可以使用long来存储时间戳

  • 可以想办法用zip之类算法压缩一下

  • 等等
    我们暂时也不用这些方法,对照本文的的标题,大家应该能想到用什么办法,嘿嘿,那就是使用结构体来替代类,我们定义了一个一样的结构体,如下所示。

[StructLayout(LayoutKind.Auto)]
public struct FlightPriceStruct
{// 属性与类一致......
}

我们可以使用Unsafe.SizeOf来查看值类型所需要的内存大小,比如像下面这样。
f123e738bf8e6e9ee906a0659dbb6f38.png

可以看到这个结构体只需要88byte,比类所需要的112byte少了27%。来实际看看能节省多少内存。
d4a103479cc3457d1453260d8694dc4d.png
结果很不错呀,内存确实如我们计算的一样少了27%,另外赋值速度快了57%,而且更重要的是GC发生的次数也少了。
那么为什么结构体可以节省那么多的内存呢?这里需要聊一聊结构体和类存储数据的区别,下图是类数组的存储格式。
2853e14a3ae09b73c44c045f1bfc81fc.png
我们可以看到类数组只存放指向数组引用元素的指针,不直接存储数据,而且每个引用类型的实例都有以下这些东西。

  • 对象头:大小为8Byte,CoreCLR上的描述是存储“需要负载到对象上的所有附加信息”,比如存储对象的lock值或者HashCode缓存值。

  • 方法表指针:大小为8Byte,指向类型的描述数据,也就是经常提到的(Method Table),MT里面会存放GCInfo,字段以及方法定义等等。

  • 对象占位符:大小为8Byte,当前的GC要求所有的对象至少有一个当前指针大小的字段,如果是一个空类,除了对象头和方法表指针以外,还会占用8Byte,如果不是空类,那就是存放第一个字段。
    也就是说一个空类不定义任何东西,也至少需要24byte的空间,8byte对象头+8byte方法表指针+8byte对象占位符
    回到本文中,由于不是一个空类,所以每个对象除了数据存储外需要额外的16byte存储对象头和方法表,另外数组需要8byte存放指向对象的指针,所以一个对象存储在数组中需要额外占用24byte的空间。我们再来看看值类型(结构体)。
    efcb1267f25961fa714e7b0c06213a5e.png
    从上图中,我们可以看到如果是值类型的数组,那么数据是直接存储在数组上,不需要引用。所以存储相同的数据,每个空结构体都能省下24byte(无需对象头、方法表和指向实例的指针)。
    另外结构体数组当中的数组,数组也是引用类型,所以它也有24byte的数据,它的对象占位符用来存放数组类型的第一个字段-数组大小。
    我们可以使用ObjectLayoutInspector这个Nuget包打印对象的布局信息,类定义的布局信息如下,可以看到除了数据存储需要的88byte以外,还有16byte额外空间。
    067355407bfc09a93fb2b028d0c5555b.png
    结构体定义的布局信息如下,可以看到每个结构体都是实际的数据存储,不包含额外的占用。
    06d15eb25c75a429ace8a16f0d438b16.png

那可不可以节省更多的内存呢?我们知道在64位平台上一个引用(指针)是8byte,而在C#上默认的字符串使用Unicode-16,也就是说2byte代表一个字符,像航司二字码、起抵机场这些小于4个字符的完全可以使用char数组来节省内存,比一个指针占用还要少,那我们修改一下代码。

// 跳过本地变量初始化
[SkipLocalsInit]
// 调整布局方式 使用Explicit自定义布局
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
public struct FlightPriceStructExplicit
{// 需要手动指定偏移量[FieldOffset(0)]// 航司使用两个字符存储public unsafe fixed char Airline[2];// 由于航司使用了4byte 所以起始机场偏移4byte[FieldOffset(4)]public unsafe fixed char Start[3];// 同理起始机场使用6byte 偏移10byte[FieldOffset(10)]public unsafe fixed char End[3];[FieldOffset(16)]public unsafe fixed char FlightNo[4];[FieldOffset(24)]public unsafe fixed char Cabin[2];// decimal 16byte[FieldOffset(28)]public decimal Price;// DateOnly 4byte[FieldOffset(44)]public DateOnly DepDate;// TimeOnly 8byte[FieldOffset(48)]public TimeOnly DepTime;[FieldOffset(56)]public DateOnly ArrDate;[FieldOffset(60)]public TimeOnly ArrTime;}

在来看看这个新结构体对象的布局信息。
5d314ee994b85bb0d8c9add14949abd8.png
可以看到现在只需要68byte了,最后4byte是为了地址对齐,因为CPU字长是64bit,我们不用管。按照我们的计算能比88Byte节省了29%的空间。当然使用unsafe fixed char以后就不能直接赋值了,需要进行数据拷贝才行,代码如下。

// 用于设置string值的扩展方法
[MethodImpl(MethodImplOptions.AggressiveInlining)]  
public static unsafe void SetTo(this string str, char* dest)  
{  fixed (char* ptr = str)  {  Unsafe.CopyBlock(dest, ptr, (uint)(Unsafe.SizeOf<char>() * str.Length));  }  
}// Benchmark的方法
public static unsafe FlightPriceStructExplicit[] GetStructStoreStructExplicit()  
{  var arrays = new FlightPriceStructExplicit[FlightPrices.Length];  for (int i = 0; i < FlightPrices.Length; i++)  {  ref var item = ref FlightPrices[i];  arrays[i] = new FlightPriceStructExplicit  {  Price = item.Price,  DepDate = item.DepDate,  DepTime = item.DepTime,  ArrDate = item.ArrDate,  ArrTime = item.ArrTime  };  ref var val = ref arrays[i];  // 需要先fixed 然后再赋值fixed (char* airline = val.Airline)  fixed (char* start = val.Start)  fixed (char* end = val.End)  fixed (char* flightNo = val.FlightNo)  fixed (char* cabin = val.Cabin)  {  item.Airline.SetTo(airline);  item.Start.SetTo(start);  item.End.SetTo(end);  item.FlightNo.SetTo(flightNo);  item.Cabin.SetTo(cabin);  }  }  return arrays;  
}

再来跑一下,看看这样存储提升是不是能节省29%的空间呢。
02d0ccda967e914aedbccf65ae21af12.png

是吧,从84MB->65MB节省了大约29%的内存,不错不错,基本可以达到预期了。
但是我们发现这个Gen0 Gen1 Gen2这些GC发生了很多次,在实际中的话因为这些都是使用的托管内存,GC在进行回收的时候会扫描这65MB的内存,可能会让它的STW变得更久;既然这些是缓存的数据,一段时间内不会回收和改变,那我们能让GC别扫描这些嘛?答案是有的,我们可以直接使用非托管内存,使用Marshal类就可以申请和管理非托管内存,可以达到你写C语言的时候用的malloc函数类似的效果。

// 分配非托管内存 
// 传参是所需要分配的字节数
// 返回值是指向内存的指针
IntPtr Marshal.AllocHGlobal(int cb);// 释放分配的非托管内存
// 传参是由Marshal分配内存的指针地址
void Marshal.FreeHGlobal(IntPtr hglobal);

再修改一下Benchmark的代码,将它改成使用非托管内存。

// 定义了out ptr参数,用于将指针传回
public static unsafe int GetStructStoreUnManageMemory(out IntPtr ptr)  
{  // 使用AllocHGlobal分配内存,大小使用SizeOf计算结构体大小乘需要的数量var unManagerPtr = Marshal.AllocHGlobal(Unsafe.SizeOf<FlightPriceStructExplicit>() * FlightPrices.Length);  ptr = unManagerPtr;  // 将内存空间指派给FlightPriceStructExplicit数组使用var arrays = new Span<FlightPriceStructExplicit>(unManagerPtr.ToPointer(), FlightPrices.Length);  for (int i = 0; i < FlightPrices.Length; i++)  {  ref var item = ref FlightPrices[i];  arrays[i] = new FlightPriceStructExplicit  {  Price = item.Price,  DepDate = item.DepDate,  DepTime = item.DepTime,  ArrDate = item.ArrDate,  ArrTime = item.ArrTime  };  ref var val = ref arrays[i];  fixed (char* airline = val.Airline)  fixed (char* start = val.Start)  fixed (char* end = val.End)  fixed (char* flightNo = val.FlightNo)  fixed (char* cabin = val.Cabin)  {  item.Airline.SetTo(airline);  item.Start.SetTo(start);  item.End.SetTo(end);  item.FlightNo.SetTo(flightNo);  item.Cabin.SetTo(cabin);  }  }  // 返回长度return arrays.Length;  
}// 切记,非托管内存不使用的时候 需要手动释放
[Benchmark]  
public void GetStructStoreUnManageMemory()  
{  _ = FlightPriceCreate.GetStructStoreUnManageMemory(out var ptr);  // 释放非托管内存Marshal.FreeHGlobal(ptr);  
}

再来看看Benchmark的结果。
378ea87e935ddbeed1659262dff550b9.png
结果非常Amazing呀,没有在托管内存上分配空间,赋值的速度也比原来快了很多,后面发生GC的时候也无需扫描这一段内存,降低了GC压力。这样的结果基本就比较满意了。
到现在的话存储1亿的数据差不多6.3GB,如果使用上文中提高的其它方法,应该还能降低一些,比如像如下代码一样,使用枚举来替换字符串,金额使用'分'存储,只存时间戳。

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
[SkipLocalsInit]
public struct FlightPriceStructExplicit
{// 使用byte标识航司  byte范围0~255[FieldOffset(0)]public byte Airline;// 使用无符号整形表示起抵机场和航班号 2^16次方[FieldOffset(1)]public UInt16 Start;[FieldOffset(3)]public UInt16 End;[FieldOffset(5)]public UInt16 FlightNo;[FieldOffset(7)]public byte Cabin;// 不使用decimal 价格精确到分存储[FieldOffset(8)]public long PriceFen;// 使用时间戳替代[FieldOffset(16)]public long DepTime;[FieldOffset(24)]public long ArrTime;
}

最后的出来的结果,每个数据只需要32byte的空间存储,这样存储一亿的的话也不到3GB
6ed130921315e96ca209da14b0e68467.png
本文就不继续讨论这些方式了。

计算速度

那么使用结构体有什么问题吗?我们来看看计算,这个计算很简单,就是把符合条件的航线筛选出来,首先类和结构体都定义了如下代码的方法,Explicit结构体比较特殊,我们使用Span比较。

// 类和结构体定义的方法 当然实际中的筛选可能更加复杂
// 比较航司
public bool EqulasAirline(string airline)  
{  return Airline == airline;  
}  
// 比较起飞机场
public bool EqualsStart(string start)  
{  return Start == start;  
}  
// 比较抵达机场
public bool EqualsEnd(string end)  
{  return End == end;  
}
// 比较航班号
public bool EqualsFlightNo(string flightNo)  
{  return FlightNo == flightNo;  
}
// 价格是否小于指定值
public bool IsPriceLess(decimal min)  
{  return Price < min;  
}
// 对于Explicit结构体 定义了EqualsSpan方法
[MethodImpl(MethodImplOptions.AggressiveInlining)]  
public static unsafe bool SpanEquals(this string str, char* dest, int length)  
{  // 使用span来比较两个数组return new Span<char>(dest, length).SequenceEqual(str.AsSpan());  
}// 实现的方法如下所示
public static unsafe bool EqualsAirline(FlightPriceStructExplicit item, string airline)  
{  // 传需要比较的长度return airline.SpanEquals(item.Airline, 2);  
}
// 下面的方式类似,不再赘述
public static unsafe bool EqualsStart(FlightPriceStructExplicit item, string start)  
{  return start.SpanEquals(item.Start, 3);  
}  
public static unsafe bool EqualsEnd(FlightPriceStructExplicit item, string end)  
{  return end.SpanEquals(item.End, 3);  
}  
public static unsafe bool EqualsFlightNo(FlightPriceStructExplicit item, string flightNo)  
{  return flightNo.SpanEquals(item.FlightNo, 4);  
}  
public static unsafe bool EqualsCabin(FlightPriceStructExplicit item, string cabin)  
{  return cabin.SpanEquals(item.Cabin, 2);  
}  
public static bool IsPriceLess(FlightPriceStructExplicit item, decimal min)  
{  return item.Price < min;  
}

最后Benchmark的代码如下所示,对于每种存储结构都是同样的代码逻辑,由于100W数据一下就跑完了,每种存储方式的数据量都为150W

// 将需要的数据初始化好  避免对测试造成影响
private static readonly FlightPriceClass[] FlightPrices = FlightPriceCreate.GetClassStore();  
private static readonly FlightPriceStruct[] FlightPricesStruct = FlightPriceCreate.GetStructStore();  
private static readonly FlightPriceStructUninitialized[] FlightPricesStructUninitialized =  FlightPriceCreate.GetStructStoreUninitializedArray();  
private static readonly FlightPriceStructExplicit[] FlightPricesStructExplicit =  FlightPriceCreate.GetStructStoreStructExplicit(); 
// 非托管内存比较特殊 只需要存储指针地址即可
private static IntPtr _unManagerPtr;  
private static readonly int FlightPricesStructExplicitUnManageMemoryLength =  FlightPriceCreate.GetStructStoreUnManageMemory(out _unManagerPtr);  
[Benchmark(Baseline = true)]  
public int GetClassStore()  
{  var caAirline = 0;  var shaStart = 0;  var peaStart = 0;  var ca0001FlightNo = 0;  var priceLess500 = 0;  for (int i = 0; i < FlightPrices.Length; i++)  {  // 简单的筛选数据var item = FlightPrices[i];  if (item.EqualsAirline("CA"))caAirline++;  if (item.EqualsStart("SHA"))shaStart++;  if (item.EqualsEnd("PEA"))peaStart++;  if (item.EqualsFlightNo("0001"))ca0001FlightNo++;  if (item.IsPriceLess(500))priceLess500++;  }  Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");  return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;  
}  
[Benchmark]  
public int GetStructStore()  
{  var caAirline = 0;  var shaStart = 0;  var peaStart = 0;  var ca0001FlightNo = 0;  var priceLess500 = 0;  for (int i = 0; i < FlightPricesStruct.Length; i++)  {  var item = FlightPricesStruct[i];  if (item.EqualsAirline("CA"))caAirline++;  if (item.EqualsStart("SHA"))shaStart++;  if (item.EqualsEnd("PEA"))peaStart++;  if (item.EqualsFlightNo("0001"))ca0001FlightNo++;  if (item.IsPriceLess(500))priceLess500++;  }  Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");  return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;  
}  
[Benchmark]  
public int GetFlightPricesStructExplicit()  
{  var caAirline = 0;  var shaStart = 0;  var peaStart = 0;  var ca0001FlightNo = 0;  var priceLess500 = 0;  for (int i = 0; i < FlightPricesStructExplicit.Length; i++)  {  var item = FlightPricesStructExplicit[i];  if (FlightPriceStructExplicit.EqualsAirline(item,"CA"))caAirline++;  if (FlightPriceStructExplicit.EqualsStart(item,"SHA"))shaStart++;  if (FlightPriceStructExplicit.EqualsEnd(item,"PEA"))peaStart++;  if (FlightPriceStructExplicit.EqualsFlightNo(item,"0001"))ca0001FlightNo++;  if (FlightPriceStructExplicit.IsPriceLess(item,500))priceLess500++;  }  Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");  return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;  
}  
[Benchmark]  
public unsafe int GetFlightPricesStructExplicitUnManageMemory()  
{  var caAirline = 0;  var shaStart = 0;  var peaStart = 0;  var ca0001FlightNo = 0;  var priceLess500 = 0;  var arrays = new Span<FlightPriceStructExplicit>(_unManagerPtr.ToPointer(), FlightPricesStructExplicitUnManageMemoryLength);  for (int i = 0; i < arrays.Length; i++)  {  var item = arrays[i];  if (FlightPriceStructExplicit.EqualsAirline(item,"CA"))caAirline++;  if (FlightPriceStructExplicit.EqualsStart(item,"SHA"))shaStart++;  if (FlightPriceStructExplicit.EqualsEnd(item,"PEA"))peaStart++;  if (FlightPriceStructExplicit.EqualsFlightNo(item,"0001"))ca0001FlightNo++;  if (FlightPriceStructExplicit.IsPriceLess(item,500))priceLess500++;  }  Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");  return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;  
}

Benchmark的结果如下。
3b0ac22fe392eba9600f0dcf901242bf.png

我们看到单独使用结构体比类要慢一点点,但是后面那些使用Explicit布局方式和非托管内存的就慢很多很多了,有一倍多的差距,鱼和熊掌真的不可兼得吗?
我们来分析一下后面2种方式比较慢的原因,原因是因为值拷贝,我们知道在C#中默认引用类型是引用传递,而值类型是值传递。

  • 引用类型调用方法传递时只需要拷贝一次,长度为CPU字长,32位系统就是4byte,64位就是8byte

  • 值类型调用方法是值传递,比如值需要占用4byte,那么就要拷贝4byte,在小于等于CPU字长时有优势,大于时优势就变为劣势。
    而我们的结构体都远远大于CPU字长64位8byte,而我们的后面的代码实现发生了多次值拷贝,这拖慢了整体的速度。
    那么有没有什么办法不发生值拷贝呢?当然,值类型在C#中也可以引用传递,我们有ref关键字,只需要在值拷贝的地方加上就好了,代码如下所示。

// 改造比较方法,使其支持引用传递
// 加入ref
public static unsafe bool EqualsAirlineRef(ref FlightPriceStructExplicit item, string airline)  
{  // 传递的是引用 需要fixed获取指针fixed(char* ptr = item.Airline)  {  return airline.SpanEquals(ptr, 2);  }  
}// Benchmark内部代码也修改为引用传递
[Benchmark]  
public unsafe int GetStructStoreUnManageMemoryRef()  
{  var caAirline = 0;  var shaStart = 0;  var peaStart = 0;  var ca0001FlightNo = 0;  var priceLess500 = 0;  var arrays = new Span<FlightPriceStructExplicit>(_unManagerPtr.ToPointer(), FlightPricesStructExplicitUnManageMemoryLength);  for (int i = 0; i < arrays.Length; i++)  {  // 从数组里面拿直接引用ref var item = ref arrays[i];// 传参也直接传递引用if (FlightPriceStructExplicit.EqualsAirlineRef(ref item,"CA"))caAirline++;  if (FlightPriceStructExplicit.EqualsStartRef(ref item,"SHA"))shaStart++;  if (FlightPriceStructExplicit.EqualsEndRef(ref item,"PEA"))peaStart++;  if (FlightPriceStructExplicit.EqualsFlightNoRef(ref item,"0001"))ca0001FlightNo++;  if (FlightPriceStructExplicit.IsPriceLessRef(ref item,500))priceLess500++;  }  Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");  return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;  
}

我们再来跑一下结果,我们的Explicit结构体遥遥领先,比使用类足足快33%,而上一轮中使用非托管内存表现也很好,排在了第二的位置。
3d86248365126648ce2c2d47d5b89014.png
那么同样是引用传递,使用类会更慢一些呢?这就要回到更加底层的CPU相关的知识了,我们CPU里面除了基本的计算单元以外,还有L1、L2、L3这些数据缓存,如下图所示。
f164003ea331c6b0041c6ff13b94032e.png
e36f39243b1a1ac258d7b832903d75a2.png
这个和CPU的性能挂钩,记得文章开头那一个图吗?CPU内部的缓存是速度最快的,所以第一个原因就是对于结构体数组数据是存放的连续的地址空间,非常利于CPU缓存;而类对象,由于是引用类型,需要指针访问,对于CPU缓存不是很有利
第二个原因是因为引用类型在访问时,需要进行解引用操作,也就是说需要通过指针找到对应内存中的数据,而结构体不需要
那么如何验证我们的观点呢,其实BenchmarkDotNet提供了这样的指标展示,只需要引入BenchmarkDotNet.Diagnostics.WindowsNuget包,然后在需要评测的类上面加入以下代码。

[HardwareCounters(HardwareCounter.LlcMisses, // 缓存未命中次数  HardwareCounter.LlcReference)]  // 解引用次数
public class SpeedBench : IDisposable  
{......
}

结果如下所示,由于需要额外的统计Windows ETW的信息,所以跑的会稍微慢一点。
916696deecd49cecf3b6aabca3b355d5.png
我们可以从上图看出,使用引用类型缓存未命中的次数最多,解引用的次数也很多,这些拖慢了性能。
如下图所示,顺序存储的结构体要比跳跃式的引用类型内存访问效率高。另外对象的体积越小,对于缓存就越友好。
dc95914f32f9139755bb2a6ca7c6c884.png

13648e98ef8f51e1a1135fd4d7101215.png

总结

在本文章中,我们讨论了如何使用结构体替换类,达到降低大量内存占用和提升几乎一半计算性能的目的。也讨论了非托管内存在.NET中的简单使用。结构体是我非常喜欢的东西,它有着相当高效的存储结构和相当优异的性能。但是你不应该将所有的类都转换为结构体,因为它们有不同的适用场景。
那么我们在什么时候需要使用结构体,什么时候需要使用类呢?微软官方给出了答案。

✔️ 如果类型的实例比较小并且通常生存期较短或者常嵌入在其他对象中,则考虑定义结构体而不是类。
❌ 避免定义结构,除非具有所有以下特征:

  • 它逻辑上表示单个值,类似于基元类型(intdouble 等等)- 比如我们的缓存数据,基本都是基元类型。

  • 它的实例大小小于16字节 - 值拷贝的代价是巨大的,不过现在有了ref能有更多的适用场景。

  • 它是不可变的 - 在我们今天的例子中,缓存的数据是不会改变的,所以具有这个特征。

  • 它不必频繁装箱 - 频繁装拆箱对性能有较大的损耗,在我们的场景中,函数都做了ref适配,所以也不存在这种情况。

在所有其他情况下,都应将类型定义为类。

其实大家从这些方式也能看出来,C#是一门入门简单但是上限很高的语言,平时可以利用C#的语法特性,快速的进行需求变现;而如果有了性能瓶颈,你完全可以像写C++代码一样写C#代码,获得和C++媲美的性能。

附录

  • 本文源码链接-晚点会上传: https://github.com/InCerryGit/BlogCode-Dotnet-Opt-Perf-Use-Struct-Instead-Of-Class

  • 选择结构体还是类:https://docs.microsoft.com/zh-cn/dotnet/standard/design-guidelines/choosing-between-class-and-struct

  • 结构体设计原则: https://docs.microsoft.com/zh-cn/dotnet/standard/design-guidelines/struct

  • .NET Marshal类: https://docs.microsoft.com/zh-cn/dotnet/api/system.runtime.interopservices.marshal?view=net-6.0 

  • .NET Span类 : https://docs.microsoft.com/zh-cn/dotnet/api/system.span-1?view=net-6.0

  • CPU不同硬件的速度 : http://norvig.com/21-days.html#answers 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/287967.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ISE14.7兼容性问题集锦

一、 关于win8和win10不兼容性解决办法 &#xff08;win7不会出现闪退的问题&#xff0c;按安装流程做完就可以直接使用了&#xff09; 在安装目录下&#xff0c;我这里是D:\ISE_14.7\14.7\ISE_DS\ISE\lib\nt64中找到libPortability.dll和libPortabilityNOSH.dll两个文件夹&…

幼儿园调查过程怎么写_深圳全托幼儿园哪个好 幼儿园寄宿怎么报名

陪孩子一起阅读的精髓不是认字和背诵&#xff0c;亲子阅读的那些事 亲子阅读是当代的养育新潮流&#xff0c;但它还有更可贵的一面。 脾气再大的妈妈&#xff0c;都有温柔可爱的时刻&#xff0c;就是陪伴孩子阅读的时刻&#xff0c;曾经有一位在我看来算不上温柔的妈妈&#xf…

ArcGIS实验教程——实验二:ArcGIS地理配准完整操作步骤

ArcGIS实验视频教程合集&#xff1a;《ArcGIS实验教程从入门到精通》&#xff08;附配套实验数据&#xff09; 目录 一、地理配准工具条简介 二、地理配准过程 1、加载数据&#xff08;点击下载&#xff09;提取码&#xff1a;v9y8 2、加载地理配准工具条 3、添加控制点 …

php基础教程 第二步 通俗易懂的学习变量、常量与数据类型

简介 变量、常量以及数据类型 在编程中&#xff0c;变量指一个存储值的一个盒子&#xff0c;或者说容器。例如一个箱子&#xff0c;用来放杂物&#xff0c;这个箱子就是一个容器&#xff0c;值就为杂物。容器可以分为很多类型&#xff0c;例如瓶子、被子、盒子等。值也有很多类…

可见光能量范围_JACS:游书力团队通过可见光促进的吲哚衍生物分子内去芳构化合成环丁烷稠合的四环吲哚螺环...

点击上方了解化学加VIP会员企业导读近日&#xff0c;中国科学院上海有机所游书力团队开发了一种可见光促进的吲哚衍生物分子内[22]环加成方法&#xff0c;可以极好的收率和立体选择性得到环丁烷稠合的四环吲哚螺环(Scheme 1&#xff0c;底部)。该成果近期发表在J. Am. Chem. So…

记将一个大型客户端应用项目迁移到 dotnet 6 的经验和决策

在经过了两年的准备&#xff0c;以及迁移了几个应用项目积累了让我有信心的经验之后&#xff0c;我最近在开始将团队里面最大的一个项目&#xff0c;从 .NET Framework 4.5 迁移到 .NET 6 上。这是一个从 2016 时开始开发&#xff0c;最多有 50 多位开发者参与&#xff0c;代码…

ArcGIS实验教程——实验一:ArcGIS软件的认识与使用

ArcGIS实验视频教程合集&#xff1a;《ArcGIS实验教程从入门到精通》&#xff08;附配套实验数据&#xff09; 实验目录 第一节 Esri简介 1.1 ESRI简介 1.2 Esri产品发展历程 第二节 ArcGIS简介 2.1 ArcGIS简介 2.2 ArcGIS产品体系 第三节 Desktop简介 3.1 Desktop简介…

10分钟学会vue滚动行为

滚动行为 什么是路由的滚动行为 当切换到新路由时&#xff0c;想要页面滚到顶部&#xff0c;或者是保持原先的滚动位置&#xff0c;就像重新加载页面那样 注意: 这个功能只在 HTML5 history 模式下可用。在这个模式下我们需要启动一个服务 我们用scrollBehavior 方法来做路由滚…

php基础教程 第三步 学习字符串及相关函数

字符串 上一节中我们学习了 php 变量、常量以及数据类型的一些概念。这一节中进一步学习数据类型中的字符串类型及其相关操作方法。 字符串指一串字符的合集。例如单个字符 a&#xff0c;单个字符 b&#xff0c;单个字符 c…这些 a、b、c 皆是字符&#xff1b;当把这些字符连…

Mirantis OpenStack fuel web 安装 使用

2019独角兽企业重金招聘Python工程师标准>>> MirantisOpenStack-4.0.iso的下载地址&#xff1a;http://pan.baidu.com/s/1dDsuzXf。下载之后使用虚拟机或者物理机安装都可以&#xff0c;我使用的是vmware。需要把NAT的自动分配ip功能关闭 1、创建虚拟机 &#xff08…

[图] DevOps:提速从研发到交付流程

你的产品要让用户等多久&#xff1f;制定需求、排期开发、部署调试……这些流程都再跑一遍&#xff1f;传统产品发布流程长&#xff0c;多分支、环境不一、人工操作导致容易出错。面对这些传统发布难题&#xff0c;DevOps才是正确的出路。

php基础教程 第四步 学习运算符

在前面的章节中&#xff0c;以及了解了一些运算法&#xff0c;接下来再讲解一些常规运算法。 加运算法&#xff0c;用来使左右两边的值或表达式进行加法计算。例如有一个变量$a&#xff0c;一个变量$b&#xff0c;它们的值都为9&#xff0c;$a$b为99等于10.。输出使用echo&…

JavaScript基础学习(七)—BOM

BOM(Browser Object Model): 浏览器对象模型。提供了独立于内容而与浏览器窗口交互的对象&#xff0c;BOM主要用于管理窗口和窗口之间的通讯。 一、Navigator对象 navigator对象通常用于获取浏览器和操作系统的信息。 navigator对象是window对象的属性&#xff0c;中文是&…

Spring初识

从上学期开始决心开始学习Spring&#xff0c;自己总是利用不好时间&#xff0c;到处瞎忙&#xff0c;结果浪费了好多时间。想着利用暑假的时间&#xff0c;专心看会儿书。最初我在Spring官网下载jar包的时候&#xff0c;忙会儿了半天愣是没找到下载的链接&#xff0c;瞬间觉得学…

Blazor University (17)使用 RenderFragments 模板化组件

原文链接&#xff1a;https://blazor-university.com/templating-components-with-renderfragements/使用 RenderFragments 模板化组件源代码[1]到目前为止&#xff0c;我们已经创建了基于参数生成 100% 渲染输出的组件&#xff0c;但组件并不总是那么简单。有时我们需要创建将…

php基础教程 第五步 逻辑控制

逻辑判断 在开发项目时&#xff0c;竟然会出现逻辑控制。例如当用户输入“hello”时你需要自动回复“hello 欢迎”&#xff0c;当用户设置的定时时间到达时&#xff0c;你需要提醒用户时间已经结束&#xff1b;再举个例子&#xff0c;在玩网络游戏时&#xff0c;用户控制的角色…

博图程序需要手动同步_贴吧求助帖博图实例单按钮控制灯的程序

接上一期在贴吧看见的求助帖(上图看得见水印)&#xff0c;因为没人回复&#xff0c;发帖的楼主好像删除了帖子。结果我抽时间用博图15.1&#xff0c;S71200做了一个&#xff0c;希望给需要帮助的新人能够起到作用&#xff0c;感觉有用的话可以关注一下我的公众号低压电工&#…

php基础教程 第六步 学习数组以及条件判断switch补充

条件语句 switch 在上一节的学习中&#xff0c;学习了php的条件语句if。在php编程中进行条件判断还可以使用switch语句。switch语句语法如下&#xff1a; <?php switch (值或表达式) { case 值等于值1:当值等于值1时要执行的代码break; case 值等于值2:当值等于值2时要执…

dotnet-exec 小工具

dotnet-exec 小工具Intro在之前的文章中很多会有一些示例代码&#xff0c;这些代码一般都是一些很小的示例&#xff0c;尤其是介绍一些新特性的示例&#xff0c;基本上不会引用其他包&#xff0c;只有 SDK 就可以执行&#xff0c;对于这些示例&#xff0c;一般会每个实例单独一…

分布式服务下的关键技术(转)

系统架构演化历程-初始阶段架构 初始阶段的小型系统 应用程序、数据库、文件等所有的资源都在一台服务器上通俗称为LAMP&#xff08;linux、apache、mysql、php&#xff09;。 特征&#xff1a; 应用程序、数据库、文件等所有的资源都在一台服务器上。 描述&#xff1a; 通常服…