.Net常用的几种提高性能的手段

.Net常用的几种提高性能的手段

.NET高性能内存管理

你是不是还不了解spanMemeoryArrayPool这些用法,随着.net的不断升级,像这些提高性能的用法也层出不穷,本文重点介绍一些能够提升.net性能的用法

开篇之前,先说下提高性能的宗旨,无非就是尽量减少堆上重新分配与数据复制,让内存得以重用或在栈上分配,从而降低 GC 压力、提升访问速度

Span、ReadOnlySpan

它是栈上的结构(ref struct),所以不会触发GC,可以提供数组、字符串等的零拷贝切片,生命周期仅仅局限于当前的栈,也就是当前方法结束,就会清除,不可以跨方法或者异步操作传递。
ReadOnlySpan<T>ref struct,只能用于:局部变量、方法参数/返回值,或 ref struct 的实例字段。
整体比较好用,不过我觉得比较鸡肋的一点是只能在同步方法中使用,不能再异步方法中传递,因为await本质会把方法状态机移到堆上,不过又推出了Memory解决了这个问题,见下文。

1
2
3
4
5
6
7
{
string csv = "1,2,3,4,5";
ReadOnlySpan<char> span = csv.AsSpan();
ReadOnlySpan<char> firstTwo = span.Slice(0, 3);
firstTwo.ToString().Display(); // 输出 "1,2"此时没有有新的字符串分配
csv.Substring(0,3).Display(); // 输出 "1,2",此时有新的字符串分配
}

Span<T>仅创建视图,不分配新对象,相比 Substring 或数组切片性能高且无 GC 影响。

堆上切片Memory 、 ReadOnlyMemory

特点和用法类似于Span,不同点时可以分配到堆上

1
2
3
4
5
6
async Task<int> ReadStreamAsync(Stream stream)
{
Memory<byte> buffer = new byte[1024];
int bytesRead = await stream.ReadAsync(buffer);
return bytesRead;
}

上面返回的是Memory类型,传统的方式是返回byte[],但是byte[] 可能导致异步复制,Memory可直接传递,减少内存分配

在实际开发中,Span其实可以和Memory根据需求进行转换,比如Memory转换成span
Span<T> span = buffer.Span
同样,也可以把span转换为Memory
Memory<T> m = MemoryMarshal.AsMemory(span);

数组分段 ArraySegment

表示数组片段的结构体,也就是在不复制原始数组的情况下,安全的操作数组的一部分

1
2
3
4
5
byte[] data = new byte[10];
var segment = new ArraySegment<byte>(data, 2,5);
Console.WriteLine(segment.Count); // 输出 5
segment[0] = 42; // 修改原始数组 data 的第 2 个元素,会影响原来的数组
data.Display(); //[ 0, 0, 42, 0, 0, 0, 0, 0, 0, 0 ]

[ 0, 0, 42, 0, 0, 0, 0, 0, 0, 0 ]

内存池ArrayPool 、 MemoryPool 、 IMemoryOwner

这几个类型的核心目标是减少频繁的内存分配,减少释放内存的压力(GC)

ArrayPool<T>

主要用于管理数组,避免频繁的new(),有全局的默认内存池(ArrayPool<T>.Shared),当然也可以自定义如指定长度等。
注意:用完一定要归还,否则有内存泄漏的风险

1
2
3
4
5
6
using System.Buffers;
int[] rentedArray = ArrayPool<int>.Shared.Rent(10); // 从共享池中租用一个长度至少为10的数组
rentedArray.Display();
rentedArray[0] = 42;
rentedArray.Display();
ArrayPool<int>.Shared.Return(rentedArray); // 使用完毕后归还数组

[ 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

上面用的ArrayPool<T>.Shared,Shared本质是全局的静态池实例,大多数场景是足够的,但是你要完全隔离出一块内存,或者说是高度自定义,则可以使用非共享的形式

1
2
3
4
5
6
7
8
9
10
// 自定义内存池,而不是使用共享池
//maxArrayLength: 允许被缓存复用的数组的最大长度,当请求的数组长度超过这个值时,会新建数组,但归还时,不会被缓存
//maxArraysPerBucket: 池中每个桶可以存储的最大数组数量
//是缓存长度,而不是租用长度
var mypool = ArrayPool<int>.Create(maxArrayLength: 50, maxArraysPerBucket: 2);

int[] array = mypool.Rent(3);
array[1] = 42;
array.Display();
mypool.Return(array);

[ 0, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

MemoryPool<T>

更灵活不仅仅时数组,直接可以管理内存块
池化的是内存块,通过 IMemoryOwner<T> 接口暴露,可转换为Memory<T> 或 Span<T>使用。
同样有全局默认池MemoryPool<T>.Shared,支持自定义池。

内存块的生命周期由IMemoryOwner<T>管理(通过 Dispose 归还到池),IMemoryOwner:内存块的 “所有者” 接口,必须通过 Dispose() 释放(通常用 using 语句自动管理),否则内存块无法归还到池(导致池耗尽)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Buffers;

using (IMemoryOwner<int> owner = MemoryPool<int>.Shared.Rent(5))
{
Memory<int> memory = owner.Memory; // 获取内存块
memory.Display();
Span<int> s= memory.Span;
for (int i = 0; i < 10; i++)
{
s[i] = i * 10;
}
foreach (var item in memory.Span)
{
Console.WriteLine(item); // 输出 0, 10, 20, ..., 90
}

}// 离开using作用域,自动调用Dispose()归还内存块到池
0
10
20
30
40
50
60
70
80
90
0
0
0
0
0
0

stackalloc / ref struct

stackalloc 是 C# 关键字,用于在当前方法的栈帧(Stack Frame) 上分配连续的内存块(通常是数组),只能分配 unmanaged 类型(如 int、char、struct 等,不能是引用类型 class)的数组,因为引用类型需要 GC 管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
unsafe
{
int* buffer = stackalloc int[10]; // 指针形式(需unsafe上下文)
for (int i = 0; i < 10; i++)
{
buffer[i] = i;
}
for (int i = 0; i < 10; i++)
{
Console.Write(buffer[i]); // 输出 0 到 9
}
}

0123456789
1
2
3
4
5
6
7
8
9
10
11
12
{// 或分配后转换为Span<T>(推荐,更安全,无需unsafe)
Span<int> span = stackalloc int[10];
for (int i = 0; i < span.Length; i++)
{
span[i] = i * 2;
}
for (int i = 0; i < span.Length; i++)
{
Console.Write(span[i]); // 输出 0, 2, 4, ..., 18
}
}

024681012141618

ref struct结构体,只能分配到栈上,不能被装箱到堆上(object)refStruct 会编译错误,核心作用是安全地承载栈内存(如 stackalloc 分配的内存、Span等),不能异步await使用,

作者

步步为营

发布于

2025-11-10

更新于

2025-11-10

许可协议