各位 .NETer 们,大家好!自 C# 3.0 以来,语言集成查询(LINQ),特别是它的 System.Linq.Enumerable 模块(我们称为 LINQ to Objects),早已成为我们 C# 开发工具箱中的一把瑞士军刀。它那无与伦比的表达力和可读性,让我们能用声明式的优雅姿态,轻松驾驭内存中的各种集合操作。

然而,这份优雅在过去常常伴随着性能的“税”。在那些对性能要求极为苛刻的“热路径”中,我们这些老江湖们往往会小心翼翼,甚至不得不进行一种痛苦的仪式——“去 LINQ 化”(de-LINQing)。我们忍痛将那些漂亮的查询表达式,手动重写成原始粗暴的 forforeach 循环,只为从 CPU 周期中榨出最后一滴油。

但是,朋友们,时代变了!.NET 9 的发布,将从根本上颠覆这一性能格局。这不仅仅是微调,而是一次深刻的、具有战略意义的性能革命。通过对 LINQ to Objects 的一系列架构级优化,.NET 9 带来了肉眼可见的性能飞跃。对于许多常见操作,我们现在只需要重新编译一下应用,就能“免费”享受到这份性能红利。那种在可读性与性能之间反复纠结的日子,终于要一去不复返了!

今天,就让我们一起深入探索 .NET 9 中 LINQ to Objects 的性能优化,看看 .NET 团队的那些“魔法师”们,又为我们带来了哪些令人骄傲的“骚操作”。

1. .NET 9 LINQ 新速度的两大架构支柱

.NET 9 中 LINQ 的性能飞跃并非源于某个单一的黑科技,而是建立在几个关键的架构性改进之上。这些底层策略协同工作,系统性地消除了传统 LINQ 实现中的固有开销。

1.1. 通过专用迭代器融合操作 (Iterator Fusion)

传统上,LINQ 查询链(如 source.Where(...).Select(...))在执行时,每一次方法调用都会将前一个 IEnumerable<T> 封装到一个新的迭代器对象中。这个过程会创建层层嵌套的迭代器,带来额外的堆分配和虚方法调用开销,就像给数据套上了一层又一层的俄罗斯套娃。

.NET 9 的解决方案堪称绝妙:引入 “迭代器融合”(Iterator Fusion)。运行时现在能够智能识别出常见的、相邻的 LINQ 方法调用链。一旦匹配到预定义的模式,它就会绕过标准的层层封装,直接实例化一个单一的、高度专业化的“融合迭代器”,这个迭代器一次性就能执行多个操作的逻辑。

案例研究: ListWhereSelectIterator<TSource, TResult>

Where(...).Select(...) 是最经典的 LINQ 操作链,也是迭代器融合的绝佳范例。在 .NET 9 之前,对一个 List<T> 执行此操作会创建至少两个迭代器对象。

而现在,.NET 9 引入了一个名为 ListWhereSelectIterator<TSource, TResult> 的内部迭代器,专门用于处理这种模式。当 Enumerable.Select 方法发现它的数据源是一个 ListWhereIterator<TSource>(即 WhereList<T> 上创建的专用迭代器)时,它不再傻傻地进行二次封装,而是直接创建一个融合了过滤和投影逻辑的 ListWhereSelectIterator 实例。

这个融合迭代器的 MoveNext() 方法,揭示了优化的核心:它在同一个循环迭代中调用了来自 Where 的谓词和来自 Select 的投影委托。这种设计干净利落地消除了一整个迭代器层级、相关的堆分配以及一次虚方法分派,直接转化为实打实的 CPU 和内存性能提升。

1.2. 利用 Span<T> 绕过枚举器开销

传统的 IEnumerable<T> 迭代方式存在固有的性能“税”。为了绕过这些开销,.NET 9 的 LINQ 实现引入了一个关键的内部快速通道(fast path):TryGetSpan() 方法。

现在,许多终端 LINQ 操作(如 Count, Any, First, ToArray 等)在执行前,会先尝试从源集合中获取一个 ReadOnlySpan<T>。如果源对象是数组(T[])或 List<T>TryGetSpan() 就能直接访问其底层连续内存,创建一个零开销的 Span<T>

一旦成功获取到 Span<T>,LINQ 操作符就可以在一个高度可优化的 for 循环中直接遍历内存,完全避免了 IEnumerable<T> 接口带来的所有开销。这是 .NET 9 中许多操作符性能大幅提升的主要原因。虽然此模式在旧版本中已用于少数聚合方法,但 .NET 9 将其应用范围前所未有地扩展到了所有带谓词的终端操作符,实现了革命性的性能飞跃。

2. 性能为王:基准测试见真章

得益于迭代器融合和 Span<T> 快速通道,许多我们日常使用的 LINQ 操作符都获得了新生。口说无凭,我们用 BenchmarkDotNet 的数据说话。

2.1. 终端操作的零开销革命: Any, All, Count, First, 和 Single

这些终端操作符是 TryGetSpan() 优化的最大受益者。当它们作用于数组或 List<T> 时,现在可以完全在栈上完成工作,无需任何堆分配来创建枚举器。

基准测试结果令人振奋!与 .NET 8 相比,这些操作在 .NET 9 上的执行速度提升了约 7 倍,并且操作本身的内存分配降至零

下面是一个 BenchmarkDotNet 基准测试类,你可以亲自验证这种改进:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running; BenchmarkRunner.Run<LinqTerminalMethodsBenchmark>(); [MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, baseline: true)]
[SimpleJob(RuntimeMoniker.Net90)]
public class LinqTerminalMethodsBenchmark
{
private static readonly List<int> _dataSet = Enumerable.Range(0, 1000).ToList(); [Benchmark]
public bool Any() => _dataSet.Any(x => x == 1000); [Benchmark]
public bool All() => _dataSet.All(x => x >= 0); [Benchmark]
public int Count() => _dataSet.Count(x => x == 0); [Benchmark]
public int First() => _dataSet.First(x => x == 999); [Benchmark]
public int Single() => _dataSet.Single(x => x == 0);
}

表 1: 终端 LINQ 操作符在 List<int> 上的官方基准测试结果

这是我的电脑相关信息:

BenchmarkDotNet v0.15.2, Windows 10 (10.0.19045.6093/22H2/2022Update)
Intel Core i9-9880H CPU 2.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.100-preview.5.25277.114
[Host] : .NET 8.0.17 (8.0.1725.26602), X64 RyuJIT AVX2
.NET 8.0 : .NET 8.0.17 (8.0.1725.26602), X64 RyuJIT AVX2
.NET 9.0 : .NET 9.0.6 (9.0.625.26613), X64 RyuJIT AVX2

这是我的基准测试结果:

Method Runtime Mean Ratio Gen0 Allocated Alloc Ratio
Any .NET 8.0 1,947.2 ns 1.00 0.0038 40 B 1.00
.NET 9.0 274.2 ns 0.14 - - 0.00
All .NET 8.0 2,199.1 ns 1.00 0.0038 40 B 1.00
.NET 9.0 267.7 ns 0.12 - - 0.00
Count .NET 8.0 2,199.7 ns 1.00 0.0038 40 B 1.00
.NET 9.0 275.7 ns 0.13 - - 0.00
First .NET 8.0 2,241.8 ns 1.00 0.0038 40 B 1.00
.NET 9.0 526.3 ns 0.23 - - 0.00
Single .NET 8.0 1,844.2 ns 1.00 0.0038 40 B 1.00
.NET 9.0 348.7 ns 0.19 - - 0.00

这些结果清楚地表明,.NET 9 在终端 LINQ 操作上的性能提升是革命性的,每个测试项都有 75%~85% 的提升。

2.2. 链式操作的融合之力: Where(...).Select(...)

正如第一节所讨论的,Where(...).Select(...) 链的性能提升是迭代器融合的直接成果。基准测试表明,当源是 List<T> 时,这个操作链的速度提升了约 57%,内存分配更是减少了超过 60%

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running; BenchmarkRunner.Run<LinqChainedMethodsBenchmark>(); [MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, baseline: true)]
[SimpleJob(RuntimeMoniker.Net90)]
public class LinqChainedMethodsBenchmark
{
private static readonly List<int> _dataSet = Enumerable.Range(0, 100_000).ToList(); [Benchmark]
public List<int> WhereSelect() => _dataSet.Where(x => x % 2 == 0).Select(x => x * 2).ToList();
}

Where(...).Select(...) 链的基准测试结果

Method Job Mean Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
WhereSelect .NET 8.0 392.6 us 1.00 124.5117 124.5117 124.5117 512.56 KB 1.00
WhereSelect .NET 9.0 168.4 us 0.43 62.2559 62.2559 62.2559 195.56 KB 0.38

这种提升意味着我们可以在更少的内存开销下,处理更大的数据集,同时享受 LINQ 带来的代码可读性。

3. 为性能而设计:.NET 9 的新 LINQ 方法

除了优化现有方法,.NET 9 还为我们带来了几个全新的 LINQ API,它们的设计初衷就是为了解决常见的性能和可读性反模式。

3.1. CountBy 和 AggregateBy: 告别低效的 GroupBy

在.NET 9 之前,按键分组并进行计数或求和,我们通常使用 GroupBy 后跟 SelectCount()Sum()。这种模式最大的问题是 GroupBy 会将所有中间分组和元素都缓存在内存中,导致显著的内存开销。

现在,我们可以和这种低效说再见了!.NET 9 引入的 CountByAggregateBy 方法,为此类场景提供了单次遍历、低内存分配的完美解决方案。

表 3: GroupBy vs. CountBy/AggregateBy 对比

任务 .NET 8 及更早版本 (GroupBy) .NET 9 新方式 (CountBy/AggregateBy) 关键优势
按部门统计员工人数 employees.GroupBy(e => e.Department).ToDictionary(g => g.Key, g => g.Count()) employees.CountBy(e => e.Department) 更少内存分配,意图更清晰
按部门计算总薪资 employees.GroupBy(e => e.Department).ToDictionary(g => g.Key, g => g.Sum(e => e.Salary)) employees.AggregateBy(e => e.Department, 0m, (sum, e) => sum + e.Salary) 单次遍历,避免中间集合

看看使用新方法的代码是多么简洁:

public record Employee(string Name, string Department, decimal Salary);

var employees = new List<Employee>
{
new("Alice", "IT", 80000),
new("Bob", "HR", 60000),
new("Charlie", "IT", 95000)
}; // .NET 9: 使用 CountBy 统计各部门人数
var departmentCounts = employees.CountBy(e => e.Department);
// departmentCounts is IDictionary<string, int> // .NET 9: 使用 AggregateBy 计算各部门总薪资
var departmentSalaries = employees.AggregateBy(
e => e.Department,
seed: 0m,
(total, employee) => total + employee.Salary);
// departmentSalaries is IDictionary<string, decimal>

3.2. Index() 方法: 标准化索引迭代

在迭代时获取元素索引,这个需求太常见了。以前我们要么手动维护一个计数器,要么使用 Select((item, index) =>...) 的重载,都略显笨拙。

.NET 9 引入了 IEnumerable.Index() 方法,提供了一个全新的、标准化的、并且高度可读的解决方案。它返回一个 IEnumerable<(int index, T item)>,让我们可以用元组解构在 foreach 中优雅地同时访问索引和元素。

var items = new[] { "Apple", "Banana", "Cherry" };

// .NET 9: 使用 Index() 方法进行优雅的索引迭代
foreach (var (index, item) in items.Index())
{
Console.WriteLine($"Item at index {index} is {item}");
}

这绝对是一项“开发者体验”的巨大优化,减少了我们的认知负荷,消除了样板代码,让代码更优雅、更易于维护。

4. 站在巨人的肩膀上

值得一提的是,.NET 9 的性能飞跃并非一蹴而就,而是建立在 .NET 平台多年来持续优化的深厚基础之上。例如,此前 .NET 中就对 OrderBy(...).First() 这样的模式进行了智能优化,将其转换为更高效的 Min() 操作。更早的版本中,JIT 编译器就已经能够对简单的 Sum() 等操作进行自动矢量化(SIMD),榨干 CPU 的性能。

这些来自过去版本的增强,与 .NET 9 的架构革新相结合,共同构成了今天 LINQ 强大的性能表现。它体现了 .NET 团队一种持之以恒的工匠精神。

结论与战略建议

.NET 9 为 LINQ to Objects 带来了一次多维度、深层次的性能革新。它由架构创新、全新 API 和历史累积的运行时增强共同驱动。

基于以上分析,我为各位.NETer 提供以下战略性建议:

  1. 充满信心地升级:首要建议就是尽快升级到.NET 9。性能优势是显著且广泛的,并且在许多情况下,仅需重新编译即可获得。
  2. 拥抱新 API:主动寻找代码库中复杂的 GroupBy 聚合链,并用更高效、更具可读性的 CountByAggregateBy 进行重构。在需要索引迭代时,果断采用 Index() 方法。
  3. 重新审视性能假设for 循环与 LINQ 之间的历史性能差距已在 .NET 9 中被大幅甚至完全抹平。在升级后,应该重新对关键的热路径进行性能分析。那些过去为了性能而被“去 LINQ 化”的代码,现在可能不再需要这种手动优化了。
  4. 为快速通道而设计:在设计自己的数据结构或 API 时,如果可行,优先返回数组或 List<T>,以确保你的 API 的使用者能够受益于 LINQ 的 TryGetSpan() 快速通道优化。

总而言之,.NET 9 标志着 LINQ 进入了一个新时代。它已从一个单纯追求便利性的工具,转变为一个真正的高性能数据操作利器。作为.NET 开发者,我们有理由为此感到自豪!


感谢阅读到这里,如果感觉本文对您有帮助,请不吝评论点赞,这也是我持续创作的动力!

也欢迎加入我的 .NET骚操作 QQ群:495782587,一起交流.NET 和 AI 的各种有趣玩法!

换成.NET 9,你的LINQ代码还能快上7倍的更多相关文章

  1. 由于ios由UIWebView换成了WKWebview内核后导致webview请求接口文件上传,后台接收不到文件

    2020年4月起App Store将不再接受使用UIWebView的新App上架.2020年12月起将不再接受使用UIWebView的App更新. 解决后台文件接收不到的问题 function GLA ...

  2. python txt装换成excel

    工作中,我们需要经常吧一些导出的数据文件,例如sql查出来的结果装换成excel,用文件发送.这次为大家带上python装换excel的脚本 记得先安装wlwt模块,适用版本,python2-3 #c ...

  3. 【代码笔记】iOS-把<br!>换成\n

    代码: - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. // ...

  4. JS代码获取当前日期时支持IE,不兼容FF和chrome,解决这个问题,我们需要把获取时间的getYear()函数换成getFullYear()

    以前在页面中获得当前时间的方法如下: function SelectTodayClient() { var d = new Date(); var taday = d.getYear() + &quo ...

  5. html 转字符串换成代码

    1. [文件] htmlToCode.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ...

  6. JS将秒换成时分秒实现代码 [mark]

    将秒换成时分秒的方法有很多,在本文将为大家介绍下,使用js的具体的实现思路,有需要的朋友可以参考下,希望对大家有所帮助 http://www.jb51.net/article/41098.htm fu ...

  7. WPF换肤之四:界面设计和代码设计分离

    原文:WPF换肤之四:界面设计和代码设计分离 说起WPF来,除了总所周知的图形处理核心的变化外,和Winform比起来,还有一个巨大的变革,那就是真正意义上做到了界面设计和代码设计的分离.这样可以让美 ...

  8. 国内最大的 Node.js 社区将 New Relic 的监控产品换成了 OneAPM

    国内最知名的 CNode 社区把 New Relic 的监控产品换成了 OneAPM .难道 APM 的老大 New Relic 已经被 OneAPM 超越? 毋庸置疑,在全球应用性能管理 SaaS ...

  9. 用阿里云的免费 SSL 证书让网站从 HTTP 换成 HTTPS

    HTTP 协议是不加密传输数据的,也就是用户跟你的网站之间传递数据有可能在途中被截获,破解传递的真实内容,所以使用不加密的 HTTP 的网站是不太安全的.所以, Google 的 Chrome 浏览器 ...

  10. 转:js小技巧 ,将彻底屏蔽鼠标右键,可用于Table ,取消选取、防止复制,IE地址栏前换成自己的图标

    1. oncontextmenu="window.event.returnValue=false" 将彻底屏蔽鼠标右键<table border oncontextmenu= ...

随机推荐

  1. ZigZag Conversion——LeetCode进阶路⑥

    原题链接https://leetcode.com/problems/zigzag-conversion/ 没开始看题目时,小陌发现这道题似乎备受嫌弃,被n多人踩了,还有点小同情 题目描述 The st ...

  2. MySQL的表空间释放

    概述 最近为了对 MySQL 数据库磁盘占用瘦身,对一张近100GB表的历史数据进行了 delete 删除,删除了约2/3的数据,删除后发现该表占用的空间并未减少.通过下面语句查看该表的磁盘占用情况: ...

  3. 洛谷 P5012 水の数列

    洛谷 P5012 水の数列 Problem 给你一个长度为\(n(n\le10^6)\)的数列,有\(T(T\le 10^3)\)组询问,每一组询问查询区间\([l,r]\),请选择一个\(x\),将 ...

  4. AI大模型应用开发入门-LangChain开发聊天机器人ChatBot

    在大模型应用开发中,状态管理 和 对话追踪 是不可忽视的重要能力,尤其在需要保存上下文.重放对话或进行异步处理时尤为关键. 今天我们来演示如何用 LangChain + OpenAI 的 GPT 模型 ...

  5. Windows11 关闭搜索栏中的Web网页搜索

    ️ Win11 搜索栏总弹出网页搜索通过注册表彻底关闭 在 Windows 11 系统中,当你通过任务栏中的搜索栏查找内容时,除了显示本地文件.应用和设置外,系统还会自动集成 Bing 搜索结果,展示 ...

  6. 详解 Flink Catalog 在 ChunJun 中的实践之路

    我们知道 Flink 有Table(表).View(视图).Function(函数/算子).Database(数据库)的概念,相对于这些耳熟能详的概念,Flink 里还有一个 Catalog(目录) ...

  7. 从零开始实现简易版Netty(一) MyNetty Reactor模式

    从零开始实现简易版Netty(一) MyNetty Reactor模式 自从18年作为一个java程序员入行以来,所接触到的大量组件如dubbo.rocketmq.redisson等都是基于netty ...

  8. zip文件下载

    记录一下zip压缩文件下载 下载的内容有些大 ,通过变成.zip的文件的话会小很多,response是HttpServletResponse,记得关闭流 //文件压缩下载 ZipOutputStrea ...

  9. Docker网络使用说明

    一.Docker网络访问 Docker网卡 查看Docker桥接网卡docker0 ifconfig 二.Docker随机端口映射 随机端口映射举例 #-d 表示在后台运行容器并输出容器ID #-P ...

  10. 关于自定义比较函数 usort 如何使用 类中的方法

    http://blog.csdn.net/qdujunjie/article/details/42081137