Skip to content

Commit

Permalink
Use ValueStringBuilder to fix allocations in ToString() methods of Xd…
Browse files Browse the repository at this point in the history
…sDateTime and XsdDuration (dotnet#64868)

* Use ValueStringBuilder to fix allocations in XdsDateTime struct method ToString()

* Use ValueStringBuilder in XsdDuration

* Fix failing tests

* Address feedback

* Address feedback 2

* few more brackets rmoved

* Use previous methods for formatting into 2/4/X digits as they are faster than AppendSpanFormattable

Co-authored-by: Traian Zaprianov <[email protected]>
  • Loading branch information
TrayanZapryanov and TrayanZapryanov authored Feb 24, 2022
1 parent 55abc29 commit 9a050cd
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Text
{
internal ref partial struct ValueStringBuilder
{
internal void AppendSpanFormattable<T>(T value, string? format, IFormatProvider? provider) where T : ISpanFormattable
{
if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider))
{
_pos += charsWritten;
}
else
{
Append(value.ToString(format, provider));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,9 @@
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.cs">
<Link>Common\System\Text\ValueStringBuilder.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.AppendSpanFormattable.cs">
<Link>Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\Threading\OpenExistingResult.cs">
<Link>Common\System\Threading\OpenExistingResult.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,6 @@ public void AppendFormat(IFormatProvider? provider, string format, params object
AppendFormatHelper(provider, format, new ParamsArray(args));
}

internal void AppendSpanFormattable<T>(T value, string? format, IFormatProvider? provider) where T : ISpanFormattable
{
if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider))
{
_pos += charsWritten;
}
else
{
Append(value.ToString(format, provider));
}
}

// Copied from StringBuilder, can't be done via generic extension
// as ValueStringBuilder is a ref struct and cannot be used in a generic.
internal void AppendFormatHelper(IFormatProvider? provider, string format!!, ParamsArray args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,8 @@
<Compile Include="$(CommonPath)System\LocalAppContextSwitches.Common.cs" Link="System\LocalAppContextSwitches.Common.cs" />
<Compile Include="System\Xml\Core\LocalAppContextSwitches.cs" />
<Compile Include="$(CommonPath)System\CSharpHelpers.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.cs" Link="Common\System\Text\ValueStringBuilder.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.AppendSpanFormattable.cs" Link="Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
<Compile Include="System\Xml\Xsl\Runtime\XmlCollation.Windows.cs" />
Expand Down
114 changes: 47 additions & 67 deletions src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,76 +494,59 @@ public static implicit operator DateTimeOffset(XsdDateTime xdt)
/// </summary>
public override string ToString()
{
StringBuilder sb = new StringBuilder(64);
char[] text;
var vsb = new ValueStringBuilder(stackalloc char[64]);
switch (InternalTypeCode)
{
case DateTimeTypeCode.DateTime:
PrintDate(sb);
sb.Append('T');
PrintTime(sb);
PrintDate(ref vsb);
vsb.Append('T');
PrintTime(ref vsb);
break;
case DateTimeTypeCode.Time:
PrintTime(sb);
PrintTime(ref vsb);
break;
case DateTimeTypeCode.Date:
PrintDate(sb);
PrintDate(ref vsb);
break;
case DateTimeTypeCode.GYearMonth:
text = new char[s_lzyyyy_MM];
IntToCharArray(text, 0, Year, 4);
text[s_lzyyyy] = '-';
ShortToCharArray(text, s_lzyyyy_, Month);
sb.Append(text);
vsb.AppendSpanFormattable(Year, format: "D4", provider: null);
vsb.Append('-');
vsb.AppendSpanFormattable(Month, format: "D2", provider: null);
break;
case DateTimeTypeCode.GYear:
text = new char[s_lzyyyy];
IntToCharArray(text, 0, Year, 4);
sb.Append(text);
vsb.AppendSpanFormattable(Year, format: "D4", provider: null);
break;
case DateTimeTypeCode.GMonthDay:
text = new char[s_lz__mm_dd];
text[0] = '-';
text[s_Lz_] = '-';
ShortToCharArray(text, s_Lz__, Month);
text[s_lz__mm] = '-';
ShortToCharArray(text, s_lz__mm_, Day);
sb.Append(text);
vsb.Append("--");
vsb.AppendSpanFormattable(Month, format: "D2", provider: null);
vsb.Append('-');
vsb.AppendSpanFormattable(Day, format: "D2", provider: null);
break;
case DateTimeTypeCode.GDay:
text = new char[s_lz___dd];
text[0] = '-';
text[s_Lz_] = '-';
text[s_Lz__] = '-';
ShortToCharArray(text, s_Lz___, Day);
sb.Append(text);
vsb.Append("---");
vsb.AppendSpanFormattable(Day, format: "D2", provider: null);
break;
case DateTimeTypeCode.GMonth:
text = new char[s_lz__mm__];
text[0] = '-';
text[s_Lz_] = '-';
ShortToCharArray(text, s_Lz__, Month);
text[s_lz__mm] = '-';
text[s_lz__mm_] = '-';
sb.Append(text);
vsb.Append("--");
vsb.AppendSpanFormattable(Month, format: "D2", provider: null);
vsb.Append("--");
break;
}
PrintZone(sb);
return sb.ToString();
PrintZone(ref vsb);
return vsb.ToString();
}

// Serialize year, month and day
private void PrintDate(StringBuilder sb)
private void PrintDate(ref ValueStringBuilder vsb)
{
char[] text = new char[s_lzyyyy_MM_dd];
Span<char> text = vsb.AppendSpan(s_lzyyyy_MM_dd);
int year, month, day;
GetYearMonthDay(out year, out month, out day);
IntToCharArray(text, 0, year, 4);
WriteXDigits(text, 0, year, 4);
text[s_lzyyyy] = '-';
ShortToCharArray(text, s_lzyyyy_, month);
Write2Digits(text, s_lzyyyy_, month);
text[s_lzyyyy_MM] = '-';
ShortToCharArray(text, s_lzyyyy_MM_, day);
sb.Append(text);
Write2Digits(text, s_lzyyyy_MM_, day);
}

// When printing the date, we need the year, month and the day. When
Expand Down Expand Up @@ -620,15 +603,14 @@ private void GetYearMonthDay(out int year, out int month, out int day)
}

// Serialize hour, minute, second and fraction
private void PrintTime(StringBuilder sb)
private void PrintTime(ref ValueStringBuilder vsb)
{
char[] text = new char[s_lzHH_mm_ss];
ShortToCharArray(text, 0, Hour);
Span<char> text = vsb.AppendSpan(s_lzHH_mm_ss);
Write2Digits(text, 0, Hour);
text[s_lzHH] = ':';
ShortToCharArray(text, s_lzHH_, Minute);
Write2Digits(text, s_lzHH_, Minute);
text[s_lzHH_mm] = ':';
ShortToCharArray(text, s_lzHH_mm_, Second);
sb.Append(text);
Write2Digits(text, s_lzHH_mm_, Second);
int fraction = Fraction;
if (fraction != 0)
{
Expand All @@ -638,47 +620,45 @@ private void PrintTime(StringBuilder sb)
fractionDigits--;
fraction /= 10;
}
text = new char[fractionDigits + 1];

text = vsb.AppendSpan(fractionDigits + 1);
text[0] = '.';
IntToCharArray(text, 1, fraction, fractionDigits);
sb.Append(text);
WriteXDigits(text, 1, fraction, fractionDigits);
}
}

// Serialize time zone
private void PrintZone(StringBuilder sb)
private void PrintZone(ref ValueStringBuilder vsb)
{
char[] text;
Span<char> text;
switch (InternalKind)
{
case XsdDateTimeKind.Zulu:
sb.Append('Z');
vsb.Append('Z');
break;
case XsdDateTimeKind.LocalWestOfZulu:
text = new char[s_lz_zz_zz];
text = vsb.AppendSpan(s_lz_zz_zz);
text[0] = '-';
ShortToCharArray(text, s_Lz_, ZoneHour);
Write2Digits(text, s_Lz_, ZoneHour);
text[s_lz_zz] = ':';
ShortToCharArray(text, s_lz_zz_, ZoneMinute);
sb.Append(text);
Write2Digits(text, s_lz_zz_, ZoneMinute);
break;
case XsdDateTimeKind.LocalEastOfZulu:
text = new char[s_lz_zz_zz];
text = vsb.AppendSpan(s_lz_zz_zz);
text[0] = '+';
ShortToCharArray(text, s_Lz_, ZoneHour);
Write2Digits(text, s_Lz_, ZoneHour);
text[s_lz_zz] = ':';
ShortToCharArray(text, s_lz_zz_, ZoneMinute);
sb.Append(text);
Write2Digits(text, s_lz_zz_, ZoneMinute);
break;
default:
// do nothing
break;
}
}

// Serialize integer into character array starting with index [start].
// Serialize integer into character Span starting with index [start].
// Number of digits is set by [digits]
private void IntToCharArray(char[] text, int start, int value, int digits)
private static void WriteXDigits(Span<char> text, int start, int value, int digits)
{
while (digits-- != 0)
{
Expand All @@ -687,8 +667,8 @@ private void IntToCharArray(char[] text, int start, int value, int digits)
}
}

// Serialize two digit integer into character array starting with index [start].
private void ShortToCharArray(char[] text, int start, int value)
// Serialize two digit integer into character Span starting with index [start].
private static void Write2Digits(Span<char> text, int start, int value)
{
text[start] = (char)(value / 10 + '0');
text[start + 1] = (char)(value % 10 + '0');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;

namespace System.Xml.Schema
{
using System;
Expand Down Expand Up @@ -339,93 +341,94 @@ public override string ToString()
/// </summary>
internal string ToString(DurationType durationType)
{
StringBuilder sb = new StringBuilder(20);
var vsb = new ValueStringBuilder(stackalloc char[20]);
int nanoseconds, digit, zeroIdx, len;

if (IsNegative)
sb.Append('-');
vsb.Append('-');

sb.Append('P');
vsb.Append('P');

if (durationType != DurationType.DayTimeDuration)
{
if (_years != 0)
{
sb.Append(XmlConvert.ToString(_years));
sb.Append('Y');
vsb.AppendSpanFormattable(_years, null, CultureInfo.InvariantCulture);
vsb.Append('Y');
}

if (_months != 0)
{
sb.Append(XmlConvert.ToString(_months));
sb.Append('M');
vsb.AppendSpanFormattable(_months, null, CultureInfo.InvariantCulture);
vsb.Append('M');
}
}

if (durationType != DurationType.YearMonthDuration)
{
if (_days != 0)
{
sb.Append(XmlConvert.ToString(_days));
sb.Append('D');
vsb.AppendSpanFormattable(_days, null, CultureInfo.InvariantCulture);
vsb.Append('D');
}

if (_hours != 0 || _minutes != 0 || _seconds != 0 || Nanoseconds != 0)
{
sb.Append('T');
vsb.Append('T');
if (_hours != 0)
{
sb.Append(XmlConvert.ToString(_hours));
sb.Append('H');
vsb.AppendSpanFormattable(_hours, null, CultureInfo.InvariantCulture);
vsb.Append('H');
}

if (_minutes != 0)
{
sb.Append(XmlConvert.ToString(_minutes));
sb.Append('M');
vsb.AppendSpanFormattable(_minutes, null, CultureInfo.InvariantCulture);
vsb.Append('M');
}

nanoseconds = Nanoseconds;
if (_seconds != 0 || nanoseconds != 0)
{
sb.Append(XmlConvert.ToString(_seconds));
vsb.AppendSpanFormattable(_seconds, null, CultureInfo.InvariantCulture);
if (nanoseconds != 0)
{
sb.Append('.');
vsb.Append('.');

len = sb.Length;
sb.Length += 9;
zeroIdx = sb.Length - 1;
len = vsb.Length;
Span<char> tmpSpan = stackalloc char[9];
zeroIdx = len + 8;

for (int idx = zeroIdx; idx >= len; idx--)
{
digit = nanoseconds % 10;
sb[idx] = (char)(digit + '0');
tmpSpan[idx - len] = (char)(digit + '0');

if (zeroIdx == idx && digit == 0)
zeroIdx--;

nanoseconds /= 10;
}

sb.Length = zeroIdx + 1;
vsb.EnsureCapacity(zeroIdx + 1);
vsb.Append(tmpSpan.Slice(0, zeroIdx - len + 1));
}
sb.Append('S');
vsb.Append('S');
}
}

// Zero is represented as "PT0S"
if (sb[sb.Length - 1] == 'P')
sb.Append("T0S");
if (vsb[vsb.Length - 1] == 'P')
vsb.Append("T0S");
}
else
{
// Zero is represented as "T0M"
if (sb[sb.Length - 1] == 'P')
sb.Append("0M");
if (vsb[vsb.Length - 1] == 'P')
vsb.Append("0M");
}

return sb.ToString();
return vsb.ToString();
}

internal static Exception? TryParse(string s, out XsdDuration result)
Expand Down
Loading

0 comments on commit 9a050cd

Please sign in to comment.