使用Span<T>优化代码(简单介绍)
作为一名程序员,应努力写出能提高整体应用性能的代码。在去年我刚上班时,四处寻找有没有高效处理字符串操作(如Substring()
、Split()
等)的方法,C中的Span<T>
类型给了我答案。所以,我想和大家分享一下它的一些基本细节。
什么是Span<T>
?
Span<T>
是C#7.2中引入的一种新类型,并在.NET Core 2.1得到支持。
Span<T>
能够表示任意内存的连续区域,无论该内存是与托管对象相关联,还是由本地代码通过互操作提供,或者是在堆栈上。而且它在这样做的同时,还能提供安全的访问,具有像数组那样的性能特性。
Span<T>
有一个相关类型ReadOnlySpan<T>
,它提供了内存数据的只读视图。ReadOnlySpan可以用来查看一个不可变类型(例如String
)所占用的内存。你可以把 Span<T>
看作是进入一些现有内存的窗口,不管它被分配到哪里。
Span<T>
被定义为 ref 结构,这意味着它被限制为只能在堆栈上分配。这就减少了一些潜在的用例,比如将它作为一个字段存储在类中,或者在异步方法中使用它。ref结构设计的主要原因是确保在使用Span<T>
时,我们不会造成额外的堆分配。这也是它支持编写高度优化代码的原因之一。
让我们看一些编码示例来理解Span<T>
的行为。
想象一下,你有一个像下面这样的表达式,你需要取出等号右边的值。
var str = "gender=female";//需取出female
为此,你可以写一个简单的方法,比如这样。
public string GetValueUsingSubstring(string expression)
{
var lastEqualSignIndex = expression.LastIndexOf('=', StringComparison.Ordinal);
return lastEqualSignIndex == -1
? string.Empty
: expression.Substring(lastEqualSignIndex + 1);
}
它的作用是,得到最后一个等号的索引,然后用它来得到代表值的子串。如果索引为-1,我们没有找到任何等号,所以该方法将返回一个空字符串作为我们的默认结果。如果我们确实找到了索引,那么该方法就会使用Substring
方法提取值并返回。
让我们使用Span<T>
来优化这段代码,而不是使用字符串操作。
public ReadOnlySpan<char> GetValueUsingSpan(ReadOnlySpan<char> expression)
{
var lastEqualSignIndex = expression.LastIndexOf('=');
return lastEqualSignIndex == -1
? ReadOnlySpan<char>.Empty
: expression.Slice(lastEqualSignIndex + 1);
}
请注意,优化后的方法,参数 "expression "现在的类型是ReadOnlySpan<char>
,返回类型现在也是ReadOnlySpan<char>
。
首先,和上面的代码一样,我们寻找等号的索引。同样,如果索引为-1,这意味着我们没有找到等号,该方法返回一个空的ReadOnlySpan<char>
结果。如果我们确实找到了一个等号,我们现在可以使用Span
的一个名为 Slicing
的功能。
Slicing
是一个强大的方法,我们可以将一个现有的Span
切成一个较小的窗口。切片时,我们可以定义切片的起始位置的索引,也可以定义切片的结束位置的长度。如果省略了长度,则会给出一个从起始位置到Span
结束的切片。
切片也是一种低成本的操作,因为我们没有复制任何东西。我们只是创建了一个新的Span
,它代表了现有内存范围子集的一个窗口。
为了更好的理解,请查看下图。
在这里,我们创建了一个原始Span
的Slice
,以查看其中的 "female" 值,而无需分配任何额外的原始内存副本。我们从最后一个等值字符后的索引开始取值的一个片断。由于我们没有指定长度,这个切片将运行到现有Span
的末端。一旦我们对原始Span
进行了分片,就会产生一个包含值的新Span
,我们将其作为方法的结果返回。
所以正如你所看到的,我们可以在任何需要提高性能的地方使用Span<T>
,并在需要的时候减少内存分配。虽然Span<T>
听起来有点复杂,但它的用法却非常简单。
希望你能从本文中获得一些关于Span<T>
的新知识。