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;
}
}
从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的源码,看看内部是怎么实现的.
根据上图,再去学习裁剪过的源码(保留重点的注释).
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还在修改.
秋风
2021-08-13