.NET性能优化之为结构体数组使用StructLinq的问题解析

目录
  • 前言
  • Linq是值传递
  • 使用StructLinq
    • 引入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倍的差距,结构体由于频繁装箱内存分配的也更多。

那么注定没办开开心心的在结构体上用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 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结构体内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • asp.net性能优化之使用Redis缓存(入门)

    1:使用Redis缓存的优化思路 redis的使用场景很多,仅说下本人所用的一个场景: 1.1对于大量的数据读取,为了缓解数据库的压力将一些不经常变化的而又读取频繁的数据存入redis缓存 大致思路如下:执行一个查询 1.2首先判断缓存中是否存在,如存在直接从Redis缓存中获取. 1.3如果Redis缓存中不存在,实时读取数据库数据,同时写入缓存(并设定缓存失效的时间). 1.4缺点,如果直接修改了数据库的数据而又没有更新缓存,在缓存失效的时间内将导致读取的Redis缓存是错误的数据. 2:R

  • .NET使用Collections.Pooled提升性能优化的方法

    目录 简介 Collections.Pooled 如何使用 性能对比 PooledList<T> PooledDictionary<TKey, TValue> PooledSet<T> PooledStack<T> PooledQueue<T> 未手动释放场景 原理解析 总结 简介 性能优化就是如何在保证处理相同数量的请求情况下占用更少的资源,而这个资源一般就是CPU或者内存,当然还有操作系统IO句柄.网络流量.磁盘占用等等.但是绝大多数时候,我

  • .NET使用结构体替代类提升性能优化的技巧

    目录 前言 现实的案例 内存占用 计算速度 总结 附录 前言 我们知道在C#和Java明显的一个区别就是C#可以自定义值类型,也就是今天的主角struct,我们有了更加方便的class为什么微软还加入了struct呢?这其实就是今天要谈到的一个优化性能的Tips使用结构体替代类.那么使用结构体替代类有什么好处呢?在什么样的场景需要使用结构体来替代类呢?今天的文章为大家一一解答.注意:本文全部都以x64位平台为例 现实的案例 举一个现实系统的例子,大家都知道机票购票的流程,开始选择起抵城市和机场(

  • ASP.NET比较常用的26个性能优化技巧

    本篇文章主要介绍了"ASP.NET中常用的26个优化性能方法",主要涉及到ASP.NET中常用的26个优化性能方法方面的内容,对于ASP.NET中常用的26个优化性能方法感兴趣的同学可以参考一下. 现在很多客户也慢慢开始注重网站的性能了,同时有很多运营网站的公司也不像以前那样特别在意网站是否非常漂亮,而把更多的精力放在了网站性能优化上面,提供更快更稳定的浏览速度,在这个基础上面进行网站功能上的扩充和完善,那么在asp.net中如何优化性能呢? 1. 数据库访问性能优化 数据库的连接和关

  • .NET性能优化之为集合类型设置初始大小的方法

    目录 前言 集合类型 List源码 Queue.Stack源码 HashSet.Dictionary源码 总结 附录 前言 计划开一个新的系列,来讲一讲在工作中经常用到的性能优化手段.思路和如何发现性能瓶颈,后续有时间的话应该会整理一系列的博文出来.今天要谈的一个性能优化的Tips是一个老生常谈的点,但是也是很多人没有注意的一个点.在使用集合类型是,你应该设置一个预估的初始大小,那么为什么需要这样做?我们一起来从源码的角度说一说. 集合类型 我们先来聊一聊.NET BCL库中提供的集合类型,对于

  • .NET性能优化之为结构体数组使用StructLinq的问题解析

    目录 前言 Linq是值传递 使用StructLinq 引入StructLinq 简单使用 性能 在上文场景中使用 总结 前言 本系列的主要目的是告诉大家在遇到性能问题时,有哪些方案可以去优化:并不是要求大家一开始就使用这些方案来提升性能.在之前几篇文章中,有很多网友就有一些非此即彼的观念,在实际中,处处都是开发效率和性能之间取舍的艺术.<计算机编程艺术>一书中提到过早优化是万恶之源,在进行性能优化时,你必须要问自己几个问题,看需不要进行性能优化. 优化的成本高么? 如果立刻开始优化会带来什么

  • C语言 结构体数组详解及示例代码

    所谓结构体数组,是指数组中的每个元素都是一个结构体.在实际应用中,结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生.一个车间的职工等. 定义结构体数组和定义结构体变量的方式类似,请看下面的例子: struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 }class[5]; 表示一个班级有5个学生. 结构体数组在定义的同时也可以初始化,例如: str

  • 详解C++中的指针结构体数组以及指向结构体变量的指针

    C++结构体数组 一个结构体变量中可以存放一组数据(如一个学生的学号.姓名.成绩等数据).如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组.结构体数组与以前介绍过的数值型数组的不同之处在于:每个数组元素都是一个结构体类型的数据,它们都分别包括各个成员项. 定义结构体数组和定义结构体变量的方法相仿,定义结构体数组时只需声明其为数组即可.如: struct Student //声明结构体类型Student { int num; char name[20]; char sex; i

  • C#调用C++DLL传递结构体数组的终极解决方案

    C#调用C++DLL传递结构体数组的终极解决方案 在项目开发时,要调用C++封装的DLL,普通的类型C#上一般都对应,只要用DllImport传入从DLL中引入函数就可以了.但是当传递的是结构体.结构体数组或者结构体指针的时候,就会发现C#上没有类型可以对应.这时怎么办,第一反应是C#也定义结构体,然后当成参数传弟.然而,当我们定义完一个结构体后想传递参数进去时,会抛异常,或者是传入了结构体,但是返回值却不是我们想要的,经过调试跟踪后发现,那些值压根没有改变过,代码如下. [DllImport(

  • C++结构体数组详细解析

    1.定义结构体数组 和定义结构体变量类似,定义结构体数组时只需声明其为数组即可.如: 复制代码 代码如下: struct Student{     int num;     char name[20];     char sex[5];     int age;     float score;     char addr[30];};Student stu[3]; //定义Student类型的数组stu 2.结构体数组的应用举例 题目:对候选人的票的统计程序. 设有3个候选人,最终只能有一个当

  • C语言利用结构体数组实现学生成绩管理系统

    要求: 某班有最多不超过30人(具体人数由键盘输入)参加期末考试,最多不超过6门(具体门数由键盘输入).定义结构体类型描述学生信息,每个学生信息包括:学号.姓名.多门课的成绩.总成绩和平均成绩.用结构体数组作为函数参数,编程实现如下菜单驱动的学生成绩管理系统. (1) 录入每个学生的学号.姓名和各科考试成绩. (2) 计算每门课程的总分和平均分. (3) 计算每个学生的总分和平均分. (4) 按每个学生的总分由高到低排出名次表. (5) 按学号由小到大排出成绩表. (6) 按姓名的字典顺序排出成

  • C语言结构体数组同时赋值的另类用法

    说到C语言结构体数组的同时赋值,许多人一想就会想到用以下的这种方法,咱们来写一个例子: #include <stdio.h> struct student { int a; int b ; int c ; }; struct student array1[1000] ; int main(void) { int i ; for(i = 0 ; i < 1000 ; i++) { array[i].a = 1 ; array[i].b = 2 ; array[i].c = 3 ; } fo

  • 基于C#调用c++Dll结构体数组指针的问题详解

    C#调用c++dll文件是一件很麻烦的事情,首先面临的是数据类型转换的问题,相信经常做c#开发的都和我一样把学校的那点c++底子都忘光了吧(语言特性类). 网上有一大堆得转换对应表,也有一大堆的转换实例,但是都没有强调一个更重要的问题,就是c#数据类型和c++数据类型占内存长度的对应关系. 如果dll文件中只包含一些基础类型,那这个问题可能可以被忽略,但是如果是组合类型(这个叫法也许不妥),如结构体.类类型等,在其中的成员变量的长度的申明正确与否将决定你对dll文件调用的成败. 如有以下代码,其

  • C++结构体数组实现贪吃蛇

    本文实例为大家分享了C++结构体数组实现贪吃蛇的具体代码,供大家参考,具体内容如下 代码: #include<bits/stdc++.h> #include<windows.h> #include<conio.h> using namespace std; const int h=50,w=50,MaxLen=400; void gotoxy(short y,short x)//光标移动函数 { COORD pos={x,y}; SetConsoleCursorPosi

  • C语言结构体数组的定义和使用详解

    目录 介绍 结构体数组定义时初始化 补充 介绍 一个结构体变量可以存放一个学生的一组信息,可是如果有 10 个学生呢?难道要定义 10 个结构体变量吗?难道上面的程序要复制和粘贴 10 次吗? 很明显不可能,这时就要使用数组.结构体中也有数组,称为结构体数组.它与前面讲的数值型数组几乎是一模一样的,只不过需要注意的是,结构体数组的每一个元素都是一个结构体类型的变量,都包含结构体中所有的成员项. 定义结构体数组的方法很简单,同定义结构体变量是一样的,只不过将变量改成数组.或者说同前面介绍的普通数组

随机推荐