英文原文:https://dev.to/fabriziobagala/span-memory-readonlysequence-in-net-383h
在 .NET 领域,一个关键方面是有效的数据操作。当涉及到内存管理时,这种能力更加重要。 Span<T>、Memory<T> 和 ReadOnlySequence<T> 结构提供了用于优化数据管理的高级工具。这些结构不仅提供了高度的控制性和灵活性,而且还大大提高了运营效率。
Span
提供任意内存的连续区域的类型安全和内存安全表示。
Span/ 是一个 ref struct ,因此,它是在栈上分配的,而不是在托管堆上分配的。此特定属性施加了某些限制,以确保它们不会提升到托管堆。这些约束包括防止它们被装箱、分配给类型为 Object 或动态或任何接口类型的变量、作为引用类型中的字段,以及它们跨await和yield生成边界使用。在 Span<T> 上调用 Equals(Object) 和 GetHashCode 等方法将引发 NotSupportedException。
鉴于其仅限栈的性质,Span<T> 可能不适合需要在堆上存储对缓冲区的引用的场景。此限制通常适用于进行异步方法调用的例程。在这些情况下,应考虑互补类型,例如 System.Memory<T> 和 System.ReadOnlyMemory<T>。
当处理不可变或只读结构时,使用 System.ReadOnlySpan<T> 更合适。
var array = new byte[100];
var span1 = new Span<byte>(array);
var span2 = array.AsSpan();
在此示例中,首先创建一个包含 100 个元素的字节数组。然后,使用两种不同的方法从此数组构造 Span<byte> 的两个实例。第一个实例是直接通过 Span<T> 构造函数创建的,第二个实例使用数组上可用的 AsSpan() 扩展方法。使用 AsSpan() 方法通常可以生成更具可读性和简洁性的代码,特别是在对Span内联执行其他操作时。
Memory
代表内存的连续区域。
与 Span<T> 类似,Memory<T> 表示连续的内存区域。然而,一个关键的区别在于它们的结构:与 Span<T> 不同,Memory<T> 不是 ref struct 。这种结构上的区别意味着 Memory<T> 可以驻留在托管堆上,这是与 Span<T> 不共享的功能。因此,Memory<T> 不受与 Span<T> 实例相同的约束的约束。具体来说:
- 它可以用作类中的字段。
- 它可以跨 await 和 yield 边界进行操作。
除了 Memory<T> 之外,System.ReadOnlyMemory<T> 可用于表示不可变或只读内存,使其成为处理不同数据存储需求的多功能工具。
var array = new byte[100];
var memory1 = new Memory<byte>(array);
var memory2 = array.AsMemory();
在此实例中,与前面的示例相同,我们首先创建一个包含 100 个元素的字节数组,然后从此数组创建两个 Memory<T> 实例。第一个实例是通过构造函数创建的,第二个实例是通过可读且简洁的 AsMemory() 扩展方法创建的。
ReadOnlySequence
表示一个序列,可以读取一系列连续的T。
ReadOnlySequence<T> 是一种专门设计用于管理非连续数据序列的结构,在处理分散在多个数组或缓冲区中的数据时提供了很大的实用性。本质上,它是一个序列,表示一个或多个内存对象的链接列表。如果您发现自己需要将多个内存片段视为单个实体,那么此数据结构是您的最佳选择。
var array = new byte[100];
var sequence = new ReadOnlySequence<byte>(array);
在此代码片段中,创建了一个字节数组,然后从此数组构造了一个 ReadOnlySequence<byte>。生成的 ReadOnlySequence<byte> 提供了字节数组的统一视图,就好像它是单个连续的切片一样。
基准测试
为了客观地评估这些结构的性能,我们可以使用像 BenchmarkDotNet 这样的基准测试框架。这样,我们就可以将 Span<T>、Memory<T> 和 ReadOnlySequence<T> 的性能与数组和列表进行比较。
[Config(typeof(MemoryBenchmarkConfig))]
public class MemoryBenchmark
{
private sealed class MemoryBenchmarkConfig : ManualConfig
{
public MemoryBenchmarkConfig()
{
SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend);
}
}
private const int OperationsPerInvoke = 4096;
private string _text = null!;
[GlobalSetup]
public void Setup()
{
_text = "Hello World";
}
[Benchmark(Baseline = true)]
public void ArrayRange()
{
_ = _text[4..];
}
[Benchmark]
public void ListRange()
{
_ = _text.ToList().GetRange(4, 7);
}
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
public void SpanSlice()
{
for (var i = 0; i < OperationsPerInvoke; i++)
{
_ = _text.AsSpan().Slice(4);
}
}
[Benchmark]
public void MemorySlice()
{
_ = _text.AsMemory().Slice(4);
}
[Benchmark]
public void ReadOnlySequenceSlice()
{
_ = new ReadOnlySequence<char>(_text.AsMemory()).Slice(4);
}
}
比较得出以下结果:
| Method | Mean | Error | StdDev | Ratio | RatioSD |
|---------------------- |------------:|----------:|----------:|--------------:|--------:|
| ArrayRange | 6.1415 ns | 0.0235 ns | 0.0220 ns | baseline | |
| ListRange | 102.9986 ns | 0.3432 ns | 0.3210 ns | 16.77x slower | 0.09x |
| SpanSlice | 0.5854 ns | 0.0009 ns | 0.0008 ns | 10.49x faster | 0.04x |
| MemorySlice | 0.8952 ns | 0.0029 ns | 0.0027 ns | 6.86x faster | 0.03x |
| ReadOnlySequenceSlice | 9.1725 ns | 0.0198 ns | 0.0185 ns | 1.49x slower | 0.01x |
在表中,ArrayRange 作为参考点,因为它具有本质和直接的性质。
ListRange 的性能远不如基线。这可能是由于列表固有的复杂性,虽然列表提供了很大的灵活性,但需要比数组更高的开销。
相比之下,SpanSlice 和 MemorySlice 在速度方面远远优于 ArrayRange。这表明它们对部分数据而不是整个集合进行操作的能力可以实现更高效、更快速的数据操作。
最后,ReadOnlySequenceSlice 在性能方面略低于 ArrayRange。这可能是因为 ReadOnlySequenceSlice 在数据安全方面具有优势,因为它可以防止不必要的修改。然而,这些优点在性能方面付出了很小的代价。
警告
请记住,您得到的值可能与表中给出的值不同。这取决于多种因素,例如输入字符串的长度和复杂性、运行代码的机器的硬件等。
结束语
.NET 提供的高级内存结构(例如 Span<T>、Memory<T> 和 ReadOnlySequence<T>)可以在处理连续或断开连接的数据时提供显着的效率和性能优势。熟悉这些结构以及有效利用它们的能力可能是旨在优化其应用程序的 .NET 开发人员的核心技能。