其實 C# 的 Span<T>
結構已經問世很多年了,直到昨天有同事問我這個東西要怎麼用,我就把多年前做好的簡報給他研究,然後順便複習一下這個厲害的玩意。
什麼是 Span<T>
結構
Span<T>
結構是一個泛型結構,它可以用來表示一個連續的記憶體區塊,而且它是一個可變動的結構,也就是說你可以透過它來修改記憶體中的資料。Span<T>
結構是在 .NET Core 2.1 ( C# 7.2 ) 中推出的,它的目的是為了提供一個高效率的記憶體存取方式,避免經常在 Heap 配置新的物件,我們可以在不需要額外配置記憶體的情況下,直接存取記憶體中的資料。由於 Span<T>
可以直接存取記憶體,減少無意義的記憶體取用,相對的也會大幅減少 GC (Garbage Collector) 的發生,這對於提高效能和降低記憶體用量非常有幫助。
若要在 .NET Framework 4.6.1+ 使用 Span<T>
型別,必須安裝 System.Memory 套件。
簡單來說,Span<T>
提供一個安全且有效率的方式存取記憶體!
- 安 全:宣告記憶體空間後就無法存取超出範圍的記憶體
- 有效率:減少配置記憶體的動作發生
幾個 Span<T>
使用範例
我用一段簡短的程式碼來說明 Span<T>
結構的用法,他可以將一個 int[]
轉成 Span<int>
結構:
// 建立一個整數陣列
int[] numbers = { 1, 2, 3, 4, 5 };
// 使用 Span<T> 建立一個切片(Slice)
Span<int> slice = numbers.AsSpan();
// 修改切片的內容,這也會修改原始陣列元素的內容 (因為直接存取記憶體的關係)
slice[2] = 99;
// 原始陣列的值也已經被修改
Console.WriteLine(numbers[2]); // 輸出 99
在 MemoryExtensions 靜態類別下有個 AsSpan()
擴充方法,支援將許多型別轉換成 Span<T>
結構。我再以一個 string
轉成 ReadOnlySpan<char>
的情境來說明:
// 建立一個字串
string name = "Will";
// 使用 .AsSpan() 建立一個 ReadOnlySpan<T> 唯讀切片(Slice)
ReadOnlySpan<char> slice = name.AsSpan();
// 直接存取記憶體,不用另外建立一個字串物件
Console.WriteLine(slice[0]); // 輸出 "W"
用 Span<T>
真的很快嗎?
我用 BenchmarkDotNet 對比了一下使用 Span<T>
切片與 String
的 Split()
方法的效能。
先看看我們用 String
的 Split()
方法的程式碼:
static string date = "2022-04-15";
[Benchmark]
public (string, string, string) ParseDateUsingSplit()
{
var y = date.Split("-")[0];
var m = date.Split("-")[1];
var d = date.Split("-")[2];
return (y, m, d);
}
再看看使用 Span<T>
切片的程式碼:
static string date = "2022-04-15";
[Benchmark]
public (string, string, string) ParseDateUsingReadOnlySpan()
{
ReadOnlySpan<char> nameAsSpan = date.AsSpan();
var y = nameAsSpan.Slice(0, 4);
var m = nameAsSpan.Slice(5, 2);
var d = nameAsSpan.Slice(8);
return (y.ToString(), m.ToString(), d.ToString());
}
以下是跑完效能比較的結果,使用 ReadOnlySpan<char>
的執行效率整整快了 4.7 倍:
不過,如果我拿掉回傳字串的寫法,單純的比較字串處理效率的話,ReadOnlySpan<char>
的效率將會比 Split
操作快了 557
倍!
static string date = "2024-09-19";
[Benchmark]
public void ParseDateUsingSplit()
{
var y = date.Split("-")[0];
var m = date.Split("-")[1];
var d = date.Split("-")[2];
}
[Benchmark]
public void ParseDateUsingReadOnlySpan()
{
ReadOnlySpan<char> nameAsSpan = date.AsSpan();
var y = nameAsSpan.Slice(0, 4);
var m = nameAsSpan.Slice(5, 2);
var d = nameAsSpan.Slice(8);
}
經過網友 土阿 的提醒,我加入了 Substring()
的效能評比,結果意外的發現,使用 Substring()
的處理方法,竟然比 ReadOnlySpan<char>
的 ToString()
還快,還快了 1.26
倍!
[Benchmark]
public (string, string, string) ParseDateReturnString_UsingSubstring()
{
var y = date.Substring(0, 4);
var m = date.Substring(5, 2);
var d = date.Substring(8, 2);
return (y, m, d);
}
[Benchmark]
public (string, string, string) ParseDateReturnString_UsingReadOnlySpan()
{
ReadOnlySpan<char> nameAsSpan = date.AsSpan();
var y = nameAsSpan.Slice(0, 4);
var m = nameAsSpan.Slice(5, 2);
var d = nameAsSpan.Slice(8);
return (y.ToString(), m.ToString(), d.ToString());
}
我想應該是 ParseDateReturnString_UsingReadOnlySpan()
最後的輸出的 3 個字串,因為需要將 ReadOnlySpan<char>
轉成 3 個新的字串,需要配置全新的記憶體空間,最終導致了效能變差的結果!
我們之所以要用 Span 或 ReadOnlySpan 結構,其目的不外乎就是有效率的去處理一段連續的記憶體區塊,處理的過程是非常快的,我們從上述的效能檢測看的出來,但是最終如果又會產生新的記憶體空間,那麼效能就會變差,所以我們在使用 Span 結構的時候,一定要注意不要產生新的記憶體空間,否則就會失去效能的優勢。
接著,我又調整了一下寫法如下,利用 Int32.Parse()
方法,將字串轉成 Int32 型別。這次的評測結果就比較符合預期了,使用 ReadOnlySpan<char>
的版本快了 1.67
倍:
[Benchmark]
public (int, int, int) ParseDateReturnInt32_UsingSubstring()
{
var y = date.Substring(0, 4);
var m = date.Substring(5, 2);
var d = date.Substring(8, 2);
return (Int32.Parse(y), Int32.Parse(m), Int32.Parse(d));
}
[Benchmark]
public (int, int, int) ParseDateReturnInt32_UsingReadOnlySpan()
{
ReadOnlySpan<char> nameAsSpan = date.AsSpan();
var y = nameAsSpan.Slice(0, 4);
var m = nameAsSpan.Slice(5, 2);
var d = nameAsSpan.Slice(8);
// 注意: Int32.Parse() 方法也有支援 ReadOnlySpan<char> 的重載方法
return (Int32.Parse(y), Int32.Parse(m), Int32.Parse(d));
}
由於 ParseDateReturnInt32_UsingReadOnlySpan()
方法,在處理字串時,一直都沒有產生新的字串物件,因此記憶體使用率較佳,效能也較好。
總結
自從 .NET Core 2.1 開始,之所以 .NET Core 在效能上能有大幅改進,其中 ref struct types 與 Span 的出現功不可沒,事實上 .NET 的 BCL (Base Class Library) 已經有許多方法都採用了 Span<T>
與 ReadOnlySpan<T>
結構,例如:
所以之後有機會遇到陣列或字串操作,都可以先想到用 Span<T>
來處理,相信你會有意想不到的效能提升!
Span 還有許多的跟記憶體相關的開發技巧,如果有興趣深入研究,可以參考本文的相關連結。
相關連結
- Microsoft Learn
- NDepend
- CODE Magazine
- Medium
- 博客园