.NET性能优化-为结构体数组使用StructLinq
前言
本系列的主要目的是告诉大家在遇到性能问题时,有哪些方案可以去优化;并不是要求大家一开始就使用这些方案来提升性能。
在之前几篇文章中,有很多网友就有一些非此即彼的观念,在实际中,处处都是开发效率和性能之间取舍的艺术。《计算机编程艺术》一书中提到过早优化是万恶之源,在进行性能优化时,你必须要问自己几个问题,看需不要进行性能优化。
- 优化的成本高么?
- 如果立刻开始优化会带来什么影响?
- 因为对任务目标的影响或是兴趣等其他原因而关注这个问题?
- 任务目标影响有多大?
- 随着硬件性能提升或者框架版本升级,优化的结果会不会过时?
- 如果不进行优化或延迟优化的进行会带来什么负面的影响?
- 如果不进行优化或延迟优化,相应的时间或成本可以完成什么事情,是否更有价值?
如果评估下来,还是优化的利大于弊,而且在合理的时间范围内,那么就去做;如果觉得当前应用的QPS不高、用户体验也还好、内存和CPU都有空余,那么就放一放,主要放在二八法则中能为你创建80%价值的事情上。但是大家要记住过早优化是万恶之源不是写垃圾代码的借口。
回到正题,在上篇文章《使用结构体替代类》中有写在缓存和大数据量计算时使用结构体有诸多的好处,最后关于计算性能的例子中,我使用的是简单的for
循环语句,但是在C#中我们使用LINQ
多于使用for
循环。有小伙伴就问了两个问题:
- 平时使用的
LINQ
对于结构体是值传递还是引用传递? - 如果是值传递,那么有没有办法改为引用传递?达到更好性能?
针对这两个问题特意写一篇回答一下,字数不多,几分钟就能阅读完。
Linq是值传递
在.NET平台上,默认对于值类型的方法传参都是值传递,除非在方法参数上指定ref
,才能变为引用传递。
同样,在LINQ
实现的Where
、Select
、Take
众多方法中,也没有加入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倍的差距,结构体由于频繁装箱内存分配的也更多。
那么注定没办开开心心的在结构体上用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
用于避免装箱(和分配内存),并帮助泛型参数推断。你也可以通过对Where
和Select
函数使用结构来提高性能。
性能
所有的跑分结果你可以在这里找到. 举一个例子,下方代码的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 RyuJIT
DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
Method | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|
LINQ | 65.116 μs | 0.6153 μs | 0.5756 μs | 1.00 | - | - | - | 152 B |
StructLinqWithDelegate | 26.146 μs | 0.2402 μs | 0.2247 μs | 0.40 | - | - | - | 96 B |
StructLinqWithDelegateZeroAlloc | 27.854 μs | 0.0938 μs | 0.0783 μs | 0.43 | - | - | - | - |
StructLinqZeroAlloc | 6.872 μs | 0.0155 μs | 0.0137 μs | 0.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结果如下所示。
从以上Benchmark结果可以得出以下结论:
- 类和结构体都可以使用
StructLinq
来减少内存分配。 - 类和结构体使用
StructLinq
都会导致代码跑的更慢。 - 结构体类型使用
StructLinq
的引用传递模式可以获得5倍的性能提升,比引用类型更快。 - 无论是
LINQ
还是StructLinq
由于本身的复杂性,性能都没有For
循环来得快。
总结
在已经用上结构体的高性能场景,其实不建议使用LINQ
了,因为LINQ
本身它性能就存在瓶颈,它主要就是为了提升开发效率。建议直接使用普通循环。
如果一定要使用,那么建议大于8byte的结构体使用StructLinq
的引用传递模式(ToRefStructEnumerable
),这样可以把普通LINQ
结构体的性能提升5倍以上,也能几乎不分配额外的空间。
.NET性能优化-为结构体数组使用StructLinq的更多相关文章
- .NET性能优化-使用结构体替代类
前言 我们知道在C#和Java明显的一个区别就是C#可以自定义值类型,也就是今天的主角struct,我们有了更加方便的class为什么微软还加入了struct呢?这其实就是今天要谈到的一个优化性能的T ...
- C#调用C/C++动态库 封送结构体,结构体数组
一. 结构体的传递 #define JNAAPI extern "C" __declspec(dllexport) // C方式导出函数 typedef struct { int ...
- 【C语言入门教程】7.2 结构体数组的定义和引用
7.2 结构体数组的定义和引用 当需要使用大量的结构体变量时,可使用结构体定义数组,该数组包含与结构体相同的数据结构所组成的连续存储空间.如下例所示: struct student stu_a[50] ...
- Delphi结构体数组指针的问题
//这段代码在Delphi 2007和delphi 7下是可以执行的,所以正确使用结构体数组和指针应该是这样的,已验证 unit Unit1; interface uses Windows, Mess ...
- C语言中的结构体,结构体数组
C语言中的结构体是一个小难点,下面我们详细来讲一下:至于什么是结构体,结构体为什么会产生,我就不说了,原因很简单,但是要注意到是结构体也是连续存储的,但要注意的是结构体里面类型各异,所以必然会产生内存 ...
- 结构体数组(C++)
1.定义结构体数组 和定义结构体变量类似,定义结构体数组时只需声明其为数组即可.如: struct Student{ int num; char name[20]; char sex[5]; int ...
- c语言学习之基础知识点介绍(十七):写入读取结构体、数组、结构体数组
一.结构体的写入和读取 //写入结构体 FILE *fp = fopen("/Users/ios/Desktop/1.data", "w"); if (fp) ...
- c语言结构体数组定义的三种方式
struct dangdang { ]; ]; ]; int num; int bugnum; ]; ]; double RMB; int dangdang;//成员名可以和类名同名 }ddd[];/ ...
- C#调用C++DLL传递结构体数组的终极解决方案
在项目开发时,要调用C++封装的DLL,普通的类型C#上一般都对应,只要用DllImport传入从DLL中引入函数就可以了.但是当传递的是结构体.结构体数组或者结构体指针的时候,就会发现C#上没有类型 ...
随机推荐
- Elasticsearch 对于大数据量(上亿量级)的聚合如何实现?
Elasticsearch 提供的首个近似聚合是 cardinality 度量.它提供一个字段的基数, 即该字段的 distinct 或者 unique 值的数目.它是基于 HLL 算法的.HLL 会 ...
- Executors 类是什么?
Executors 为 Executor,ExecutorService,ScheduledExecutorService, ThreadFactory 和 Callable 类提供了一些工具方法. ...
- MySQL主从复制(异步复制与半同步复制)
1.MySQl主从复制 原理:将主服务器的binlog日志复制到从服务器上执行一遍,达到主从数据的一致状态. 过程:从库开启一个I/O线程,向主库请求Binlog日志.主节点开启一个binlog du ...
- 学习saltstack (五)
Saltstack介绍 Salt三种运行方式 1.local本地运行2.Master/Minion3.Salt ssh Salt的三大功能 a.远程执行b.配置管理(状态管理)c.云管理:阿里云,aw ...
- 学习saltstack (二)
saltstack使用教程: 1.安装: 需要epel的yum源,没有的话把下面的复制并新建个文件 /etc/yum.repos.d/epel.repo 粘贴即可: [epel] name=Extra ...
- 妙趣横生的HTML5 Page Visibility API
起因 最近浏览36kr 的网页的时候偶然发现一个有趣的情况:当浏览器Tab 处于非当前页的时候,36kr 的标题栏会自动换成可爱的大白字符形象.Jeff 在第一次看到的时候就知道这种效果是通过HTML ...
- 2015 年十佳 HTML5 应用
前言 优秀的前端工程师戴着脚铐跳舞,究竟能把 HTML5 的体验推进到什么程度? 这些 Web apps 是我们运营云集浏览器的网上应用店一年来,我选出的十佳 Web apps.其中参考了同事们的意见 ...
- javascript入门教程(二):变量
大家好,我从今天开始就会正式讲javascript的语法方面.变量 js中的变量一般使用var来声明(es6的let不在本教程讨论范围内),可以用来定义任何种类的变量,如果只对变量进行了定义而没有赋值 ...
- 记一次用mpvue框架搭建的小程序
介绍 mpvue (github 地址请参见)是一个使用 Vue.js 开发小程序的前端框架.框架基于 Vue.js 核心,mpvue 修改了Vue.js 的 runtime 和 compiler 实 ...
- Java在方法中定义可变参数类型
学习目标: 掌握可变参数的应用 学习内容: 1.定义 在方法中传递数组有一种更简单的方式--方法的可变参数,其本质是一个语法糖,目的是让开发者写代码更简单. 2.语法 [修饰符] 返回值类型 方法名称 ...