.Net性能调优-MemoryPool
简单用法
//获取MemoryPool实例,实际返回了一个ArrayMemoryPool<T>
MemoryPool<char> Pool = MemoryPool<char>.Shared;
//加上using
using IMemoryOwner<char> owner = Pool.Rent(1024 * 900);
Memory<char> memory = owner.Memory;
for (int i = 0; i < 1024 * 900; i++)
{
memory.Span[i] = 'x';
}
//从100的下标开始截取10个字符
var sliceArr = memory.Span.Slice(100, 10).ToArray();
ArrayMemoryPool<T>
通过MemoryPool<int>.Shared
我们可以获取到一个MemoryPool<T>的示例,该实例的类型为ArrayMemoryPool<T>
ArrayMemoryPool<T>实际上只有一个函数可用,就是Rent(),还有一个Dispose()函数但是里面没有任何代码
Rent()限制了最大租借长度:2147483647u=(1024^3*2-1)=(2G-1bytes),并返回一个IMemoryOwner<T>对象
ArrayMemoryPoolBuffer<T>
ArrayMemoryPool<T>的Rent()实际返回了一个ArrayMemoryPoolBuffer<T>,该类继承了IMemoryOwner<T>
它提供一个Memroy属性来获取一个Memory<T>对象,我们可以借助Memroy<T>来操控我们租用的数组了
ArrayMemoryPool<T>的内部实际还是调用的ArrayPool<T>.Shared
来租用数组
实现代码如下:
public Memory<T> Memory
{
get
{
T[] array = _array;
if (array == null)
{
System.ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer();
}
return new Memory<T>(array);
}
}
public ArrayMemoryPoolBuffer(int size)
{
_array = ArrayPool<T>.Shared.Rent(size);
}
public void Dispose()
{
T[] array = _array;
if (array != null)
{
_array = null;
ArrayPool<T>.Shared.Return(array);
}
}
Memory<T>
Memory<T>的构造函数接收一个array<T>,存在私有变量_object中。Memory中对数组的操作最终又依赖于Span<T>
public readonly struct Memory<T> : IEquatable<Memory<T>>
{
private readonly object _object;
private readonly int _index;
private readonly int _length;
public static Memory<T> Empty => default(Memory<T>);
public int Length => _length;
public bool IsEmpty => _length == 0;
public unsafe Span<T> Span=>{ _object...}
public T[] ToArray()
{
return Span.ToArray();
}
}
Slice(start,length)
public Memory<T> Slice(int start)
{
return new Memory<T>(_object, _index + start, _length - start);
}
Memory的Slice函数可以对数组进行截取,该函数仍然返回一个Memory<T>对象,新的对象记录了原始的_object和要切割的index、length,所以该函数不会造成额外的内存消耗
Pin()
该函数的作用是获取数组内存的管理权,不让垃圾回收器回收,自己管理内存,但他怎么自己管理的,暂时木有研究。。。有兴趣的小伙伴可以自行研究。
但是因为我们的数组是从ArrayPool<T>租借的,由ArrayMemoryPool<T>的Dispose函数回收的,所以本文对于Memory的使用方式中并不会用到该函数
所以综上所述,简单的理解就是Memory<T>主要是用来管理Span<T>的
那么Span又是啥呢?为什么Memrory<T>不直接提供操作数组的方式,而是要返回一个Span<T>呢?
啊哈哈,这个就问倒我了
简单地概括其原因就是通过Span<T>来操作数组切割分片赋值等,更快,更节约内存,数据流转无复制。而Span本身过于底层,使用方面有很多的局限性。所以最终改用Memory<T>来控制Span<T>的生命周期,而只给用户提供Span的操作数组的相关函数。
至于它怎么底层了,局限性在哪些方面,想深入研究的话网上有很多相关的优秀文章值得阅读一下。对于Span的学习我现在是适可而止,后面把内存需要学习的相关知识学的差不多的时候再回来研究它吧,欠的债早晚是要还的啊...
使用场景
啊这...,对于MemoryPool,确实没有想到使用场景。因为之前的文章曾经介绍过ArrayPool。而MemoryPool的数组又是依赖ArrayPool创建的,反而相比于ArrayPool他又多了创建IMemoryOwner的消耗。这里推荐一篇文章对二者进行了对比:https://endjin.com/blog/2020/09/arraypool-vs-memorypool-minimizing-allocations-ais-dotnet。
其实我们更需要的是Memory<T>和Span<T>,在System.Memory
命名空间下的MemoryExtensions为我们的Array[]提供了扩展方法AsMemory<T>(this T[]? array)
AsSpan<T>(this T[]? array)
等等几十种扩展,足够满足你的需要
所以如果不是非得要用MemoryPool的场景还是推荐直接使用ArrayPool吧,当然如果你有必须使用MemoryPool的场景还请在下面留言告诉我一下是什么场景,互相学习一下,嘿嘿