[譯]C# 7系列,Part 10: Span and universal memory management Span 和統一內存管理

原文:https://blogs.msdn.microsoft.com/mazhou/2018/03/25/c-7-series-part-10-spant-and-universal-memory-management/html

譯註:這是本系列最後一篇文章api

背景數組

.NET是一個託管平臺,這意味着內存訪問和管理是安全的、自動的。全部類型都是由.NET徹底管理的,它在執行棧或託管堆上分配內存。安全

在互操做的事件或低級別開發中,你可能但願訪問本機對象和系統內存,這就是爲何會有互操做這部分了,有一部分類型能夠封送進入本機世界,調用本機api,轉換託管/本機類型和在託管代碼中定義一個本機結構。網絡

問題1:內存訪問模式框架

在.NET世界中,你可能會對3種內存類型感興趣。異步

  • 託管堆內存,如數組;
  • 棧內存,如使用stackalloc建立的對象;
  • 本機內存,例如本機指針引用。

上面每種類型的內存訪問可能須要使用爲它設計的語言特性:async

  • 要訪問堆內存,請在支持的類型(如字符串)上使用fixed(固定)指針,或者使用其餘能夠訪問它的適當.NET類型,如數組或緩衝區;
  • 要訪問堆棧內存,請使用stackalloc建立指針;
  • 要訪問非託管系統內存,請使用Marshal api建立指針。

你看,不一樣的訪問模式須要不一樣的代碼,對於全部連續的內存訪問沒有單一的內置類型。函數

問題2:性能性能

在許多應用程序中,最消耗CPU的操做是字符串操做。若是你對你的應用程序運行一個分析器會話,你可能會發現95%的CPU時間都用於調用字符串和相關函數。

Trim、IsNullOrWhiteSpace和SubString多是最經常使用的字符串api,它們也很重:

  • Trim()或SubString()返回一個新的字符串對象,該對象是原始字符串的一部分,若是有辦法切片並返回原始字符串的一部分來保存一個副本,其實沒有必要這樣作。
  • IsNullOrWhiteSpace()獲取一個須要內存拷貝的字符串對象(由於字符串是不可變的)。
  • 特別的,字符串鏈接很昂貴(譯註:指消耗不少CPU),須要n個字符串對象,產生n個副本,生成n-1個臨時字符串對象,並返回一個字符串對象,那n-1個副本本能夠排除的若是有辦法直接訪問返回字符串內存和執行順序寫入。

Span<T>

System.Span<T>是一個只在棧上的類型(ref struct),它封裝了全部的內存訪問模式,它是一種用於通用連續內存訪問的類型。你能夠認爲Span<T>的實現包含一個虛擬引用和一個長度,接受所有3種內存訪問類型。

你可使用Span<T>的構造函數重載或來自數組、stackalloc的指針和非託管指針的隱式操做符來建立Span<T>。

// 使用隱式操做 Span<char>(char[])。
Span<char> span1 = new char[] { 's', 'p', 'a', 'n' }; // 使用stackalloc。
Span<byte> span2 = stackalloc byte[50]; // 使用構造函數。
IntPtr array = new IntPtr(); Span<int> span3 = new Span<int>(array.ToPointer(), 1);

一旦你有了一個Span<T>對象,你能夠用指定的索引來設置值,或者返回Span的一部分:

// 建立一個實例:
Span<char> span = new char[] { 's', 'p', 'a', 'n' }; // 訪問第一個元素的引用。
ref char first = ref span[0]; // 給引用設置一個新的值。
first = 'S'; // 新的字符串"Span".
Console.WriteLine(span.ToArray());
// 返回一個新的span從索引1到末尾. // 獲得"pan"。
Span<char> span2 = span.Slice(1); Console.WriteLine(span2.ToArray());

你可使用Slice()方法編寫一個高性能Trim()方法:

private static void Main(string[] args) { string test = " Hello, World! "; Console.WriteLine(Trim(test.ToCharArray()).ToArray()); } private static Span<char> Trim(Span<char> source) { if (source.IsEmpty) { return source; } int start = 0, end = source.Length - 1; char startChar = source[start], endChar = source[end]; while ((start < end) && (startChar == ' ' || endChar == ' ')) { if (startChar == ' ') { start++; } if (endChar == ' ') { end—; } startChar = source[start]; endChar = source[end]; } return source.Slice(start, end - start + 1); }

上面的代碼不復制字符串,也不生成新的字符串,它經過調用Slice()方法返回原始字符串的一部分。

由於Span<T>是一個ref結構,因此全部的ref結構限制都適用。也就是說,你不能在字段、屬性、迭代器和異步方法中使用Span<T>。

Memory<T>

System.Memory<T>是一個System.Span<T>的包裝。使其在迭代器和異步方法中可訪問。使用Memory<T>上的Span屬性來訪問底層內存,這在異步場景中很是有用,好比文件流和網絡通訊(HttpClient等)。

下面的代碼展現了這種類型的簡單用法。

private static async Task Main(string[] args) { Memory<byte> memory = new Memory<byte>(new byte[50]); int count = await ReadFromUrlAsync("https://www.microsoft.com", memory).ConfigureAwait(false); Console.WriteLine("Bytes written: {0}", count); } private static async ValueTask<int> ReadFromUrlAsync(string url, Memory<byte> memory) { using (HttpClient client = new HttpClient()) { Stream stream = await client.GetStreamAsync(new Uri(url)).ConfigureAwait(false); return await stream.ReadAsync(memory).ConfigureAwait(false); } }

框架類庫/核心框架(FCL/CoreFx)將在.NET Core 2.1中爲流、字符串等添加基於類Span類型的api。

ReadOnlySpan<T> 和 ReadOnlyMemory<T>

System.ReadOnlySpan<T>是System.Span<T>的只讀版本。其中,索引器返回一個只讀的ref對象,而不是ref對象。在使用System.ReadOnlySpan<T>這個只讀的ref結構時,你能夠得到只讀的內存訪問權限

這對於string類型很是有用,由於string是不可變的,因此它被視爲只讀的span。

咱們能夠重寫上面的代碼來實現Trim()方法,使用ReadOnlySpan<T>:

private static void Main(string[] args) { // Implicit operator ReadOnlySpan(string).
    ReadOnlySpan<char> test = " Hello, World! "; Console.WriteLine(Trim(test).ToArray()); } private static ReadOnlySpan<char> Trim(ReadOnlySpan<char> source) { if (source.IsEmpty) { return source; } int start = 0, end = source.Length - 1; char startChar = source[start], endChar = source[end]; while ((start < end) && (startChar == ' ' || endChar == ' ')) { if (startChar == ' ') { start++; } if (endChar == ' ') { end—; } startChar = source[start]; endChar = source[end]; } return source.Slice(start, end - start + 1); }

如你所見,方法體中沒有任何更改;我只是將參數類型從Span<T>更改成ReadOnlySpan<T>,並使用隱式操做符將字符串直接轉換爲ReadOnlySpan<char>。

Memory擴展方法

System.MemoryExtensions類包含針對不一樣類型的擴展方法,這些方法使用span類型進行操做,下面是經常使用的擴展方法列表,其中許可能是使用span類型的現有api的等效實現。

  • AsSpan, AsMemory:將數組轉換成Span<T>或Memory<T>或它們的只讀副本。
  • BinarySearch, IndexOf, LastIndexOf:搜索元素和索引。
  • IsWhiteSpace, Trim, TrimStart, TrimEnd, ToUpper, ToUpperInvariant, ToLower, ToLowerInvariant:相似字符串的Span<char>操做。

內存封送

在某些狀況下,你可能但願對內存類型和系統緩衝區有較低級別的訪問權限,並在span和只讀span之間進行轉換。System.Runtime.InteropServices.MemoryMarshal靜態類提供了此類功能,容許你控制這些訪問場景。下面的代碼展現了使用span類型來作首字母大寫,這個實現性能高,由於沒有臨時的字符串分配。

private static void Main(string[] args) { string source = "span like types are awesome!"; // source.ToMemory() 轉換變量 source 從字符串類型爲 ReadOnlyMemory<char>, // and MemoryMarshal.AsMemory 轉換 ReadOnlyMemory<char> 爲 Memory<char> // 這樣你就能夠修改元素了。
 TitleCase(MemoryMarshal.AsMemory(source.AsMemory())); // 獲得 "Span like types are awesome!";
 Console.WriteLine(source); } private static void TitleCase(Memory<char> memory) { if (memory.IsEmpty) { return; } ref char first = ref memory.Span[0]; if (first >= 'a' && first <= 'z') { first = (char)(first - 32); } }

結論

Span<T>和Memory<T>支持以統一的方式訪問連續內存,而無論內存是如何分配的。它對本地開發場景以及高性能場景很是有幫助。特別是,在使用span類型處理字符串時,你將得到顯著的性能改進。這是C# 7.2中一個很是好的創新特性。

注意:要使用此功能,你須要使用Visual Studio 2017.5和C#語言版本7.2或最新版本。

 

系列文章:

相關文章
相關標籤/搜索