.NET10疯狂的性能提升
.NET10带来了一系列令人振奋的性能优化,主要包括逃逸分析、委托优化和抽象性能提升。逃逸分析能智能判断对象生命周期,将短期对象分配到栈上而非堆,显著减少GC压力。委托优化使编译器能内联不会逃逸的委托,降低内存占用。此外,数组接口和Linq操作(如Contains)也获得重大改进,能更智能地理解开发者意图,避免不必要的中间操作。这些优化使.NET10在内存管理和执行效率上都有显著提升,为开发者带来
.NET10的更新中,性能再一次获得了大幅提升。.NET团队尝试将一些频繁使用的操作,尽可能的优化到极限,那么有哪些内容呢?接下来我们就来看其中一些令人兴奋的更新!
逃逸分析
在.NET10之前了,我们声明在代码中数组都将会默认的分配在堆之上, 这将会有相对较大的开销与内存压力,直到stackalloc与span的出现,我们有能力较为简单的直接在栈上申请内存,并且span的出现也让我们代码的性能能够得到巨量的优化。
到了.NET10,我们再次迎来了一个巨大的更新,逃逸分析。当编译器分析出函数中的一些对象如果在返回后不再会使用,那么便会尝试将其分配到栈上而不是堆上(分配的开销相对堆要小非常多),退出方法之后其便会自动释放,这样也可以减少GC的压力。
在.NET10中我们有几个部分已经得到了优化...
数组
[Benchmark]
public int TestSum()
{
int[] numbers = [1, 2, 3];
int sum = 0;
foreach (var item in numbers)
{
sum += item;
}
return sum;
}
当以上代码在.NET10中调用时,编译器能智能的分析出
这个时候,数组将不会再被分配到堆上,而是直接将其分配至栈之上,当函数退出时,其自动就会被释放(比较类似C的行为,但如果返回数组了,则会继续分配到堆上,退出也不会自动释放)
以下例子为同样的原理
[Benchmark]
public void Test()
{
foreach (var item in new string[] { "ABC", "DEF", "GHI"
})
{
Use(item);
}
}
public void Use(string s) { }
测试结果如下,可以看到在.NET10中我们不需要有任何的内存分配!这是一个重大进步,并且有关于这个优化,也是我们一位国人大佬(@hez2010)的pr相关工作

委托
在.NET10中,委托也获得了同样的优化
[Benchmark]
[Arguments(42)]
public int Sum(int y)
{
Func<int, int> addY = x => x + y;
return DoubleResult(addY, y);
}
private int DoubleResult(Func<int, int> func, int arg)
{
int result = func(arg);
return result + result;
}
在.NET10之前代码中将生成一个闭包的类占用了24字节,以及一个委托占用了64字节
但现在,编译器也能发现委托并不会逃逸出方法,于是便能直接将委托内联到代码中,不在需要申请委托相关的内存(这也会利好一点函数式编程)
于是内存申请就会从88字节变为24字节,巨大的进步!
测试结果

不过我们也能发现,事实上连这个24字节的内存我们也有可能能够省略,也许未来的.NET就能做到这一件事。
去抽象
.NET中接口和虚拟方法是实现抽象能力的重要能力,但其也会造成相应的性能损耗,JIT如何理解这种抽象并取消性能惩罚是一件很重要的任务,而在.NET10中也对此做了许多优化。
数组一直是C#中重要的一员,不过其众多的接口实现令优化一直很麻烦,我们先来看一段测试代码
public partial class Tests
{
private ReadOnlyCollection<int> _list
= new(Enumerable.Range(1, 1000).ToArray());
[Benchmark]
public int SumEnumerable()
{
int sum = 0;
foreach (var item in _list)
{
sum += item;
}
return sum;
}
[Benchmark]
public int SumForLoop()
{
ReadOnlyCollection<int> list = _list;
int sum = 0;
int count = list.Count;
for (int i = 0; i < count; i++)
{
sum += _list[i];
}
return sum;
}
}
这两个方法谁会更快的?可能会觉得是SumForLoop,毕竟直接数组的索引访问,肯定会快过迭代器,迭代器甚至还需要申请内存,但事实是...

为什么?甚至要慢的好多,但如果我们将ToArray切换为ToList, 这个结果就会更符合我们的预期。

不过幸运的是,.NET10解决了这个问题,当然效率也一并得到了提升,酷

理所当然的,部分与此Linq方法的效率也会因此再攀升一次。
Enumerable
Enumerable在此版本也获得了性能提升,其中也包括获得了一些新的方法
var seq = Enumerable.Sequence(1, 10, 2);
// 1,3,5,7,9
var infSeq = Enumerable.InfiniteSequence(1, 10);
// 1,11,21,31,...
序列(Sequence),与无限序列(InfiniteSequence)
我想这两个方法特别是Sequence应该被期待很久了,从前我们需要使用Repeat和Range配合Select等方法使用达成类似的效果,而使用序列就可以轻易的实现构建不同步长的序列
而无限序列也可以减少了我们必须要写迭代器的场景。
Linq的优化
Linq的Contains方法在本版本迎来了一次疯狂的优化,或是说本身变得能更智能的理解我们的意图。
例如当我们使用了排序,但最终只是消费了其中第一个值,事实上我们的意图只是获取了其最大值或是最小值。
Contains亦是如此,我们可能会在Contains之前做出很多操作,但实际上最终可能只是需要简单的判断其中是否存在某个值,.NET10的优化让编译器能更智能的理解这一点
[CPUUsageDiagnoser]
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net90)]
[SimpleJob(RuntimeMoniker.Net10_0)]
public partial class Tests
{
private IEnumerable<int> _source = Enumerable.Range(0, 1000).ToArray();
[Benchmark]
public bool AppendContains() => _source.Append(100).Contains(999);
[Benchmark]
public bool ConcatContains() => _source.Concat(_source).Contains(999);
[Benchmark]
public bool DefaultIfEmptyContains() => _source.DefaultIfEmpty(42).Contains(999);
[Benchmark]
public bool DistinctContains() => _source.Distinct().Contains(999);
[Benchmark]
public bool OrderByContains() => _source.OrderBy(x => x).Contains(999);
[Benchmark]
public bool ReverseContains() => _source.Reverse().Contains(999);
[Benchmark]
public bool UnionContains() => _source.Union(_source).Contains(999);
[Benchmark]
public bool SelectManyContains() => _source.SelectMany(x => _source).Contains(999);
[Benchmark]
public bool WhereSelectContains() => _source.Where(x => true).Select(x => x).Contains(999);
}

当然 .NET10还有非常非常多的性能相关的优化。如果希望了解全貌,请一定要阅读Stephen Toub发布的文章
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/
微信公众号: @scixing的炼丹房
Bilibili: @无聊的年
ssccinng/InsaneLinqNET10: .NET10性能优化
https://github.com/ssccinng/InsaneLinqNET10
更多推荐



所有评论(0)