通俗易懂,C#如何安全、高效地玩轉任何種類的記憶體之Memory<T>(三)
前言
我們都知道,.Net Core是微軟推出的一個通用開發平臺,它是跨平臺和開源的,由一個.NET執行時、一組可重用的框架庫、一組SDK工具和語言編譯器組成,旨在讓.Net developers可以更容易地編寫高效能的服務應用程式和基於雲的可伸縮服務,比如微服務、物聯網、雲原生等等;在這些場景下,對於記憶體的消耗往往十分敏感,也十分苛刻;為了解決這個棘手問題,同時釋放應用開發人員的精力,讓他們能夠安心地使用Net Core,而不用擔心這些應用場景下的效能問題,故從.NET Core 2.1開始引進了兩個新的旗艦型別:Span<T>
、Memory<T>
,使用它們可以避免分配緩衝區和不必要的資料複製
前面已經對span做了詳細地講解,所以今天主題是Memory,同樣以Why、What和How的方式緩緩道來 ,讓你知其然,更知其所以然。
Memory<T>
是Span的補充,它是為了解決Span無法駐留到堆上而誕生的,可以說Span是Memory的奠基,故在讀這篇文章前,請先仔細品讀前面兩篇文章:
現在,作者就當你已經閱讀了前面的部落格,並明白了Span的本質(ref-like type)和秉性特點(stack-only)。
why - 為什麼需要memory ?
span的侷限性
- span只能儲存到執行棧上,保障操作效率與陣列一樣高,並提供穩定的生命週期。
- span不能被裝箱到堆上,避免棧撕裂問題。
- span不能用作泛型型別引數。
- Span不能作為類的欄位。
- Span不能實現任何介面。
- Span不能用於非同步方法,因為無法跨越await邊界,所有無法跨非同步操作暫留。
下面來看一個例子:
async Task DoSomethingAsync(Span<byte> buffer) {// 這裡編譯器會提示報錯,作為例子而已,請忽略。 buffer[0] = 0; await Something(); // 這裡可執行棧已經釋放,Span也回收了。 buffer[0] = 1; // 這裡buffer將無法繼續。 }
備註:C#編譯器和core執行時內部會強制驗證Span的侷限性,所以上面例子才會編譯不過。
正是因為這些侷限性,確保了更高效、安全的記憶體訪問。
也是因為這些侷限性,無法用於需要將引用資料儲存到堆上的一些高階應用場景,比如:非同步方法、類欄位、泛型引數、集合成員、lambda表示式、迭代器等。
還是因為這些侷限性,增加了span對於高層開發人員的複雜性。
所以Memory<T>
誕生了,作為span的補充,它就是目前的解決方案,沒有之一,也是高層開發人員日後使用最普遍的型別。
what - memory是什麼 ?
和Span<T>
一樣,也是sliceable type
,但它不是ref-like type
,就是普通的C#結構體。這意味著,可以將它裝箱到堆上、作為類的欄位或非同步方法的引數、儲存到集合等等,對於高層開發人員非常友好,嘿嘿,並且當需要處理Memory底層緩衝區,即做同步處理時,直接呼叫它的Span屬性,同時又獲得了高效的索引能力。
備註:
Memory<T>
表示一段可讀寫的連續記憶體區域,ReadOnlyMemory
表示一段只讀的連續記憶體區域。
static async Task<uint> ChecksumReadAsync(Memory<byte> buffer, Stream stream)
{
var bytesRead = await stream.ReadAsync(buffer);
// 需要同步處理時,直接呼叫span屬性。
return SafeSum(buffer.Span.Slice(0, bytesRead));
// 千萬不要這樣寫,除非你想要先持久化分片資料到託管堆上,但這又無法使用Span<T>實現;其次Memory <T>是一個比Span<T>更大的結構體,切片往往相對較慢。
//return SafeSum(buffer.Slice(0,bytesRead).Span());
}
static uint SafeSum(Span<byte> buffer)
{
uint sum = 0;
foreach (var t in buffer)
{
sum += t;
}
return sum;
}
Memory核心設計
public readonly struct Memory<T>
{
private readonly object _object; //表示Memory能包裹的物件,EveryThing。
private readonly int _index;
private readonly int _length;
public Span<T> Span { get; } // 實際的內部緩衝區
}
如前所述,Memory的目的是為了解決Span無法駐留到堆上的問題,也就是Memory代表的記憶體塊並不會隨方法執行棧的unwind
而回收,也就是說它的內部緩衝區是有生命週期的,並不是短暫的,這就是為什麼欄位_object
的型別被設計成object
,而不是型別化為T[],就是為了通過傳遞IMemoryOwner
來管理Span的生命週期,從而避免UAF(use-after-free)bug。
private static MemoryPool<byte> _memPool = MemoryPool<byte>.Shared;
public async Task UsageWithLifeAsync(int size)
{
using (var owner = _memPool.Rent(size)) // 從池裡租借一塊IMemoryOwner包裹的記憶體。
{
await DoSomethingAsync(owner.Memory); // 把實際的記憶體借給非同步方法使用。
} // 作用域結束,儲存的Memory<T>被回收,這裡是返回記憶體池,有借有還,再借不難,嘿嘿。
}
// 不用擔心span會隨著方法執行棧unwind而回收
async Task DoSomethingAsync(Memory<byte> buffer) {
buffer.Span[0] = 0; // 沒問題
await Something(); // 跨越await邊界。
buffer.Span[0] = 1; // 沒問題
}
IMemoryOwner
,顧名思義,Memory<T>
擁有者,通過屬性Memory來表示,如下:
public interface IMemoryOwner<T> : IDisposable
{
Memory<T> Memory { get; }
}
所以,可以使用IMemoryOwner
來轉移Memory<T>
內部緩衝區的所有權,從而讓開發人員不必管理緩衝區。
Memory<T>
內部緩衝區生命週期的管理實際上非常複雜,用法如上所訴,感興趣的同學可以自行下去研究。
How - 如何運用memory ?
如前所述, Memory<T>
其實就是Span<T>
的heap-able
型別,故它的API和span基本相同,如下:
public Memory(T[] array);
public Memory(T[] array, int start, int length);
public Memory<T> Slice(int start);// 支援sliceable
public bool TryCopyTo(Memory<T> destination);
不同的是Memory<T>
有兩個獨一無二的API,如下:
public MemoryHandle Pin(); // 釘住_object的記憶體地址,即告知垃圾回收器不要回收它,我們自己管理記憶體。
public System.Span<T> Span { get; }// 當_object欄位為陣列時,提供快速索引的能力。
和Span<T>
一樣,通常Memory<T>
都是包裹陣列、字串,用法也基本相同,只是應用場景不一樣而已。
Memory<T>
的使用指南:
- 同步方法應該接受Span
引數,非同步方法應該接受Memory 引數。 - 以
Memory<T>
作為引數無返回值的同步方法,方法結束後,不應該再使用它。 - 以
Memory<T>
作為引數返回Task的非同步方法,方法結束後,不應該再使用它。 - 同一
Memory<T>
例項不能同時被多個消費者使用。
所以啊,千萬不要將好東西用錯地方了,聰明反被聰明誤,最後,弄巧成拙,嘿嘿。
總結
綜上所述,和Span<T>
一樣,Memory<T>
也是Sliceable type
,它是Span無法駐留到堆上的解決方案。一般Span<T>
由底層開發人員用在資料同步處理和轉換方面,而高層開發人員使用Memory<T>
比較多,因為它可以用於一些高階的場景,比如:非同步方法、類欄位、lambda表示式、泛型引數等等。兩者的完美運用就能夠支援不復制地流動資料,這就是資料管道應用場景(System.IO.Pipelines)。
到目前為止,作者花了三篇部落格終於把這兩個旗艦型別講完了,相信認真品讀這三篇部落格的同學,一定會受益匪淺。後面的系列將講兩者的高階應用場景,比如資料管道(Data Pipelines )、不連續緩衝區(Discontiguous Buffers)、緩衝池(Buffer Pooling)、以及為什麼讓Aspnet Core Web Server變得如此高效能等。
一圖勝千言:
最後
如果有什麼疑問和見解,歡迎評論區交流。
如果你覺得本篇文章對您有幫助的話,感謝您的【推薦】。
如果你對.NET高效能程式設計感興趣的話可以【關注我】,我會定期的在部落格分享我的學習心得。
歡迎轉載,請在明顯位置給出出處及連結。
延伸閱讀
https://en.wikipedia.org/wiki/Reference_counting
https://msdn.microsoft.com/en-us/magazine/mt814808
https://blogs.msdn.microsoft.com/oldnewthing/20040406-00/?p=39903
https://github.com/dotnet/corefxlab/blob/master/docs/specs/memory.md
https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1
https://docs.microsoft.com/zh-cn/dotnet/api/system.memory-1?view=netcore-2.2
https://blogs.msdn.microsoft.com/dotnet/2018/07/09/system-io-pipelines-high-performance-io-in-net
https://www.codemag.com/Article/1807051/Introducing-.NET-Core-2.1-Flagship-Types-Span-T-and-Memory-T