内存和跨度pt.2


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 scopeCS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope 。 编译器是正确的,因为如果绕过此错误,则在插件中时,有机会窃取他人的密码或提升运行插件的特权。


本章由作者和专业翻译员共同译自俄语。 您可以帮助我们将俄语或英语翻译成任何其他语言,主要是中文或德语。

另外,如果您想感谢我们,最好的方法是在github上给我们加星号或分支存储库 github / sidristij / dotnetbook

Source: https://habr.com/ru/post/zh-CN443976/


All Articles