Skip to content

Commit

Permalink
Reduce ThicknessConverter allocs to minimum and improve conversion pe…
Browse files Browse the repository at this point in the history
…rformance (#9363)

* use DefaultInterpolatedStringHandler over StringBuilder and remove large chunk of allocs

* simplify and remove alloc on FromString method as well

* further optimize codegen based on manual review/benchmark

* removal additional type casts, simplify code logic

* remove unsafe code, move FormatDoubleAsString back to LengthConverter
  • Loading branch information
h3xds1nz authored Oct 3, 2024
1 parent cae5cef commit 673c354
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

using System;
using System.ComponentModel;

using System.ComponentModel.Design.Serialization;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
Expand Down Expand Up @@ -162,7 +162,7 @@ public override object ConvertTo(ITypeDescriptorContext typeDescriptorContext,
}
throw GetConvertToException(value, destinationType);
}
#endregion
#endregion

//-------------------------------------------------------------------
//
Expand All @@ -172,6 +172,18 @@ public override object ConvertTo(ITypeDescriptorContext typeDescriptorContext,

#region Internal Methods

/// <summary> Format <see cref="double"/> into <see cref="string"/> using specified <see cref="CultureInfo"/>
/// in <paramref name="handler"/>. <br /> <br />
/// Special representation applies for <see cref="double.NaN"/> values, emitted as "Auto" string instead. </summary>
/// <param name="value">The value to format as string.</param>
/// <param name="handler">The handler specifying culture used for conversion.</param>
static internal void FormatLengthAsString(double value, ref DefaultInterpolatedStringHandler handler)
{
if (double.IsNaN(value))
handler.AppendLiteral("Auto");
else
handler.AppendFormatted(value);
}

// Parse a Length from a string given the CultureInfo.
// Formats:
Expand Down Expand Up @@ -221,11 +233,6 @@ private static double ParseDouble(ReadOnlySpan<char> span, CultureInfo cultureIn
}
}

static internal string ToString(double l, CultureInfo cultureInfo)
{
if(double.IsNaN(l)) return "Auto";
return Convert.ToString(l, cultureInfo);
}

#endregion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
//
//
//
// Description: Contains the ThicknessConverter: TypeConverter for the Thicknessclass.
// Description: Contains the ThicknessConverter: TypeConverter for the Thickness struct.
//
//

using System;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Runtime.CompilerServices;
using System.Globalization;
using System.Reflection;
using System.Text;
Expand Down Expand Up @@ -73,15 +74,7 @@ public override bool CanConvertFrom(ITypeDescriptorContext typeDescriptorContext
public override bool CanConvertTo(ITypeDescriptorContext typeDescriptorContext, Type destinationType)
{
// We can convert to an InstanceDescriptor or to a string.
if ( destinationType == typeof(InstanceDescriptor)
|| destinationType == typeof(string))
{
return true;
}
else
{
return false;
}
return destinationType == typeof(InstanceDescriptor) || destinationType == typeof(string);
}

/// <summary>
Expand All @@ -102,20 +95,22 @@ public override bool CanConvertTo(ITypeDescriptorContext typeDescriptorContext,
/// <param name="source"> The object to convert to a Thickness. </param>
public override object ConvertFrom(ITypeDescriptorContext typeDescriptorContext, CultureInfo cultureInfo, object source)
{
if (source != null)
{
if (source is string) { return FromString((string)source, cultureInfo); }
else if (source is double) { return new Thickness((double)source); }
else { return new Thickness(Convert.ToDouble(source, cultureInfo)); }
}
throw GetConvertFromException(source);
if (source is null)
throw GetConvertFromException(source);

if (source is string sourceString)
return FromString(sourceString, cultureInfo);
else if (source is double sourceValue)
return new Thickness(sourceValue);
else
return new Thickness(Convert.ToDouble(source, cultureInfo));
}

/// <summary>
/// ConvertTo - Attempt to convert a Thickness to the given type
/// </summary>
/// <returns>
/// The object which was constructoed.
/// The object which was constructed.
/// </returns>
/// <exception cref="ArgumentNullException">
/// An ArgumentNullException is thrown if the example object is null.
Expand All @@ -133,22 +128,18 @@ public override object ConvertTo(ITypeDescriptorContext typeDescriptorContext, C
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(destinationType);

if (!(value is Thickness))
{
#pragma warning suppress 6506 // value is obviously not null
throw new ArgumentException(SR.Format(SR.UnexpectedParameterType, value.GetType(), typeof(Thickness)), "value");
}
if (value is not Thickness thickness)
throw new ArgumentException(SR.Format(SR.UnexpectedParameterType, value.GetType(), typeof(Thickness)), nameof(value));

Thickness th = (Thickness)value;
if (destinationType == typeof(string)) { return ToString(th, cultureInfo); }
if (destinationType == typeof(InstanceDescriptor))
if (destinationType == typeof(string))
return ToString(thickness, cultureInfo);
else if (destinationType == typeof(InstanceDescriptor))
{
ConstructorInfo ci = typeof(Thickness).GetConstructor(new Type[] { typeof(double), typeof(double), typeof(double), typeof(double) });
return new InstanceDescriptor(ci, new object[] { th.Left, th.Top, th.Right, th.Bottom });
return new InstanceDescriptor(ci, new object[] { thickness.Left, thickness.Top, thickness.Right, thickness.Bottom });
}

throw new ArgumentException(SR.Format(SR.CannotConvertType, typeof(Thickness), destinationType.FullName));

}


Expand All @@ -162,60 +153,70 @@ public override object ConvertTo(ITypeDescriptorContext typeDescriptorContext, C

#region Internal Methods

static internal string ToString(Thickness th, CultureInfo cultureInfo)
/// <summary>
/// Converts <paramref name="th"/> to its string representation using the specified <paramref name="cultureInfo"/>.
/// </summary>
/// <param name="th">The <see cref="Thickness"/> to convert to string.</param>
/// <param name="cultureInfo">Culture to use when formatting doubles and choosing separator.</param>
/// <returns>The formatted <paramref name="th"/> as string using the specified <paramref name="cultureInfo"/>.</returns>
internal static string ToString(Thickness th, CultureInfo cultureInfo)
{
char listSeparator = TokenizerHelper.GetNumericListSeparator(cultureInfo);

// Initial capacity [64] is an estimate based on a sum of:
// 48 = 4x double (twelve digits is generous for the range of values likely)
// 8 = 4x Unit Type string (approx two characters)
// 4 = 4x separator characters
StringBuilder sb = new StringBuilder(64);

sb.Append(LengthConverter.ToString(th.Left, cultureInfo));
sb.Append(listSeparator);
sb.Append(LengthConverter.ToString(th.Top, cultureInfo));
sb.Append(listSeparator);
sb.Append(LengthConverter.ToString(th.Right, cultureInfo));
sb.Append(listSeparator);
sb.Append(LengthConverter.ToString(th.Bottom, cultureInfo));
return sb.ToString();
// 3 = 3x separator characters
// 1 = 1x scratch space for alignment

DefaultInterpolatedStringHandler handler = new(0, 7, cultureInfo, stackalloc char[64]);
LengthConverter.FormatLengthAsString(th.Left, ref handler);
handler.AppendFormatted(listSeparator);

LengthConverter.FormatLengthAsString(th.Top, ref handler);
handler.AppendFormatted(listSeparator);

LengthConverter.FormatLengthAsString(th.Right, ref handler);
handler.AppendFormatted(listSeparator);

LengthConverter.FormatLengthAsString(th.Bottom, ref handler);

return handler.ToStringAndClear();
}

static internal Thickness FromString(string s, CultureInfo cultureInfo)
/// <summary>
/// Constructs a <see cref="Thickness"/> struct out of string representation supplied by <paramref name="s"/> and the specified <paramref name="cultureInfo"/>.
/// </summary>
/// <param name="s">The string representation of a <see cref="Thickness"/> struct.</param>
/// <param name="cultureInfo">The <see cref="CultureInfo"/> which was used to format this string.</param>
/// <returns>A new instance of <see cref="Thickness"/> struct representing the data contained in <paramref name="s"/>.</returns>
/// <exception cref="FormatException">Thrown when <paramref name="s"/> contains invalid string representation.</exception>
internal static Thickness FromString(string s, CultureInfo cultureInfo)
{
TokenizerHelper th = new TokenizerHelper(s, cultureInfo);
double[] lengths = new double[4];
TokenizerHelper th = new(s, cultureInfo);
Span<double> lengths = stackalloc double[4];
int i = 0;

// Peel off each double in the delimited list.
while (th.NextToken())
{
if (i >= 4)
{
i = 5; // Set i to a bad value.
break;
}
if (i >= 4) // In case we've got more than 4 doubles, we throw
throw new FormatException(SR.Format(SR.InvalidStringThickness, s));

lengths[i] = LengthConverter.FromString(th.GetCurrentToken(), cultureInfo);
i++;
}

// We have a reasonable interpreation for one value (all four edges), two values (horizontal, vertical),
// We have a reasonable interpretation for one value (all four edges),
// two values (horizontal, vertical),
// and four values (left, top, right, bottom).
switch (i)
return i switch
{
case 1:
return new Thickness(lengths[0]);

case 2:
return new Thickness(lengths[0], lengths[1], lengths[0], lengths[1]);

case 4:
return new Thickness(lengths[0], lengths[1], lengths[2], lengths[3]);
}

throw new FormatException(SR.Format(SR.InvalidStringThickness, s));
1 => new Thickness(lengths[0]),
2 => new Thickness(lengths[0], lengths[1], lengths[0], lengths[1]),
4 => new Thickness(lengths[0], lengths[1], lengths[2], lengths[3]),
_ => throw new FormatException(SR.Format(SR.InvalidStringThickness, s)),
};
}

#endregion
Expand Down

0 comments on commit 673c354

Please sign in to comment.