c#中的ReadOnlySequence<T>和ReadOnlySequenceSegment<T>

變形精怪發表於2020-09-16

關於.net core高效能程式設計中的Span<T>和Memory<T>網上資料很多,這裡就不說了。今天一直在看ReadOnlySequenceSegment<T>SequenceReader<T>,看得腦殼痛,本篇著重說說對ReadOnlySequenceSegment<T>的理解。

如果對Span<T>和Memory<T>不瞭解,可以暫時理解為byte[],最好先去搜下相關資料。緩衝區相關知識可以參考官方文件:https://docs.microsoft.com/zh-cn/dotnet/standard/io/buffers

記憶體片段ReadOnlySequenceSegment<T>

假設你已經瞭解了Memory<T>,它表示一段連續的記憶體,有時候我們讀取一條資料,它可能並不是存在連續的記憶體中。

這個我理解得不是很準確,但總體來說就是我們一個完整的資料分成了多個記憶體片段,每個記憶體片段用Memory<byte>(你也可以暫時理解為byte[])表示,那麼可以以連結串列的形式,從邏輯上來表示這段完整的資料。比如Memory1上有個next屬性指向Memory2,同理Memory2上的next屬性指向Memory3,這樣的連結串列就能表示這段完整的資料了。

ReadOnlySequenceSegment<T>就是這樣一個連結串列,3個核心屬性定義如下:

1 public ReadOnlyMemory<T> Memory { get; protected set; }
2 public ReadOnlySequenceSegment<T>? Next { get; protected set; }
3 public long RunningIndex { get; protected set; }
  • Memory:表示這個連結串列節點下的記憶體資料,也就是上面的Memory1、2、3
  • Next:就是指向的下一個節點
  • RunningIndex:指當前節點之前的節點的資料之和,比如Memory1裡有1個位元組、Memeory2裡有2個位元組,那麼Memory3對應節點的RunningIndex就是3

這玩意是個抽象類,不過暫時可以不關心,因為我們通常開發時都可以從某個方法的引數獲得ReadOnlySequenceSegment<T>(下面馬上會說),而它裡面就儲存著這個連結串列的收尾兩個節點。

這裡重點記住:

  • ReadOnlySequenceSegment裡面儲存的ReadOnlyMemory<T>(理解上約等於byte[])
  • 多個ReadOnlySequenceSegment可以組成一個連結串列,從邏輯上表示一個完整的資料,ReadOnlySequenceSegment只是其中一個節點

記憶體片段容器ReadOnlySequence<T>

上面說的這個記憶體片段連結串列其實已經可以從邏輯上表示一段完整的資料了,但是ReadOnlySequenceSegment<T>只是這個連結串列中的一個節點,它能提供的屬性、方法等api只能針對自己這個節點,所以需要一個容器來容納整個連結串列,以提供對此連續記憶體片段操作的api

這裡說的容器不是很準確,因為ReadOnlySequence只是儲存了整個連結串列的首位節點,但是由於是連結串列,其實只要知道首節點,就可以通過Next遞迴獲得整個連結串列的所有節點,因此我這裡把它稱為容器

下面引用官方文件的一張圖

 

綠色框中有3段藍色塊,我們可以理解為是連結串列中的一個節點(ReadOnlySequenceSegment),由於這個節點內部重要的就是儲存著具體的資料Memory<T>,所以我們可以簡單的看成是3個Memory<T>,這裡便於理解,也可以看成是3個byte[]。
根據綠色部分的3個不連續的記憶體片段,可以生成一個表示邏輯上連續的記憶體片段集合ReadOnlySequence,這個ReadOnlySequence包含3個Memory<T>,其中首位的片段只取原始片段的一部分。下面我根據理解再來一張圖

注:上面簡寫的16進位制,A=0x0A

連續記憶體片段中的索引SequencePosition

只要知道一個資料在哪個片段中,並且知道它在這個片段中的哪個位置,就能表示一個具體的索引了。

但特別注意這個索引是針對原始連結串列來說的,也就是上面綠色快的部分,比如圖片中的“4”在第1段的索引3的位置;“A”,在第2段的索引2處。這種情況沒有辦法用單個數字來表示索引,因此單獨定義了SequencePosition來表示索引。

ReadOnlySequence的api

  • 建構函式ReadOnlySequence(ReadOnlySequenceSegment<T> startSegment, int startIndex, ReadOnlySequenceSegment<T> endSegment, int endIndex)
    • startSegment:連結串列的首個節點
    • startIndex:首個節點不一定完全加入到ReadOnlySequence,此參數列示從第幾個值開始
    • endSegment:連結串列的尾節點
    • endIndex:尾節點也不一定完全加入ReadOnlySequence,此參數列示要加入的索引+1
    • 按上圖所示,程式碼應該這樣:new  ReadOnlySequence(片段1,3,片段3,1); 注意最後一個引數是1,可以簡單理解為在尾節點取前幾個值加入到ReadOnlySequence
  • End:就是最後一個片段的最後一個資料的索引物件,就是圖片中的片段3索引1
  • Start:第一個片段的索引,片段1,索引2
  • Length:ReadOnlySequence包含的值的長度,按圖中就是4 5 6 ....D F 2  長度為10
  • GetPosition(int index):獲取第幾個值的索引物件,比如GetPosition(0),那就是黃色塊的0為4,它所處於綠色塊的索引為:片段1,索引2;GetPosition(4),那就是黃色塊的2,所處綠色快的片段2,索引1
  • PositionOf(T value):查早某個值在這個序列中所處的索引,比如PositionOf(4),那就是在黃色塊的片段1的索引0處,最終結果就是綠色塊片段1的索引3處
  • Slice():從這個連續記憶體片段集合中指定索引處開始,取一段資料,返回的是一個新的ReadOnlySequence。有幾個過載,比較容易猜到它的意義
  •  bool TryGet(ref SequencePosition position, out ReadOnlyMemory<T> memory, bool advance = true) 

    嘗試從指定索引處開始讀取,所指定的索引處所在片段還有剩餘資料,則本次讀取這些剩餘資料,否則讀取下一個片段的資料。最終若讀取成功,則返回true,且將讀取到的資料賦值給memory引數。advance為true時,position將被直接賦值為下一個片段的索引0處。理解這個再看官方文件那個迴圈就容易了。

主要api就這幾個。

後續

即使自己造輪子時不在乎效能,在使用一些第三方庫時也可能會遇到此物件,對它有些瞭解的話不至於太迷茫。.net core中提供了System.Buffers名稱空間,裡面包含好幾個跟位元組陣列處理相關的類,後面學到哪裡就紀錄到哪裡。它是System.IO.Pipelines的基礎。而System.IO.Pipelines又是編寫高效能程式必不可少的玩意。

下一篇學完SequenceReader<T>再寫...它幫助我們更簡單的讀取ReadOnlySequence

 

相關文章