.NET性能优化-为结构体数组使用StructLinq

前言

本系列的主要目的是告诉大家在遇到性能问题时,有哪些方案可以去优化;并不是要求大家一开始就使用这些方案来提升性能。
在之前几篇文章中,有很多网友就有一些非此即彼的观念,在实际中,处处都是开发效率和性能之间取舍的艺术。《计算机编程艺术》一书中提到过早优化是万恶之源,在进行性能优化时,你必须要问自己几个问题,看需不要进行性能优化。

  • 优化的成本高么?

  • 如果立刻开始优化会带来什么影响?

  • 因为对任务目标的影响或是兴趣等其他原因而关注这个问题?

  • 任务目标影响有多大?

  • 随着硬件性能提升或者框架版本升级,优化的结果会不会过时?

  • 如果不进行优化或延迟优化的进行会带来什么负面的影响?

  • 如果不进行优化或延迟优化,相应的时间或成本可以完成什么事情,是否更有价值?

如果评估下来,还是优化的利大于弊,而且在合理的时间范围内,那么就去做;如果觉得当前应用的QPS不高、用户体验也还好、内存和CPU都有空余,那么就放一放,主要放在二八法则中能为你创建80%价值的事情上。但是大家要记住过早优化是万恶之源不是写垃圾代码的借口。

回到正题,在上篇文章《使用结构体替代类》中有写在缓存和大数据量计算时使用结构体有诸多的好处,最后关于计算性能的例子中,我使用的是简单的for循环语句,但是在C#中我们使用LINQ多于使用for循环。有小伙伴就问了两个问题:

  • 平时使用的LINQ对于结构体是值传递还是引用传递?

  • 如果是值传递,那么有没有办法改为引用传递?达到更好性能?
    针对这两个问题特意写一篇回答一下,字数不多,几分钟就能阅读完。

Linq是值传递

在.NET平台上,默认对于值类型的方法传参都是值传递,除非在方法参数上指定ref,才能变为引用传递。
同样,在LINQ实现的WhereSelectTake众多方法中,也没有加入ref关键字,所以在LINQ中全部都是值传递,如果结构体Size大于8byte(当前平台的指针大小),那么在调用方法时,结构体的速度要慢于引用传递的类。
比如我们编写如下代码,使用常见的Linq API进行数据的结构化查询,分别使用结构体和类,看看效果,数组数据量为1w。

public class SomeClass  
{  public int Value1; public int Value2;  public float Value3; public double Value4;  public string? Value5; public decimal Value6;  public DateTime Value7; public TimeOnly Value8;  public DateOnly Value9;  
}  public struct SomeStruct  
{  public int Value1; public int Value2;  public float Value3; public double Value4;  public string? Value5; public decimal Value6;  public DateTime Value7; public TimeOnly Value8;  public DateOnly Value9;
}[MemoryDiagnoser]  
[Orderer(SummaryOrderPolicy.FastestToSlowest)]  
public class Benchmark  
{  private static readonly SomeClass[] ClassArray;  private static readonly SomeStruct[] StructArray;  static Benchmark()  {  var baseTime = DateTime.Now;  ClassArray = new SomeClass[10000];  StructArray = new SomeStruct[10000];  for (int i = 0; i < 10000; i++)  {  var item = new SomeStruct  {  Value1 = i, Value2 = i, Value3 = i,  Value4 = i, Value5 = i.ToString(),  Value6 = i, Value7 = baseTime.AddHours(i),  Value8 = TimeOnly.MinValue, Value9 = DateOnly.MaxValue  };  StructArray[i] = item;  ClassArray[i] = new SomeClass  {  Value1 = i, Value2 = i, Value3 = i,  Value4 = i, Value5 = i.ToString(),  Value6 = i, Value7 = baseTime.AddHours(i),  Value8 = TimeOnly.MinValue, Value9 = DateOnly.MaxValue  };  }  }  [Benchmark(Baseline = true)]  public decimal Class()  {  return ClassArray.Where(x => x.Value1 > 5000)  .Where(x => x.Value3 > 5000)  .Where(x => x.Value7 > DateTime.MinValue)  .Where(x => x.Value5 != string.Empty)  .Where(x => x.Value6 > 1)  .Where(x => x.Value8 > TimeOnly.MinValue)  .Where(x => x.Value9 > DateOnly.MinValue)  .Skip(100)  .Take(10000)  .Select(x => x.Value6)  .Sum();  }  [Benchmark]  public decimal Struct()  {  return StructArray.Where(x => x.Value1 > 5000)  .Where(x => x.Value3 > 5000)  .Where(x => x.Value7 > DateTime.MinValue)  .Where(x => x.Value5 != string.Empty)  .Where(x => x.Value6 > 1)  .Where(x => x.Value8 > TimeOnly.MinValue)  .Where(x => x.Value9 > DateOnly.MinValue)  .Skip(100)  .Take(10000)  .Select(x => x.Value6)  .Sum();  }  
}

Benchmakr的结果如下,大家看到在速度上有5倍的差距,结构体由于频繁装箱内存分配的也更多。
b703a4a2e022e7aae875080c8a4957a2.png
那么注定没办开开心心的在结构体上用LINQ了吗?那当然不是,引入我们今天要给大家介绍的项目。

使用StructLinq

首先来介绍一下StructLinq,在C#中用结构体实现LINQ,以大幅减少内存分配并提高性能。引入IRefStructEnumerable,以提高元素为胖结构体(胖结构体是指结构体大小大于16Byte)时的性能。

引入StructLinq

这个库已经分发在 NuGet上。可以直接通过下面的命令安装 StructLinq :

PM> Install-Package StructLinq

简单使用

下方就是一个简单的使用,用来求元素和。唯一不同的地方就是需要调用ToStructEnumerable方法。

using StructLinq;int[] array = new [] {1, 2, 3, 4, 5};int result = array.ToStructEnumerable().Where(x => (x & 1) == 0, x=>x).Select(x => x *2, x => x).Sum();

x=>x用于避免装箱(和分配内存),并帮助泛型参数推断。你也可以通过对WhereSelect函数使用结构来提高性能。

性能

所有的跑分结果你可以在这里找到. 举一个例子,下方代码的Linq查询:

list.Where(x => (x & 1) == 0).Select(x => x * 2).Sum();

可以被替换为下面的代码:

list.ToStructEnumerable().Where(x => (x & 1) == 0).Select(x => x * 2).Sum();

或者你想零分配内存,可以像下面一样写(类型推断出来,没有装箱):

list.ToStructEnumerable().Where(x => (x & 1) == 0, x=>x).Select(x => x * 2, x=>x).Sum(x=>x);

如果想要零分配和更好的性能,可以像下面一样写:

var where = new WherePredicate();var select = new SelectFunction();list.ToStructEnumerable().Where(ref @where, x => x).Select(ref @select, x => x, x => x).Sum(x => x);

上方各个代码的Benchmark结果如下所示:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.101[Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJITDefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
MethodMeanErrorStdDevRatioGen 0Gen 1Gen 2Allocated
LINQ65.116 μs0.6153 μs0.5756 μs1.00---152 B
StructLinqWithDelegate26.146 μs0.2402 μs0.2247 μs0.40---96 B
StructLinqWithDelegateZeroAlloc27.854 μs0.0938 μs0.0783 μs0.43----
StructLinqZeroAlloc6.872 μs0.0155 μs0.0137 μs0.11----

StructLinq在这些场景里比默认的LINQ实现快很多。

在上文场景中使用

我们也把上面的示例代码使用StructLinq改写一下。

// 引用类型使用StructLinq
[Benchmark]  
public double ClassStructLinq()  
{  return ClassArray  .ToStructEnumerable()  .Where(x => x.Value1 > 5000)  .Where(x => x.Value3 > 5000)  .Where(x => x.Value7 > DateTime.MinValue)  .Where(x => x.Value5 != string.Empty)  .Where(x => x.Value6 > 1)  .Where(x => x.Value8 > TimeOnly.MinValue)  .Where(x => x.Value9 > DateOnly.MinValue)  .Skip(100)  .Take(10000)  .Select(x => x.Value4)  .Sum(x => x);  
}  // 结构体类型使用StructLinq
[Benchmark]  
public double StructLinq()  
{  return StructArray  .ToStructEnumerable()  .Where(x => x.Value1 > 5000)  .Where(x => x.Value3 > 5000)  .Where(x => x.Value7 > DateTime.MinValue)  .Where(x => x.Value5 != string.Empty)  .Where(x => x.Value6 > 1)  .Where(x => x.Value8 > TimeOnly.MinValue)  .Where(x => x.Value9 > DateOnly.MinValue)  .Skip(100)  .Take(10000)  .Select(x => x.Value4)  .Sum(x => x);  
}  // 结构体类型 StructLinq 零分配
[Benchmark]  
public double StructLinqZeroAlloc()  
{  return StructArray  .ToStructEnumerable()  .Where(x => x.Value1 > 5000, x=> x)  .Where(x => x.Value3 > 5000, x => x)  .Where(x => x.Value7 > DateTime.MinValue, x => x)  .Where(x => x.Value5 != string.Empty, x => x)  .Where(x => x.Value6 > 1, x => x)  .Where(x => x.Value8 > TimeOnly.MinValue, x => x)  .Where(x => x.Value9 > DateOnly.MinValue, x => x)  .Skip(100)  .Take(10000)  .Select(x => x.Value4, x => x)  .Sum(x => x);  
}  // 结构体类型 StructLinq 引用传递
[Benchmark]  
public double StructLinqRef()  
{  return StructArray  .ToRefStructEnumerable()  // 这里使用的是ToRefStructEnumerable.Where((in SomeStruct x) => x.Value1 > 5000)  .Where((in SomeStruct x) => x.Value3 > 5000)  .Where((in SomeStruct x) => x.Value7 > DateTime.MinValue)  .Where((in SomeStruct x) => x.Value5 != string.Empty)  .Where((in SomeStruct x) => x.Value6 > 1)  .Where((in SomeStruct x) => x.Value8 > TimeOnly.MinValue)  .Where((in SomeStruct x) => x.Value9 > DateOnly.MinValue)  .Skip(100)  .Take(10000)  .Select((in SomeStruct x) => x.Value4)  .Sum(x => x);  
}  // 结构体类型 StructLinq 引用传递 零分配
[Benchmark]  
public double StructLinqRefZeroAlloc()  
{  return StructArray  .ToRefStructEnumerable()  .Where((in SomeStruct x) => x.Value1 > 5000, x=> x)  .Where((in SomeStruct x) => x.Value3 > 5000, x=> x)  .Where((in SomeStruct x) => x.Value7 > DateTime.MinValue, x=> x)  .Where((in SomeStruct x) => x.Value5 != string.Empty, x=> x)  .Where((in SomeStruct x) => x.Value6 > 1, x => x)  .Where((in SomeStruct x) => x.Value8 > TimeOnly.MinValue, x=> x)  .Where((in SomeStruct x) => x.Value9 > DateOnly.MinValue, x=> x)  .Skip(100, x => x)  .Take(10000, x => x)  .Select((in SomeStruct x) => x.Value4, x=> x)  .Sum(x => x, x=>x);  
}  // 结构体 直接for循环
[Benchmark]  
public double StructFor()  
{  double sum = 0;  int skip = 100;  int take = 10000;  for (int i = 0; i < StructArray.Length; i++)  {  ref var x = ref StructArray[i];  if(x.Value1 <= 5000) continue;  if(x.Value3 <= 5000) continue;  if(x.Value7 <= DateTime.MinValue) continue;  if(x.Value5 == string.Empty) continue;  if(x.Value6 <= 1) continue;  if(x.Value8 <= TimeOnly.MinValue) continue;  if(x.Value9 <= DateOnly.MinValue) continue;  if(i < skip) continue;  if(i >= skip + take) break;  sum += x.Value4;  }  return sum;  
}

最后的Benchmark结果如下所示。
f8bfaab4fd0d3898e6b576cef36b8482.png
从以上Benchmark结果可以得出以下结论:

  • 类和结构体都可以使用StructLinq来减少内存分配。

  • 类和结构体使用StructLinq都会导致代码跑的更慢。

  • 结构体类型使用StructLinq的引用传递模式可以获得5倍的性能提升,比引用类型更快。

  • 无论是LINQ还是StructLinq由于本身的复杂性,性能都没有For循环来得快。

总结

在已经用上结构体的高性能场景,其实不建议使用LINQ了,因为LINQ本身它性能就存在瓶颈,它主要就是为了提升开发效率。建议直接使用普通循环。
如果一定要使用,那么建议大于8byte的结构体使用StructLinq的引用传递模式(ToRefStructEnumerable),这样可以把普通LINQ结构体的性能提升5倍以上,也能几乎不分配额外的空间。

作者:InCerry

出处:https://www.cnblogs.com/InCerry/p/Dotnet-Perf-Opt-Use-StructLinq-For-ValueType.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

声明:本博客版权归「InCerry」所有。

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

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

相关文章

《假如编程是魔法之零基础看得懂的Python入门教程 》——(七)我把魔法变成了积木

学习目标 了解魔法积木的使用——自定义函数了解魔法积木的结果反馈——自定义函数返回值了解魔法积木的原料传递——自定义函数传参了解魔法积木的类型分类——类与对象 推荐 1.《备受好评的看得懂的C语言入门教程》 目录 第一篇&#xff1a;《假如编程是魔法之零基础看得…

关于Activity的getReferrer():如何在Activity中获取调用者?

http://blog.csdn.net/u013553529/article/details/53856800 关于Activity的getReferrer()之一&#xff1a;如何在Activity中获取调用者&#xff1f; http://blog.csdn.net/u013553529/article/details/53882440 关于Activity的getReferrer()之二&#xff1a;调用者的包名是如何…

【遥感数字图像处理】实验:Erdas 软件的认识与使用

Erdas软件下载地址:《GISer福音来了:测绘地理信息类专业软件版本大全汇总下载!》 1.1 简介 ERDAS IMAGINE 是美国 ERDAS(Earth Resource Data Analysis System)公司开发的遥感图像处理系统,它以其先进的图像处理技术,友好、灵活的用户界面和操作方式,面向广阔应用领域…

import 别名_Python基础找茬系列09--import和from-import的引用区别

一、语法区别二、两种导包在内存上的区别一图看懂import与from-import的区别无论是使用import隐式导包还是form-import显示导包&#xff0c;整个模块都会被加载到内存中例如&#xff1a;from test import b,整个模块依旧进入内存&#xff0c;因为如果只有函数b进入内存&#xf…

设计一个限速器

限速器 (Rate Limiter) 相信大家都不会陌生&#xff0c;在网络系统中&#xff0c;限速器可以控制客户端发送流量的速度&#xff0c;比如 TCP, QUIC 等协议。而在 HTTP 的世界中&#xff0c; 限速器可以限制客户端在一段时间内发送请求的次数&#xff0c;如果超过设定的阈值&…

C语言新手的100个报错解法 已更新11个错误

学习目标 收藏文章报错可以过来查 [更新数据] 此文将会持续更新&#xff0c;收录错误信息&#xff0c;若本文没有收录记得联系我~ CSDN 1_bit 持续更新中… [发布日期&#xff1a;2020年11月16日 14:55:00] 更新&#xff1a; 暂无 C语言教程 C语言真的很难吗&#xff1f;那…

【遥感数字图像处理】实验:遥感图像显示与数据输入/输出(Erdas版)

一、实验平台&#xff1a;Erdas 9.1 二、实验内容&#xff1a;视窗功能简介、图形和图像显示操作、实用菜单操作、显示操作、AOI菜单操作、矢量和栅格菜单、数据的输入输出等。 三、实验目的&#xff1a;初步了解Erdas的主要功能模块&#xff0c;在此基础上&#xff0c;掌握视…

在Windows Server2016中安装SQL Server2016(转)

在Windows Server2016中安装SQL Server2016&#xff08;转&#xff09; 转自&#xff1a; http://blog.csdn.net/yenange/article/details/52980135 参考&#xff1a; SQL Server2016企业版 附全版本key - moonpure的专栏 - CSDN博客 http://blog.csdn.net/moonpure/article/d…

Unity3D 之UGUI 滑动条(Slider)

这里来讲解下UGUI 滑动条(Slider)的用法 控件下面有三个游戏对象 Background -->背景 Fill Area --> 前景区域 Handle Slide Area --> 滑动条 Slider的属性 其他几个设置和其他控件都差不多&#xff0c;这里来讲解几个特有的属性。 Direction -->方向 Whole Number…

C语言真的很难吗?那是你没看这张图,化整为零轻松学习C语言。

真不难 C语言难不难&#xff1f;这个问题是相对的&#xff0c;对于找到合适方法学习C语言的同学想必是觉得很简单&#xff1b;但对于一部分同学来说&#xff0c;没有众观全局就会误以为刚入门就需要学习庞大的知识&#xff0c;学着学着开始看不懂&#xff0c;由于心理作怪&…

【中间件】.net Core中使用HttpReports进行接口统计,分析, 可视化, 监控,追踪等...

HttpReports 基于.Net Core 开发的APM监控系统&#xff0c;使用MIT开源协议&#xff0c;主要功能包括&#xff0c;统计, 分析, 可视化&#xff0c; 监控&#xff0c;追踪等&#xff0c;适合在微服务环境中使用。官方地址&#xff1a;https://www.yuque.com/httpreports/docs/u…

【遥感数字图像处理】实验:遥感影像辐射纠正(大气纠正)完整操作图文教程(Erdas版)

一、实验平台:Erdas 9.1 二、实验数据:dmtm.img 三、实验内容:利用回归分析法校正影像 四、实验原理:大气散射只影响短波波段,长短波进行对比,找出影响短波的程辐射值,将其减去 五、实验目的:掌握回归分析法校正影像的方法及步骤,能熟练地对影像进行校正 六、实…

Acitivty生命周期

为什么80%的码农都做不了架构师&#xff1f;>>> Acitivty 有七个生命周期&#xff1a; onCreate&#xff1a;当第一次调用一个Activity就会执行onCreate方法 onStart&#xff1a;当Activity处于可见状态的时候就会调用onStart方法 onResume&#xff1a;当Activity可…

还不懂你现在学习的编程语言能做什么?还不懂如何进阶?过来看图

前言说七说八 本篇文章的配图标注、内容并不代表仅有&#xff1b;本篇仅以个人经验及当前大学&#xff08;大专、本科&#xff09;相关课程作对比&#xff0c;列出比较常规的语言发展走向及相关技术&#xff1b;再次重申&#xff0c;本图及本文所涉及的技术发展走向并不代表着…

【遥感数字图像处理】实验:遥感影像几何纠正完整操作流程(Erdas版)

☆☆☆ 几何纠正预备知识 ☆☆☆ 1、几何变形误差的影响因素 遥感器本身引起的畸变外部因素引起的畸变处理过程中引起的畸变2、需要做精纠正的情况 景与景之间作比较GIS建模之前监督分类时提取样本创建高精度比例尺的影像地图与矢量数据叠加源于不同比例尺的地图之间比较提取精…

openid 钉钉_钉钉开发入门,微应用识别用户身份,获取用户免登授权码code,获取用户userid,获取用户详细信息...

最近有个需求,在钉钉内,点击微应用,获取用户身份,根据获取到的用户身份去企业内部的用户中心做校验,校验通过,相关子系统直接登陆;就是在获取这个用户身份的时候,网上的资料七零八落的,找的人烦躁的很,所以自己记录一下;实现这个要求,有好几种方式,使用ISV方式相对来说比较简单…

趣味二维码生成

1背景介绍 最近在 Github 看到了一个有趣的项目 amazing-qr&#xff0c;它支持生成普通二维码&#xff0c;带图片的艺术二维码&#xff0c;动态二维码。项目是用 python 编写的&#xff0c;以命令行的方式运行生成&#xff0c;不太方便调用&#xff0c;因此&#xff0c;我…

《零基础看得懂的C++入门教程 》——(1)第一个C++程序就让你知其所以然

一、学习目标 了解第一个C程序了解第一个C程序结构了解什么是注释了解什么是命名空间了解C语言的输出&#xff08;如何在程序运行时显示内容&#xff09;了解语句结束后需要使用什么符号表示结束 了解程序入口 目录 预备第一篇&#xff0c;使用软件介绍在这一篇&#xff0c;…

1、Locust压力测试环境搭建

环境准备&#xff1a;阿里云服务器一台、python2.7、pip Locust 介绍Locust 是一个开源负载测试工具。使用 Python 代码定义用户行为&#xff0c;也可以仿真百万个用户。 Locust 简单易用&#xff0c;分布式&#xff0c;用户负载测试工具。Locust 主要为网站或者其他系统进行负…

交互式 .Net

1名词解析 1. 交互式交互式是指输入代码后可直接运行该代码&#xff0c;然后持续输入运行代码。2. 交互式 .Net.Net 是一种编译型语言&#xff0c;不像 python 这类的脚本型语言&#xff0c;可以边输入代码边运行结果。幸运的是&#xff0c;软微推出了 interactive 这个项…