
Span <T>用法示例
一个人天生就无法完全理解某种乐器的目的,除非他或她获得一些经验。 因此,让我们来看一些例子。
ValueStringBuilder
就算法而言,最有趣的示例之一是ValueStringBuilder
类型。 但是,它被深埋在mscorlib的internal
并用internal
修饰符标记为其他许多非常有趣的数据类型。 这意味着,如果我们没有研究mscorlib源代码,就不会找到这种出色的优化工具。
StringBuilder
系统类型的主要缺点是什么? 它的主要缺点是类型及其基础-它是引用类型,并基于char[]
(即字符数组)。 至少,这意味着两件事:无论如何我们都使用堆(尽管不多),并增加了错过CPU现金的机会。
我遇到的StringBuilder
另一个问题是小字符串的构造,即生成的字符串必须短(例如少于100个字符)时。 短格式会引起性能问题。
本章由作者和专业翻译员共同译自俄语。 您可以帮助我们将俄语或英语翻译成任何其他语言,主要是中文或德语。
另外,如果您想感谢我们,最好的方法是在github上给我们加星号或分支存储库
github / sidristij / dotnetbook
$"{x} is in range [{min};{max}]"
与通过StringBuilder
手动构建相比,此变体在多大程度上糟糕? 答案并不总是很明显。 它取决于构造的位置和调用此方法的频率。 最初, string.Format
为内部StringBuilder
分配内存,该内存将创建字符数组(SourceString.Length + args.Length * 8)。 如果在数组构造过程中发现长度确定不正确,则将创建另一个StringBuilder
来构造其余部分。 这将导致创建单个链接列表。 结果,它必须返回构造的字符串,这意味着要进行另一次复制。 那是浪费。 如果我们可以摆脱在堆上分配已形成字符串的数组的问题,那就太好了:这将解决我们的问题之一。
让我们从mscorlib
的深度来看这种类型:
ValueStringBuilder类
/ src / mscorlib /共享/系统/文本/ ValueStringBuilder
internal ref struct ValueStringBuilder { // this field will be active if we have too many characters private char[] _arrayToReturnToPool; // this field will be the main private Span<char> _chars; private int _pos; // the type accepts the buffer from the outside, delegating the choice of its size to a calling party public ValueStringBuilder(Span<char> initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public int Length { get => _pos; set { int delta = value - _pos; if (delta > 0) { Append('\0', delta); } else { _pos = value; } } } // Here we get the string by copying characters from the array into another array public override string ToString() { var s = new string(_chars.Slice(0, _pos)); Clear(); return s; } // To insert a required character into the middle of the string //you should add space into the characters of that string and then copy that character public void Insert(int index, char value, int count) { if (_pos > _chars.Length - count) { Grow(count); } int remaining = _pos - index; _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); _chars.Slice(index, count).Fill(value); _pos += count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { int pos = _pos; if (pos < _chars.Length) { _chars[pos] = c; _pos = pos + 1; } else { GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } // If the original array passed by the constructor wasn't enough // we allocate an array of a necessary size from the pool of free arrays // It would be ideal if the algorithm considered // discreteness of array size to avoid pool fragmentation. [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int requiredAdditionalCapacity) { Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos); char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); _chars.CopyTo(poolArray); char[] toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Clear() { char[] toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } // Missing methods: the situation is crystal clear private void AppendSlow(string s); public bool TryCopyTo(Span<char> destination, out int charsWritten); public void Append(string s); public void Append(char c, int count); public unsafe void Append(char* value, int length); public Span<char> AppendSpan(int length); }
此类在功能上类似于其高级StringBuilder
,但具有一个有趣且非常重要的功能:它是一种值类型。 这意味着它完全按值存储和传递。 另外,作为类型声明签名一部分的新ref
类型修饰符表示该类型还有一个附加约束:只能在堆栈上分配它。 我的意思是将其实例传递给类字段将产生错误。 这些东西是干什么用的? 要回答这个问题,您只需要查看StringBuilder
类,我们已经描述了其本质:
StringBuilder类 /src/mscorlib/src/System/Text/StringBuilder.cs
public sealed class StringBuilder : ISerializable { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, // so that is what we do. internal char[] m_ChunkChars; // The characters in this block internal StringBuilder m_ChunkPrevious; // Link to the block logically before this block internal int m_ChunkLength; // The index in m_ChunkChars that represent the end of the block internal int m_ChunkOffset; // The logical offset (sum of all characters in previous blocks) internal int m_MaxCapacity = 0; // ... internal const int DefaultCapacity = 16;
StringBuilder
是一个类,其中包含对字符数组的引用。 因此,当您创建它时,实际上会出现两个对象: StringBuilder
和至少16个字符的字符数组。 这就是为什么必须设置字符串的预期长度的原因:它将通过生成单个链接列表(每个数组包含16个字符)来构建。 承认,那是浪费。 就ValueStringBuilder
类型而言,这意味着没有默认capacity
,因为它借用了外部内存。 另外,它是一个值类型,它使用户可以为堆栈上的字符分配缓冲区。 因此,将类型的整个实例及其内容一起放入堆栈中,从而解决了优化问题。 由于无需在堆上分配内存,因此在处理堆时不会出现性能下降的问题。 因此,您可能会有一个问题:为什么我们不总是使用ValueStringBuilder
(或它的自定义类似物,因为我们不能使用原始物,因为它是内部的)? 答案是:这取决于任务。 结果字符串是否具有确定的大小? 它具有已知的最大长度吗? 如果回答“是”,并且字符串没有超出合理范围,则可以使用StringBuilder
的值版本。 但是,如果希望使用较长的字符串,请使用通常的版本。
ValueListBuilder
internal ref partial struct ValueListBuilder<T> { private Span<T> _span; private T[] _arrayFromPool; private int _pos; public ValueListBuilder(Span<T> initialSpan) { _span = initialSpan; _arrayFromPool = null; _pos = 0; } public int Length { get; set; } public ref T this[int index] { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(T item); public ReadOnlySpan<T> AsSpan(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose(); private void Grow(); }
我特别要注意的ValueListBuilder
类型是ValueListBuilder
类型。 当您需要在短时间内创建元素集合并将其传递给算法进行处理时,可以使用它。
承认,此任务看起来与ValueStringBuilder
任务非常相似。 可以通过类似的方式解决:
文件ValueListBuilder.cs coreclr / src /../ Generic / ValueListBuilder.cs
明确地说,这些情况经常发生。 但是,以前我们以另一种方式解决了该问题。 我们曾经创建过一个List
,将其填充数据并丢失了对其的引用。 如果频繁调用该方法,则将导致悲惨的情况:许多List
实例(和关联的数组)被挂起在堆上。 现在,此问题已解决:不会创建其他对象。 但是,就ValueStringBuilder
它仅适用于Microsoft程序员:此类具有internal
修饰符。
规则和使用惯例
要完全了解新型数据,您需要编写两种或三种或更多种使用它的方法来使用它。 但是,现在可以学习主要规则:
- 如果您的方法在不更改输入数据集大小的情况下进行处理,则可以尝试使用
Span
类型。 如果您不打算修改缓冲区,请选择ReadOnlySpan
类型。 - 如果您的方法处理计算某些统计信息或解析这些字符串的字符串,则它必须接受
ReadOnlySpan<char>
。 必须是一个新规则。 因为当您接受一个字符串时,您会让别人为您创建一个子字符串。 - 如果您需要为一个方法创建一个短数据数组(不超过10 kB),则可以使用
Span<TType> buf = stackalloc TType[size]
轻松地进行排列。 请注意,TType应该是值类型,因为stackalloc
仅适用于值类型。
在其他情况下,您最好仔细看一下Memory
或使用经典数据类型。
跨度如何工作?
我想再说几句关于Span
功能以及为何如此显着。 而且有话要说。 这种类型的数据有两个版本:一个用于.NET Core 2.0+,另一个用于其他版本。
文件Span.Fast.cs,.NET Core 2.0 coreclr /.../ System / Span.Fast.cs **
public readonly ref partial struct Span<T> { /// A reference to a .NET object or a pure pointer internal readonly ByReference<T> _pointer; /// The length of the buffer based on the pointer private readonly int _length; // ... }
文件??? [反编译]
public ref readonly struct Span<T> { private readonly System.Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; // ... }
事实是, 巨大的 .NET Framework和.NET Core1。*没有以特殊方式更新垃圾收集器(与.NET Core 2.0+不同),它们必须使用指向缓冲区中缓冲区开头的附加指针。使用。 这意味着,内部Span
就像处理非托管对象一样处理托管的.NET对象。 只需查看该结构的第二个变体:它具有三个字段。 第一个是对被管理对象的引用。 第二个是从该对象开始的偏移量(以字节为单位),用于定义数据缓冲区的开头(在字符串中,此缓冲区包含char
字符,而在数组中,它包含数组的数据)。 最后,第三个字段包含行中放置的缓冲区中的元素数量。
让我们看看Span
如何处理字符串,例如:
文件MemoryExtensions.Fast.cs
coreclr /../ MemoryExtensions.Fast.cs
public static ReadOnlySpan<char> AsSpan(this string text) { if (text == null) return default; return new ReadOnlySpan<char>(ref text.GetRawStringData(), text.Length); }
其中string.GetRawStringData()
看起来如下:
定义字段 coreclr /../ System / String.CoreCLR.cs的文件
定义了GetRawStringData coreclr /../ System / String.cs的文件
public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable<char>, IComparable<string>, IEquatable<string>, ICloneable { // // These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // [NonSerialized] private int _stringLength; // For empty strings, this will be '\0' since // strings are both null-terminated and length prefixed [NonSerialized] private char _firstChar; internal ref char GetRawStringData() => ref _firstChar; }
事实证明,该方法直接访问字符串的内部,而ref char
规范允许GC处于活动状态时,通过将其与字符串一起移动,GC可以跟踪对该字符串内部的非托管引用。
数组也是如此:创建Span
时,一些内部JIT代码会计算数据数组开头的偏移量,并使用此偏移量初始化Span
。 有关内存中对象的结构(。\ ObjectsStructure.md)的章节中讨论了计算字符串和数组的偏移量的方法。
跨度<T>作为返回值
尽管很协调,但Span
在从方法返回时仍有一些逻辑但出乎意料的限制。 如果我们看下面的代码:
unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = new byte[100]; return reff; }
我们可以看到它是合乎逻辑的和良好的。 但是,如果我们将一条指令替换为另一条指令:
unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = stackalloc byte[100]; return reff; }
编译器将禁止它。 在我说为什么之前,我希望您先猜一下此构造带来哪些问题。
好吧,我希望你能思考,猜测甚至理解原因。 如果是的话,我撰写有关[线程堆栈](./ThreadStack.md)的详细章节的努力得到了回报。 因为当您从完成工作的方法返回对局部变量的引用时,您可以调用另一个方法,等它也完成工作,然后使用x [0.99]读取那些局部变量的值。
幸运的是,当我们尝试编写这样的代码时,编译器会发出警告,使您CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope
: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope
。 编译器是正确的,因为如果绕过此错误,则在插件中时,有机会窃取他人的密码或提升运行插件的特权。
本章由作者和专业翻译员共同译自俄语。 您可以帮助我们将俄语或英语翻译成任何其他语言,主要是中文或德语。
另外,如果您想感谢我们,最好的方法是在github上给我们加星号或分支存储库
github / sidristij / dotnetbook