1 文章范圍
本文將.netcore新出現(xiàn)的與Buffer操作相關(guān)的類型進行簡單分析與講解,由于資料有限,一些見解為個人見解,可能不是很準(zhǔn)確。這些新類型將包括BinaryPrimitives、Span<>,Memory<>,ArrayPool<>,Memorypool<>
2 BinaryPrimitives
在網(wǎng)絡(luò)傳輸中,最小單位是byte,很多場景,我們需要將int long
short等類型與byte[]相互轉(zhuǎn)換。比如,將int轉(zhuǎn)換為BigEndian的4個字節(jié),在過去,我們很容易就想到BitConverter,但BitConverter設(shè)計得不夠好友,BitConverter.GetBytes(int
value)得到的byte[]的字節(jié)順序永遠與主機的字節(jié)順序一樣,我們不得不再根據(jù)BitConverter的IsLittleEndian屬性判斷是否需要對得到byte[]進行轉(zhuǎn)換字節(jié)順序,而BinaryPrimitives的Api設(shè)計為嚴(yán)格區(qū)分Endian,每個Api都指定了目標(biāo)Endian。
BitConverter
var intValue = 1; var bytes = BitConverter.GetBytes(intValue); if
(BitConverter.IsLittleEndian == true) { Array.Reverse(bytes); }
BinaryPrimitives
var intValue = 1; var bytes = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32BigEndian(bytes, intValue);
3 Span<>
Span是一個高效的連續(xù)內(nèi)存范圍操作值類型,我們知道Array
是一個連接的內(nèi)存范圍的引用類型,那為什么還需要Span類型呢?可以簡單這么認(rèn)為:Span除了提供更高性能的Array的讀寫功能之外,還提供了比ArraySegment更易于理解和使用的內(nèi)存局部視圖,也就是說Span功能包含了Array+ArraySegment的功能,我可以使用BenchmarkDotNet對比Span、Array和指針讀寫一個連接內(nèi)存的性能比較,測試結(jié)果為Span>Pointer>Array:
讀寫代碼
public class DemoContext { private byte[] array = new byte[1024]; [Benchmark]
public void ByteArray() { for (var i = 0; i < array.Length; i++) { array[i] =
array[i]; } } [Benchmark] public void ByteSpan() { var span = array.AsSpan();
for (var i = 0; i < span.Length; i++) { span[i] = span[i]; } } [Benchmark]
unsafe public void BytePointer() { fixed (byte* pointer = &array[0]) { for (var
i = 0; i < array.Length; i++) { *(pointer + i) = *(pointer + i); } } } }
Benchmark報告
| Method | Mean | Error | StdDev | |------------
|---------:|--------:|--------:| | ByteArray | 577.4 ns | 9.07 ns | 8.48 ns | |
ByteSpan | 323.8 ns | 0.87 ns | 0.81 ns | | BytePointer | 499.4 ns | 4.09 ns |
3.82 ns |
Memory<>
如果嘗試將Span<>作為全局變量,或在異步方法聲明為變量,你會得到編譯器的錯誤,原因不在本文講解范圍內(nèi),而Memory<>類型可以滿足這些需求,Memory<>提供了用于數(shù)據(jù)讀寫的Span屬性,這個Span屬性是每將獲取時都有一些計算,所以我們應(yīng)該盡量避免多次獲取它的Span屬性。
合理的獲取Span
var span = memory.Span; for (var i = 0; i < span.Length; i++) { span[i] =
span[i]; }
不合理的獲取Span
for (var i = 0; i < memory.Length; i++) { memory.Span[i] = memory.Span[i]; }
Benchmark報告
| Method | Mean | Error | StdDev | |------------
|-----------:|---------:|---------:| | ByteMemory1 | 325.8 ns | 1.03 ns | 0.97
ns | | ByteMemory2 | 3,344.9 ns | 11.91 ns | 11.14 ns |
ArrayPool<>
ArrayPool<>用于解決頻繁申請內(nèi)存和釋放內(nèi)存導(dǎo)致GC壓力過大的場景,比如System.Text.Json在序列對象時為utf8的byte[]時,事先是無法計算最終byte[]的長度的,過程中可能要不斷申請和調(diào)整緩沖區(qū)的大小。在沒有ArrayPool加持的情況下,高頻次的序列化,則會生產(chǎn)高頻創(chuàng)建byte[]的過程,隨之GC壓力也會增大。ArrayPool的設(shè)計邏輯是,從pool申請一個指定最小長度的緩沖區(qū),緩沖區(qū)在不需要的時候,將其返回到pool里,待以重復(fù)利用。
var pool = ArrayPool<byte>.Shared; var buffer = pool.Rent(1024); // 開始利用buffer
// ... // 使用結(jié)束 pool.Return(buffer);
Rent用于申請,實際上是租賃,Return是歸還,返回到池中。我們可以使用IDisposable接口來包裝Return功能,使用上更方便一些:
/// <summary> /// 定義數(shù)組持有者的接口 /// </summary> /// <typeparam
name="T"></typeparam> public interface IArrayOwner<T> : IDisposable { ///
<summary> /// 獲取持有的數(shù)組 /// </summary> T[] Array { get; } /// <summary> ///
獲取數(shù)組的有效長度 /// </summary> int Count { get; } } /// <summary> /// 表示共享的數(shù)組池 ///
</summary> public static class ArrayPool { /// <summary> /// 租賃數(shù)組 ///
</summary> /// <typeparam name="T">元素類型</typeparam> /// <param
name="minLength">最小長度</param> /// <returns></returns> public static
IArrayOwner<T> Rent<T>(int minLength) { return new ArrayOwner<T>(minLength); }
/// <summary> /// 表示數(shù)組持有者 /// </summary> /// <typeparam name="T"></typeparam>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(ArrayOwnerDebugView<>))] private class ArrayOwner<T>
:IDisposable, IArrayOwner<T> { /// <summary> /// 獲取持有的數(shù)組 /// </summary> public
T[] Array { get; } /// <summary> /// 獲取數(shù)組的有效長度 /// </summary> public int Count
{ get; } /// <summary> /// 數(shù)組持有者 /// </summary> /// <param
name="minLength"></param> public ArrayOwner(int minLength) { this.Array =
ArrayPool<T>.Shared.Rent(minLength); this.Count = minLength; } /// <summary>
/// 歸還數(shù)組 /// </summary> Public void Dispose() {
ArrayPool<T>.Shared.Return(this.Array); } } /// <summary> /// 調(diào)試視圖 ///
</summary> /// <typeparam name="T"></typeparam> private class
ArrayOwnerDebugView<T> { [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public T[] Items { get; } /// <summary> /// 調(diào)試視圖 /// </summary> /// <param
name="owner"></param> public ArrayOwnerDebugView(IArrayOwner<T> owner) {
this.Items = owner.Array.AsSpan(0, owner.Count).ToArray(); } } }
改造之后的使用
using var buffer = ArrayPool.Rent<byte>(1024); // 盡情的使用buffer吧,自動回收
Memorypool<>
Memorypool<>本質(zhì)上還是使用了ArrayPool<>,Memorypool只提供了Rent功能,返回一個IMomoryOwner<>,對其Dispose等同于Return過程,使用方式和我們上面改造過的ArrayPool靜態(tài)類的使用方式是一樣的。
MemoryMarshal靜態(tài)類
MemoryMarshal是一個工具類,類似于我們指針操作時常常用到的Marshal類,它操作一些更底層的Span或Memory操作,比如提供將不同基元類型的Span相互轉(zhuǎn)換等。
獲取Span的指針
var span = new Span<byte>(new byte[] { 1, 2, 3, 4 }); ref var p0 = ref
MemoryMarshal.GetReference(span); fixed (byte* pointer = &p0) {
Debug.Assert(span[0] == *pointer); }
Span泛型參數(shù)類型轉(zhuǎn)換
Span<int> intSpan = new Span<int>(new int[] { 1024 }); Span<byte> byteSpan =
MemoryMarshal.AsBytes(intSpan);
ReadonlyMemory<>轉(zhuǎn)換為Memory
// 相當(dāng)于給ReadonlyMemory移除只讀功能 Memory<T>
MemoryMarshal.AsMemory<T>(ReadonlyMemory<T> readonly)
熱門工具 換一換