目录
介绍
Span实现
Ref返回
Ref结构
Span实现
Span限制
重写现有代码库以使用Span
评价
结论
介绍
以我的经验,提高应用程序性能的主要步骤是减少IO调用的次数和持续时间。但是,一旦执行了此选项,开发人员所采取的另一条路径就是使用堆栈上的内存。堆栈允许非常快速的分配和重新分配,尽管由于堆栈大小很小,堆栈仅应用于分配较小的部分。另外,使用堆栈可以减少GC的压力。为了在堆栈上分配内存,一种方法是将值类型或stackalloc运算符与非托管内存的使用结合使用。
由于用于非托管内存访问的API非常冗长,因此开发人员很少使用第二个选项。
Span是C#7.2中提供的一组值类型,它是来自不同来源的内存的无分配表示形式。Span允许开发人员以更方便的方式使用连续内存区域,从而确保内存和类型安全。
Span实现 Ref返回Span对于那些不密切关注C#语言更新的用户,实现的第一步是了解C#7.0中引入的ref返回。
虽然大多数读者都熟悉按引用传递方法参数,但是现在C#允许返回对值的引用,而不是值本身。
让我们检查一下它是如何工作的。我们将围绕着一系列杰出的音乐家创建一个简单的包装,既展示传统行为又展示新的参考返回功能。
public class ArtistsStore
{
private readonly string[] _artists =
new[] { "Amenra", "The Shadow Ring", "Hiroshi Yoshimura" };
public string ReturnSingleArtist()
{
return _artists[1];
}
public ref string ReturnSingleArtistByRef()
{
return ref _artists[1];
}
public string AllAritsts => string.Join(", ", _artists);
}
现在让我们调用这些方法
var store = new ArtistsStore();
var artist = store.ReturnSingleArtist();
artist = "Henry Cow";
var allArtists = store.AllAritsts; //Amenra, The Shadow Ring, Hiroshi Yoshimura
artist = store.ReturnSingleArtistByRef();
artist = "Frank Zappa";
allArtists = store.AllAritsts; //Amenra, The Shadow Ring, Hiroshi Yoshimura
ref var artistReference = ref store.ReturnSingleArtistByRef();
artistReference = "Valentyn Sylvestrov";
allArtists = store.AllAritsts; //Amenra, Valentyn Sylvestrov, Hiroshi Yoshimura
请注意,在第一个和第二个示例中,原始集合未更改,而在最后一个示例中,我们设法更改了该集合的第二个艺术家。如您在本文后面所看到的那样,此有用的功能将帮助我们以类似引用的方式操作位于堆栈上的数组。
众所周知,值类型可能在堆栈上分配。同样,它们不一定取决于使用该值的上下文。为了确保始终在堆栈上分配该值,在C#7.0中引入了ref struct的概念。Span是一个ref struct,因此我们确定它总是分配在堆栈上。
Span实现Span是一个ref struct,其中包含指向内存的指针和跨度的长度,如下所示。
public readonly ref struct Span
{
private readonly ref T _pointer;
private readonly int _length;
public ref T this[int index] => ref _pointer + index;
...
}
注意指针字段附近的ref修饰符。不能在.NET Core的普通C#中声明这种构造,而是通过ByReference来实现的。
如您所见,索引是通过ref return实现的,它允许只支持堆栈的struct使用类似引用类型的行为。
Span限制为了确保ref struct总是在堆栈使用的,它具有许多局限性,即,包括它们不能被装箱,它们不能被分配给object,dynamic类型的变量或任何接口类型,它们不能是引用类型字段,并且它们不能跨await和yield边界使用。另外,调用Equals和GetHashCode两个方法,并抛出NotSupportedException。Span是一个ref struct。
重写现有代码库以使用Span让我们研究一下将Linux权限转换为八进制表示形式的代码。您可以在此处访问它。这是原始代码:
internal class SymbolicPermission
{
private struct PermissionInfo
{
public int Value { get; set; }
public char Symbol { get; set; }
}
private const int BlockCount = 3;
private const int BlockLength = 3;
private const int MissingPermissionSymbol = '-';
private readonly static Dictionary Permissions =
new Dictionary() {
{0, new PermissionInfo {
Symbol = 'r',
Value = 4
} },
{1, new PermissionInfo {
Symbol = 'w',
Value = 2
}},
{2, new PermissionInfo {
Symbol = 'x',
Value = 1
}} };
private string _value;
private SymbolicPermission(string value)
{
_value = value;
}
public static SymbolicPermission Parse(string input)
{
if (input.Length != BlockCount * BlockLength)
{
throw new ArgumentException
("input should be a string 3 blocks of 3 characters each");
}
for (var i = 0; i < input.Length; i++)
{
TestCharForValidity(input, i);
}
return new SymbolicPermission(input);
}
public int GetOctalRepresentation()
{
var res = 0;
for (var i = 0; i < BlockCount; i++)
{
var block = GetBlock(i);
res += ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
}
return res;
}
private static void TestCharForValidity(string input, int position)
{
var index = position % BlockLength;
var expectedPermission = Permissions[index];
var symbolToTest = input[position];
if (symbolToTest != expectedPermission.Symbol &&
symbolToTest != MissingPermissionSymbol)
{
throw new ArgumentException($"invalid input in position {position}");
}
}
private string GetBlock(int blockNumber)
{
return _value.Substring(blockNumber * BlockLength, BlockLength);
}
private int ConvertBlockToOctal(string block)
{
var res = 0;
foreach (var (index, permission) in Permissions)
{
var actualValue = block[index];
if (actualValue == permission.Symbol)
{
res += permission.Value;
}
}
return res;
}
}
public static class SymbolicUtils
{
public static int SymbolicToOctal(string input)
{
var permission = SymbolicPermission.Parse(input);
return permission.GetOctalRepresentation();
}
}
推理非常简单:string是一个char数组,所以为什么不将其分配在堆栈上而不是堆上。
因此,我们的第一个目标是标记SymbolicPermission的_value字段作为ReadOnlySpan,而不是string。为此,我们必须声明SymbolicPermission为ref struct字段或属性不能是Span类型,除非它是ref struct的实例。
internal ref struct SymbolicPermission
{
...
private ReadOnlySpan _value;
}
现在我们只需将我们所能到达的每个string更改为ReadOnlySpan。惟一感兴趣的是GetBlock方法,因为在这里我们用Slice替换了Substring。
private ReadOnlySpan GetBlock(int blockNumber)
{
return _value.Slice(blockNumber * BlockLength, BlockLength);
}
评价
让我们来衡量结果:
我们注意到速度提高了50纳秒,大约是性能提高了10%。可以说50纳秒不算什么,但是实现它几乎不花时间!
现在,我们将评估具有18个块(每个字符12个字符)的权限上的改进,以查看我们是否可以获得显着的改进。
如您所见,我们设法获得了0.5微秒或5%的性能提升。同样,它看起来像是一个微不足道的成就。但是记住,这是很容易实现的。
结论Span提供了一种安全且易于使用的替代方案stackallock,其可轻松提高性能。虽然从每次使用中获得的收益相对较小,但始终如一的使用却可以避免因数千次切割而导致的死亡。Span在.NET Core 3.0代码库中广泛使用,与以前的版本相比,它可以提高性能。