C# String.Format性能优化

起因

由于在前面看了String中Contact/Join在性能进行了改进,便顺便看了Format的源码是否也进行了改进了,关于String.Format方法其实之前也写过,当时是建议需要性能的时候使用Contact/或者StringBuilder.

关于C#字符串相关的博文:

用BenchmarkDotNet对Format进行性能测试,看性能改进有多少提高

////构建一组参数
[Params(1024, 2048, 4096)]
public int Count { get; set; }

[Benchmark]
public void Format()
{
    for (int i = 0; i < Count; i++)
    {
        string s1 = $"hello csharp {i}";
        string s2 = s1;
    }
}

String.Format分别在.Net Framework 4.8和.Net Core 3.1及.Net 5和.Net 6性能测试对比

从Benchmark看出.Net Framework 4.8和.Net 6对比,在时间上减少了3倍,从GC次数减少了1倍多.即使.Net 5和.Net 6在时间也减少了1倍左右.从而得出这一块性能提升的还是很高的.这里的.Net 6版本是preview 7,Format这一块的改进还没稳定下来(这个到下边会说为什么),到.Net 6正式版发布的时候,性能可能还有提升的.

Format源码

public static string Format(string format, object? arg0)
{
    return FormatHelper(null, format, new ParamsArray(arg0));
}

private static string FormatHelper(IFormatProvider? provider, string format, ParamsArray args)
{
    if (format == null)
        throw new ArgumentNullException(nameof(format));

    var sb = new ValueStringBuilder(stackalloc char[256]);
    sb.EnsureCapacity(format.Length + args.Length * 8);
    sb.AppendFormatHelper(provider, format, args);
    return sb.ToString();
}

Format源码不复杂,这里就没有加注释,如果看过String.Contact/Join方法这一篇文章的话,就知道ValueStringBuilder是什么了? Format源码应该是在.Net Core3.1之后就没有调整,那为什么在.Net 6性能有了提升了.为什么会这么说呢? 

主要是根据BenchmarkDotNet性能测试输出的汇编代码得出的这个结论.因为生成的汇编代码蛮长的,下边只列出.Net 5和.Net 6生成的汇报代码:

.Net 5汇编代码:

; dotnet_perf.TestString.Format()
       push      rdi
       push      rsi
       sub       rsp,48
       xor       eax,eax
       mov       [rsp+28],rax
       vxorps    xmm4,xmm4,xmm4
       vmovdqa   xmmword ptr [rsp+30],xmm4
       mov       [rsp+40],rax
       mov       rsi,rcx
;             for (int i = 0; i < Count; i++)
;                  ^^^^^^^^^
       xor       edi,edi
       cmp       dword ptr [rsi+8],0
       jle       short M00_L01
;                 string s1 = $"hello csharp {i}";
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L00:
       mov       rcx,offset MT_System.Int32
       call      CORINFO_HELP_NEWSFAST
       mov       [rax+8],edi
       xor       r8d,r8d
       mov       rdx,16218001338
       mov       rdx,[rdx]
       mov       rcx,16218009B48
       mov       rcx,[rcx]
       lea       r9,[rsp+28]
       mov       [r9],rax
       mov       [r9+8],r8
       mov       [r9+10],r8
       mov       [r9+18],rdx
       lea       r8,[rsp+28]
       mov       rdx,rcx
       xor       ecx,ecx
       call      System.String.FormatHelper(System.IFormatProvider, System.String, System.ParamsArray)
       inc       edi
       cmp       edi,[rsi+8]
       jl        short M00_L00
M00_L01:
       add       rsp,48
       pop       rsi
       pop       rdi
       ret
; Total bytes of code 135
; System.String.FormatHelper(System.IFormatProvider, System.String, System.ParamsArray)
       push      rbp
       push      rdi
       push      rsi
       push      rbx
       sub       rsp,68
       vzeroupper
       lea       rbp,[rsp+20]
       xor       eax,eax
       mov       [rbp+8],rax
       vxorps    xmm4,xmm4,xmm4
       vmovdqa   xmmword ptr [rbp+10],xmm4
       vmovdqa   xmmword ptr [rbp+20],xmm4
       vmovdqa   xmmword ptr [rbp+30],xmm4
       mov       [rbp+40],rax
       mov       rax,0F85D72D962D6
       mov       [rbp],rax
       mov       rbx,rcx
       mov       rdi,rdx
       mov       rsi,r8
       test      rdi,rdi
       je        near ptr M01_L01
       add       rsp,20
       test      [rsp],esp
       sub       rsp,200
       sub       rsp,20
       lea       rdx,[rsp+20]
       xor       ecx,ecx
       mov       [rbp+28],rcx
       lea       rcx,[rbp+38]
       mov       [rcx],rdx
       mov       dword ptr [rcx+8],100
       xor       edx,edx
       mov       [rbp+30],edx
       mov       edx,[rdi+8]
       mov       rcx,[rsi+18]
       mov       ecx,[rcx+8]
       lea       edx,[rdx+rcx*8]
       lea       rcx,[rbp+28]
       call      System.Text.ValueStringBuilder.EnsureCapacity(Int32)
       lea       rcx,[rbp+28]
       mov       rdx,rbx
       mov       r8,rdi
       vmovdqu   xmm0,xmmword ptr [rsi]
       vmovdqu   xmmword ptr [rbp+8],xmm0
       vmovdqu   xmm0,xmmword ptr [rsi+10]
       vmovdqu   xmmword ptr [rbp+18],xmm0
       lea       r9,[rbp+8]
       call      System.Text.ValueStringBuilder.AppendFormatHelper(System.IFormatProvider, System.String, System.ParamsArray)
       lea       rcx,[rbp+28]
       call      System.Text.ValueStringBuilder.ToString()
       mov       rcx,0F85D72D962D6
       cmp       [rbp],rcx
       je        short M01_L00
       call      CORINFO_HELP_FAIL_FAST
M01_L00:
       nop
       lea       rsp,[rbp+48]
       pop       rbx
       pop       rsi
       pop       rdi
       pop       rbp
       ret
M01_L01:
       mov       rcx,offset MT_System.ArgumentNullException
       call      CORINFO_HELP_NEWSFAST
       mov       rsi,rax
       mov       ecx,31D
       mov       rdx,7FF8F5EA4020
       call      CORINFO_HELP_STRCNS
       mov       rdx,rax
       mov       rcx,rsi
       call      System.ArgumentNullException..ctor(System.String)
       mov       rcx,rsi
       call      CORINFO_HELP_THROW
       int       3
; Total bytes of code 283

.Net 6 生成的汇编代码:

; dotnet_perf.TestString.Format()
       push      rdi
       push      rsi
       sub       rsp,58
       vzeroupper
       vxorps    xmm4,xmm4,xmm4
       vmovdqa   xmmword ptr [rsp+20],xmm4
       vmovdqa   xmmword ptr [rsp+30],xmm4
       vmovdqa   xmmword ptr [rsp+40],xmm4
       xor       eax,eax
       mov       [rsp+50],rax
       mov       rsi,rcx
;             for (int i = 0; i < Count; i++)
;                  ^^^^^^^^^
       xor       edi,edi
       cmp       dword ptr [rsi+8],0
       jle       near ptr M00_L06
;                 string s1 = $"hello csharp {i}";
;                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
M00_L00:
       xor       ecx,ecx
       mov       [rsp+30],rcx
       mov       rcx,212395E2F38
       mov       rcx,[rcx]
       mov       edx,100
       call      qword ptr [480C]
       lea       rcx,[rsp+30]
       mov       [rsp+38],rax
       test      rax,rax
       jne       short M00_L01
       xor       edx,edx
       xor       r8d,r8d
       jmp       short M00_L02
M00_L01:
       lea       rdx,[rax+10]
       mov       r8d,[rax+8]
M00_L02:
       add       rcx,18
       mov       [rcx],rdx
       mov       [rcx+8],r8d
       xor       ecx,ecx
       mov       [rsp+40],ecx
       mov       byte ptr [rsp+44],0
       mov       ecx,[rsp+40]
       cmp       ecx,[rsp+50]
       ja        near ptr M00_L07
       mov       rdx,[rsp+48]
       mov       r8d,[rsp+50]
       sub       r8d,ecx
       mov       ecx,ecx
       lea       rcx,[rdx+rcx*2]
       cmp       r8d,0D
       jb        short M00_L03
       mov       rdx,212395EA040
       mov       rdx,[rdx]
       add       rdx,0C
       mov       r8d,1A
       call      System.Buffer.Memmove(Byte ByRef, Byte ByRef, UIntPtr)
       mov       edx,[rsp+40]
       add       edx,0D
       mov       [rsp+40],edx
       jmp       short M00_L04
M00_L03:
       mov       rdx,212395EA040
       mov       rdx,[rdx]
       lea       rcx,[rsp+30];     在.Net 6使用DefaultInterpolatedStringHandler 进行插值处理
       call      System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.GrowThenCopyString(System.String)
;                 string s2 = s1;
;                 ^^^^^^^^^^^^^^^
M00_L04:
       lea       rcx,[rsp+30]
       mov       edx,edi;     在.Net 6使用DefaultInterpolatedStringHandler 插值处理,将参数放入       call      System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted[[System.Int32, System.Private.CoreLib]](Int32)
mov edx,[rsp+40] mov ecx,edx mov eax,[rsp+50] cmp rcx,rax ja short M00_L07 mov rcx,[rsp+48] mov [rsp+20],rcx mov [rsp+28],edx lea rdx,[rsp+20] xor ecx,ecx call System.String..ctor(System.ReadOnlySpan`1<Char>) mov rdx,[rsp+38] xor ecx,ecx vxorps xmm0,xmm0,xmm0 vmovdqu xmmword ptr [rsp+30],xmm0 vmovdqu xmmword ptr [rsp+40],xmm0 mov [rsp+50],rcx test rdx,rdx je short M00_L05 mov rcx,212395E2F38 mov rcx,[rcx] xor r8d,r8d call qword ptr [470D] M00_L05: inc edi cmp edi,[rsi+8] jl near ptr M00_L00 M00_L06: add rsp,58 pop rsi pop rdi ret M00_L07: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 ; Total bytes of code 371

由于.Net 6生成的汇编代码比较多,这里只是展示一部分,DefaultInterpolatedStringHandler是怎么来的.其实在编译时由编译器处理的.

看看DefaultInterpolatedStringHandler

在来一段测试代码:
int x = 100;
Console.WriteLine($"hello x={x}");

使用ILSpy看看反编译还原的代码:

//DefaultInterpolatedStringHandler在编译时,获取需要格式化的字符串 所有的字面量的字符的长度,要格式化的个数
//字面量用AppendLiteral,参数用AppendFormatted
//最后用ToStringAndClear生成一个新的字符串,并将内部的分配的空间进行归还
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(8, 1);
defaultInterpolatedStringHandler.AppendLiteral("hello x=");
defaultInterpolatedStringHandler.AppendFormatted(x);
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());

我们继续学习DefaultInterpolatedStringHandler的源码,看看内部是怎么实现的.

解析DefaultInterpolatedStringHandler调用顺序

根据上图,再去学习裁剪过的源码(保留重点的注释).

using System.Buffers;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using Internal.Runtime.CompilerServices;

namespace System.Runtime.CompilerServices
{
    /// <summary>Provides a handler used by the language compiler to process interpolated strings into <see cref="string"/> instances.</summary>
    [InterpolatedStringHandler]
    public ref struct DefaultInterpolatedStringHandler
    {
        // Implementation note:
        // As this type lives in CompilerServices and is only intended to be targeted by the compiler,
        // public APIs eschew argument validation logic in a variety of places, e.g. allowing a null input
        // when one isn't expected to produce a NullReferenceException rather than an ArgumentNullException.

        /// <summary>Expected average length of formatted data used for an individual interpolation expression result.</summary>
        /// <remarks>
        /// This is inherited from string.Format, and could be changed based on further data.
        /// string.Format actually uses `format.Length + args.Length * 8`, but format.Length
        /// includes the format items themselves, e.g. "{0}", and since it's rare to have double-digit
        /// numbers of items, we bump the 8 up to 11 to account for the three extra characters in "{d}",
        /// since the compiler-provided base length won't include the equivalent character count.
        /// </remarks>
        private const int GuessedLengthPerHole = 11;
        /// <summary>Minimum size array to rent from the pool.</summary>
        /// <remarks>Same as stack-allocation size used today by string.Format.</remarks>
        private const int MinimumArrayPoolLength = 256;

        /// <summary>Optional provider to pass to IFormattable.ToString or ISpanFormattable.TryFormat calls.</summary>
        private readonly IFormatProvider? _provider;
        /// <summary>Array rented from the array pool and used to back <see cref="_chars"/>.</summary>
        private char[]? _arrayToReturnToPool;
        /// <summary>The span to write into.</summary>
        private Span<char> _chars;
        /// <summary>Position at which to write the next character.</summary>
        private int _pos;
        /// <summary>Whether <see cref="_provider"/> provides an ICustomFormatter.</summary>
        /// <remarks>
        /// Custom formatters are very rare.  We want to support them, but it's ok if we make them more expensive
        /// in order to make them as pay-for-play as possible.  So, we avoid adding another reference type field
        /// to reduce the size of the handler and to reduce required zero'ing, by only storing whether the provider
        /// provides a formatter, rather than actually storing the formatter.  This in turn means, if there is a
        /// formatter, we pay for the extra interface call on each AppendFormatted that needs it.
        /// </remarks>
        private readonly bool _hasCustomFormatter;

        /// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
        /// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
        /// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount)
        {
            _provider = null;
            _chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
            _pos = 0;
            _hasCustomFormatter = false;
        }

        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider)
        {
            _provider = provider;
            _chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
            _pos = 0;
            _hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
        }

        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Span<char> initialBuffer)
        {
            _provider = provider;
            _chars = initialBuffer;
            _arrayToReturnToPool = null;
            _pos = 0;
            _hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // becomes a constant when inputs are constant
        internal static int GetDefaultLength(int literalLength, int formattedCount) =>
            Math.Max(MinimumArrayPoolLength, literalLength + (formattedCount * GuessedLengthPerHole));


        public override string ToString() => new string(Text);

        public string ToStringAndClear()
        {
            string result = new string(Text);
            Clear();
            return result;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // used only on a few hot paths
        internal void Clear()
        {
            char[]? toReturn = _arrayToReturnToPool;
            this = default; // defensive clear
            if (toReturn is not null)
            {
                ArrayPool<char>.Shared.Return(toReturn);
            }
        }

        /// <summary>Gets a span of the written characters thus far.</summary>
        internal ReadOnlySpan<char> Text => _chars.Slice(0, _pos);

        /// <summary>Writes the specified string to the handler.</summary>
        /// <param name="value">The string to write.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void AppendLiteral(string value)
        {
            // AppendLiteral is expected to always be called by compiler-generated code with a literal string.
            // By inlining it, the method body is exposed to the constant length of that literal, allowing the JIT to
            // prune away the irrelevant cases.  This effectively enables multiple implementations of AppendLiteral,
            // special-cased on and optimized for the literal's length.  We special-case lengths 1 and 2 because
            // they're very common, e.g.
            //     1: ' ', '.', '-', '\t', etc.
            //     2: ", ", "0x", "=>", ": ", etc.
            // but we refrain from adding more because, in the rare case where AppendLiteral is called with a non-literal,
            // there is a lot of code here to be inlined.

            // TODO: https://github.com/dotnet/runtime/issues/41692#issuecomment-685192193
            // What we really want here is to be able to add a bunch of additional special-cases based on length,
            // e.g. a switch with a case for each length <= 8, not mark the method as AggressiveInlining, and have
            // it inlined when provided with a string literal such that all the other cases evaporate but not inlined
            // if called directly with something that doesn't enable pruning.  Even better, if "literal".TryCopyTo
            // could be unrolled based on the literal, ala https://github.com/dotnet/runtime/pull/46392, we might
            // be able to remove all special-casing here.

            if (value.Length == 1)
            {
                Span<char> chars = _chars;
                int pos = _pos;
                if ((uint)pos < (uint)chars.Length)
                {
                    chars[pos] = value[0];
                    _pos = pos + 1;
                }
                else
                {
                    GrowThenCopyString(value);
                }
                return;
            }

            if (value.Length == 2)
            {
                Span<char> chars = _chars;
                int pos = _pos;
                if ((uint)pos < chars.Length - 1)
                {
                    Unsafe.WriteUnaligned(
                        ref Unsafe.As<char, byte>(ref Unsafe.Add(ref MemoryMarshal.GetReference(chars), pos)),
                        Unsafe.ReadUnaligned<int>(ref Unsafe.As<char, byte>(ref value.GetRawStringData())));
                    _pos = pos + 2;
                }
                else
                {
                    GrowThenCopyString(value);
                }
                return;
            }

            AppendStringDirect(value);
        }

        private void AppendStringDirect(string value)
        {
            if (value.TryCopyTo(_chars.Slice(_pos)))
            {
                _pos += value.Length;
            }
            else
            {
                GrowThenCopyString(value);
            }
        }

        public void AppendFormatted<T>(T value)
        {
            if (_hasCustomFormatter)
            {
                AppendCustomFormatter(value, format: null);
                return;
            }

            string? s;
            if (value is IFormattable)
            {
                // If the value can format itself directly into our buffer, do so.
                if (value is ISpanFormattable)
                {
                    int charsWritten;
                    while (!((ISpanFormattable)value).TryFormat(_chars.Slice(_pos), out charsWritten, default, _provider))
                    {
                        Grow();
                    }

                    _pos += charsWritten;
                    return;
                }

                s = ((IFormattable)value).ToString(format: null, _provider); // constrained call avoiding boxing for value types
            }
            else
            {
                s = value?.ToString();
            }

            if (s is not null)
            {
                AppendStringDirect(s);
            }
        }

        public void AppendFormatted<T>(T value, string? format)
        {
            // If there's a custom formatter, always use it.
            if (_hasCustomFormatter)
            {
                AppendCustomFormatter(value, format);
                return;
            }

            // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter
            // requires the former.  For value types, it won't matter as the type checks devolve into
            // JIT-time constants.  For reference types, they're more likely to implement IFormattable
            // than they are to implement ISpanFormattable: if they don't implement either, we save an
            // interface check over first checking for ISpanFormattable and then for IFormattable, and
            // if it only implements IFormattable, we come out even: only if it implements both do we
            // end up paying for an extra interface check.
            string? s;
            if (value is IFormattable)
            {
                // If the value can format itself directly into our buffer, do so.
                if (value is ISpanFormattable)
                {
                    int charsWritten;
                    while (!((ISpanFormattable)value).TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) 
                    {
                        Grow();
                    }

                    _pos += charsWritten;
                    return;
                }

                s = ((IFormattable)value).ToString(format, _provider); // constrained call avoiding boxing for value types
            }
            else
            {
                s = value?.ToString();
            }

            if (s is not null)
            {
                AppendStringDirect(s);
            }
        }

        public void AppendFormatted<T>(T value, int alignment)
        {
            int startingPos = _pos;
            AppendFormatted(value);
            if (alignment != 0)
            {
                AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
            }
        }

        public void AppendFormatted<T>(T value, int alignment, string? format)
        {
            int startingPos = _pos;
            AppendFormatted(value, format);
            if (alignment != 0)
            {
                AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
            }
        }

        public void AppendFormatted(ReadOnlySpan<char> value)
        {
            // Fast path for when the value fits in the current buffer
            if (value.TryCopyTo(_chars.Slice(_pos)))
            {
                _pos += value.Length;
            }
            else
            {
                GrowThenCopySpan(value);
            }
        }

        public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null)
        {
            bool leftAlign = false;
            if (alignment < 0)
            {
                leftAlign = true;
                alignment = -alignment;
            }

            int paddingRequired = alignment - value.Length;
            if (paddingRequired <= 0)
            {
                // The value is as large or larger than the required amount of padding,
                // so just write the value.
                AppendFormatted(value);
                return;
            }

            // Write the value along with the appropriate padding.
            EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
            if (leftAlign)
            {
                value.CopyTo(_chars.Slice(_pos));
                _pos += value.Length;
                _chars.Slice(_pos, paddingRequired).Fill(' ');
                _pos += paddingRequired;
            }
            else
            {
                _chars.Slice(_pos, paddingRequired).Fill(' ');
                _pos += paddingRequired;
                value.CopyTo(_chars.Slice(_pos));
                _pos += value.Length;
            }
        }

        public void AppendFormatted(string? value)
        {
            // Fast-path for no custom formatter and a non-null string that fits in the current destination buffer.
            if (!_hasCustomFormatter &&
                value is not null &&
                value.TryCopyTo(_chars.Slice(_pos)))
            {
                _pos += value.Length;
            }
            else
            {
                AppendFormattedSlow(value);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void AppendFormattedSlow(string? value)
        {
            if (_hasCustomFormatter)
            {
                AppendCustomFormatter(value, format: null);
            }
            else if (value is not null)
            {
                EnsureCapacityForAdditionalChars(value.Length);
                value.CopyTo(_chars.Slice(_pos));
                _pos += value.Length;
            }
        }

        public void AppendFormatted(string? value, int alignment = 0, string? format = null) => AppendFormatted<string?>(value, alignment, format);

        public void AppendFormatted(object? value, int alignment = 0, string? format = null) => AppendFormatted<object?>(value, alignment, format);

        [MethodImpl(MethodImplOptions.AggressiveInlining)] // only used in a few hot path call sites
        internal static bool HasCustomFormatter(IFormatProvider provider)
        {
            return
                provider.GetType() != typeof(CultureInfo) && // optimization to avoid GetFormat in the majority case
                provider.GetFormat(typeof(ICustomFormatter)) != null;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void AppendCustomFormatter<T>(T value, string? format)
        {
            Debug.Assert(_hasCustomFormatter);
            Debug.Assert(_provider != null);

            ICustomFormatter? formatter = (ICustomFormatter?)_provider.GetFormat(typeof(ICustomFormatter));
            Debug.Assert(formatter != null, "An incorrectly written provider said it implemented ICustomFormatter, and then didn't");

            if (formatter is not null && formatter.Format(format, value, _provider) is string customFormatted)
            {
                AppendStringDirect(customFormatted);
            }
        }

        private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
        {
            Debug.Assert(startingPos >= 0 && startingPos <= _pos);
            Debug.Assert(alignment != 0);

            int charsWritten = _pos - startingPos;

            bool leftAlign = false;
            if (alignment < 0)
            {
                leftAlign = true;
                alignment = -alignment;
            }

            int paddingNeeded = alignment - charsWritten;
            if (paddingNeeded > 0)
            {
                EnsureCapacityForAdditionalChars(paddingNeeded);

                if (leftAlign)
                {
                    _chars.Slice(_pos, paddingNeeded).Fill(' ');
                }
                else
                {
                    _chars.Slice(startingPos, charsWritten).CopyTo(_chars.Slice(startingPos + paddingNeeded));
                    _chars.Slice(startingPos, paddingNeeded).Fill(' ');
                }

                _pos += paddingNeeded;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void EnsureCapacityForAdditionalChars(int additionalChars)
        {
            if (_chars.Length - _pos < additionalChars)
            {
                Grow(additionalChars);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void GrowThenCopyString(string value)
        {
            Grow(value.Length);
            value.CopyTo(_chars.Slice(_pos));
            _pos += value.Length;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void GrowThenCopySpan(ReadOnlySpan<char> value)
        {
            Grow(value.Length);
            value.CopyTo(_chars.Slice(_pos));
            _pos += value.Length;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void Grow(int additionalChars)
        {
            Debug.Assert(additionalChars > _chars.Length - _pos);
            GrowCore((uint)_pos + (uint)additionalChars);
        }

        [MethodImpl(MethodImplOptions.NoInlining)] 
        private void Grow()
        {
            GrowCore((uint)_chars.Length + 1);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void GrowCore(uint requiredMinCapacity)
        {
            uint newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, string.MaxLength));
            int arraySize = (int)Math.Clamp(newCapacity, MinimumArrayPoolLength, int.MaxValue);

            char[] newArray = ArrayPool<char>.Shared.Rent(arraySize);
            _chars.Slice(0, _pos).CopyTo(newArray);

            char[]? toReturn = _arrayToReturnToPool;
            _chars = _arrayToReturnToPool = newArray;

            if (toReturn is not null)
            {
                ArrayPool<char>.Shared.Return(toReturn);
            }
        }
    }
}

为什么会在上边说现在还不稳定?

是因为在更新.Net Runtime的源码的时候发现DefaultInterpolatedStringHandler还在修改.
DefaultInterpolatedStringHandler的AppendLiteral还在进行修改
秋风 2021-08-13