Perf: Eliminate chatty virtual dispatches

This gives a speedup of approx. 20% (for overloads which take TextWriter) to 40% (for overloads which don't take TextWriter) for inputs in which at least one character requires encoding.
This commit is contained in:
Levi B 2015-02-12 11:13:44 -08:00
parent 26cd8d51b6
commit 0dd3a49463
4 changed files with 94 additions and 85 deletions

View File

@ -117,17 +117,17 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
} }
// Writes a scalar value as an HTML-encoded entity. // Writes a scalar value as an HTML-encoded entity.
protected override void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value) protected override void WriteEncodedScalar(ref Writer writer, uint value)
{ {
if (value == (uint)'\"') { writeString(output, "&quot;"); } if (value == (uint)'\"') { writer.Write("&quot;"); }
else if (value == (uint)'&') { writeString(output, "&amp;"); } else if (value == (uint)'&') { writer.Write("&amp;"); }
else if (value == (uint)'<') { writeString(output, "&lt;"); } else if (value == (uint)'<') { writer.Write("&lt;"); }
else if (value == (uint)'>') { writeString(output, "&gt;"); } else if (value == (uint)'>') { writer.Write("&gt;"); }
else { WriteEncodedScalarAsNumericEntity(output, writeChar, value); } else { WriteEncodedScalarAsNumericEntity(ref writer, value); }
} }
// Writes a scalar value as an HTML-encoded numeric entity. // Writes a scalar value as an HTML-encoded numeric entity.
private static void WriteEncodedScalarAsNumericEntity<T>(T output, Action<T, char> writeChar, uint value) where T : class private static void WriteEncodedScalarAsNumericEntity(ref Writer writer, uint value)
{ {
// We're building the characters up in reverse // We're building the characters up in reverse
char* chars = stackalloc char[8 /* "FFFFFFFF" */]; char* chars = stackalloc char[8 /* "FFFFFFFF" */];
@ -141,15 +141,15 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
} while (value != 0); } while (value != 0);
// Finally, write out the HTML-encoded scalar value. // Finally, write out the HTML-encoded scalar value.
writeChar(output, '&'); writer.Write('&');
writeChar(output, '#'); writer.Write('#');
writeChar(output, 'x'); writer.Write('x');
Debug.Assert(numCharsWritten > 0, "At least one character should've been written."); Debug.Assert(numCharsWritten > 0, "At least one character should've been written.");
do do
{ {
writeChar(output, chars[--numCharsWritten]); writer.Write(chars[--numCharsWritten]);
} while (numCharsWritten != 0); } while (numCharsWritten != 0);
writeChar(output, ';'); writer.Write(';');
} }
} }
} }

View File

@ -124,7 +124,7 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
// See ECMA-262, Sec. 7.8.4, and ECMA-404, Sec. 9 // See ECMA-262, Sec. 7.8.4, and ECMA-404, Sec. 9
// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4 // http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
// http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf // http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
protected override void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value) protected override void WriteEncodedScalar(ref Writer writer, uint value)
{ {
// ECMA-262 allows encoding U+000B as "\v", but ECMA-404 does not. // ECMA-262 allows encoding U+000B as "\v", but ECMA-404 does not.
// Both ECMA-262 and ECMA-404 allow encoding U+002F SOLIDUS as "\/". // Both ECMA-262 and ECMA-404 allow encoding U+002F SOLIDUS as "\/".
@ -133,46 +133,46 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
// be written out as numeric entities for defense-in-depth. // be written out as numeric entities for defense-in-depth.
// See UnicodeEncoderBase ctor comments for more info. // See UnicodeEncoderBase ctor comments for more info.
if (value == (uint)'\b') { writeString(output, @"\b"); } if (value == (uint)'\b') { writer.Write(@"\b"); }
else if (value == (uint)'\t') { writeString(output, @"\t"); } else if (value == (uint)'\t') { writer.Write(@"\t"); }
else if (value == (uint)'\n') { writeString(output, @"\n"); } else if (value == (uint)'\n') { writer.Write(@"\n"); }
else if (value == (uint)'\f') { writeString(output, @"\f"); } else if (value == (uint)'\f') { writer.Write(@"\f"); }
else if (value == (uint)'\r') { writeString(output, @"\r"); } else if (value == (uint)'\r') { writer.Write(@"\r"); }
else if (value == (uint)'/') { writeString(output, @"\/"); } else if (value == (uint)'/') { writer.Write(@"\/"); }
else if (value == (uint)'\\') { writeString(output, @"\\"); } else if (value == (uint)'\\') { writer.Write(@"\\"); }
else { WriteEncodedScalarAsNumericEntity(output, writeChar, value); } else { WriteEncodedScalarAsNumericEntity(ref writer, value); }
} }
// Writes a scalar value as an JavaScript-escaped character (or sequence of characters). // Writes a scalar value as an JavaScript-escaped character (or sequence of characters).
private static void WriteEncodedScalarAsNumericEntity<T>(T output, Action<T, char> writeChar, uint value) where T : class private static void WriteEncodedScalarAsNumericEntity(ref Writer writer, uint value)
{ {
if (UnicodeHelpers.IsSupplementaryCodePoint((int)value)) if (UnicodeHelpers.IsSupplementaryCodePoint((int)value))
{ {
// Convert this back to UTF-16 and write out both characters. // Convert this back to UTF-16 and write out both characters.
char leadingSurrogate, trailingSurrogate; char leadingSurrogate, trailingSurrogate;
UnicodeHelpers.GetUtf16SurrogatePairFromAstralScalarValue((int)value, out leadingSurrogate, out trailingSurrogate); UnicodeHelpers.GetUtf16SurrogatePairFromAstralScalarValue((int)value, out leadingSurrogate, out trailingSurrogate);
WriteEncodedSingleCharacter(output, writeChar, leadingSurrogate); WriteEncodedSingleCharacter(ref writer, leadingSurrogate);
WriteEncodedSingleCharacter(output, writeChar, trailingSurrogate); WriteEncodedSingleCharacter(ref writer, trailingSurrogate);
} }
else else
{ {
// This is only a single character. // This is only a single character.
WriteEncodedSingleCharacter(output, writeChar, value); WriteEncodedSingleCharacter(ref writer, value);
} }
} }
// Writes an encoded scalar value (in the BMP) as a JavaScript-escaped character. // Writes an encoded scalar value (in the BMP) as a JavaScript-escaped character.
private static void WriteEncodedSingleCharacter<T>(T output, Action<T, char> writeChar, uint value) where T : class private static void WriteEncodedSingleCharacter(ref Writer writer, uint value)
{ {
Debug.Assert(!UnicodeHelpers.IsSupplementaryCodePoint((int)value), "The incoming value should've been in the BMP."); Debug.Assert(!UnicodeHelpers.IsSupplementaryCodePoint((int)value), "The incoming value should've been in the BMP.");
// Encode this as 6 chars "\uFFFF". // Encode this as 6 chars "\uFFFF".
writeChar(output, '\\'); writer.Write('\\');
writeChar(output, 'u'); writer.Write('u');
writeChar(output, HexUtil.IntToChar(value >> 12)); writer.Write(HexUtil.IntToChar(value >> 12));
writeChar(output, HexUtil.IntToChar((value >> 8) & 0xFU)); writer.Write(HexUtil.IntToChar((value >> 8) & 0xFU));
writeChar(output, HexUtil.IntToChar((value >> 4) & 0xFU)); writer.Write(HexUtil.IntToChar((value >> 4) & 0xFU));
writeChar(output, HexUtil.IntToChar(value & 0xFU)); writer.Write(HexUtil.IntToChar(value & 0xFU));
} }
} }
} }

View File

@ -11,12 +11,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
{ {
internal unsafe abstract class UnicodeEncoderBase internal unsafe abstract class UnicodeEncoderBase
{ {
// Stubs for appending data to a TextWriter or StringBuilder
private static readonly Action<StringBuilder, char> _appendCharToStringBuilderStub = PrepareDelegate((Action<StringBuilder, char>)AppendToStringBuilder);
private static readonly Action<TextWriter, char> _appendCharToTextWriterStub = PrepareDelegate((Action<TextWriter, char>)AppendToTextWriter);
private static readonly Action<StringBuilder, string> _appendStringToStringBuilderStub = PrepareDelegate((Action<StringBuilder, string>)AppendToStringBuilder);
private static readonly Action<TextWriter, string> _appendStringToTextWriterStub = PrepareDelegate((Action<TextWriter, string>)AppendToTextWriter);
// A bitmap of characters which are allowed to be returned unescaped. // A bitmap of characters which are allowed to be returned unescaped.
private readonly uint[] _allowedCharsBitmap = new uint[0x10000 / 32]; private readonly uint[] _allowedCharsBitmap = new uint[0x10000 / 32];
@ -77,26 +71,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
_allowedCharsBitmap[index] |= 0x1U << offset; _allowedCharsBitmap[index] |= 0x1U << offset;
} }
private static void AppendToStringBuilder(StringBuilder builder, char value)
{
builder.Append(value);
}
private static void AppendToStringBuilder(StringBuilder builder, string value)
{
builder.Append(value);
}
private static void AppendToTextWriter(TextWriter writer, char value)
{
writer.Write(value);
}
private static void AppendToTextWriter(TextWriter writer, string value)
{
writer.Write(value);
}
// Marks a character as forbidden (must be returned encoded) // Marks a character as forbidden (must be returned encoded)
protected void ForbidCharacter(char c) protected void ForbidCharacter(char c)
{ {
@ -212,19 +186,21 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
// Allocate the StringBuilder with the first (known to not require encoding) part of the input string, // Allocate the StringBuilder with the first (known to not require encoding) part of the input string,
// then begin encoding from the last (potentially requiring encoding) part of the input string. // then begin encoding from the last (potentially requiring encoding) part of the input string.
StringBuilder builder = new StringBuilder(input, 0, idxOfFirstCharWhichRequiresEncoding, sbCapacity); StringBuilder builder = new StringBuilder(input, 0, idxOfFirstCharWhichRequiresEncoding, sbCapacity);
Writer writer = new Writer(builder);
fixed (char* pInput = input) fixed (char* pInput = input)
{ {
EncodeCore(builder, _appendStringToStringBuilderStub, _appendCharToStringBuilderStub, &pInput[idxOfFirstCharWhichRequiresEncoding], (uint)numCharsWhichMayRequireEncoding); EncodeCore(ref writer, &pInput[idxOfFirstCharWhichRequiresEncoding], (uint)numCharsWhichMayRequireEncoding);
} }
return builder.ToString(); return builder.ToString();
} }
private void EncodeCore(char* input, uint charsRemaining, TextWriter output) private void EncodeCore(char* input, uint charsRemaining, TextWriter output)
{ {
EncodeCore(output, _appendStringToTextWriterStub, _appendCharToTextWriterStub, input, charsRemaining); Writer writer = new Writer(output);
EncodeCore(ref writer, input, charsRemaining);
} }
private void EncodeCore<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, char* input, uint charsRemaining) where T : class private void EncodeCore(ref Writer writer, char* input, uint charsRemaining)
{ {
while (charsRemaining != 0) while (charsRemaining != 0)
{ {
@ -232,7 +208,7 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
if (UnicodeHelpers.IsSupplementaryCodePoint(nextScalar)) if (UnicodeHelpers.IsSupplementaryCodePoint(nextScalar))
{ {
// Supplementary characters should always be encoded numerically. // Supplementary characters should always be encoded numerically.
WriteEncodedScalar(output, writeString, writeChar, (uint)nextScalar); WriteEncodedScalar(ref writer, (uint)nextScalar);
// We consume two UTF-16 characters for a single supplementary character. // We consume two UTF-16 characters for a single supplementary character.
input += 2; input += 2;
@ -246,11 +222,11 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
char c = (char)nextScalar; char c = (char)nextScalar;
if (IsCharacterAllowed(c)) if (IsCharacterAllowed(c))
{ {
writeChar(output, c); writer.Write(c);
} }
else else
{ {
WriteEncodedScalar(output, writeString, writeChar, (uint)nextScalar); WriteEncodedScalar(ref writer, (uint)nextScalar);
} }
} }
} }
@ -278,22 +254,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
return ((_allowedCharsBitmap[index] >> offset) & 0x1U) != 0; return ((_allowedCharsBitmap[index] >> offset) & 0x1U) != 0;
} }
private static T PrepareDelegate<T>(T @del) where T : class
{
#if ASPNETCORE50
// RuntimeHelpers.PrepareMethod doesn't exist on CoreCLR, so we'll depend
// on cross-gen for performance optimizations.
return del;
#else
// We prepare the method ahead of time to ensure that it's JITted before
// the delegate is constructed; this allows the delegate to point straight
// to the processor code rather than to the prestub dispatch code.
Delegate castDel = (Delegate)(object)del;
RuntimeHelpers.PrepareMethod(castDel.Method.MethodHandle);
return (T)(object)castDel.Method.CreateDelegate(typeof(T));
#endif
}
private static void ValidateInputs(int startIndex, int charCount, int actualInputLength) private static void ValidateInputs(int startIndex, int charCount, int actualInputLength)
{ {
if (startIndex < 0 || startIndex > actualInputLength) if (startIndex < 0 || startIndex > actualInputLength)
@ -306,6 +266,55 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
} }
} }
protected abstract void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value) where T : class; protected abstract void WriteEncodedScalar(ref Writer writer, uint value);
/// <summary>
/// Provides an abstraction over both StringBuilder and TextWriter.
/// Declared as a struct so we can allocate on the stack and pass by
/// reference. Eliminates chatty virtual dispatches on hot paths.
/// </summary>
protected struct Writer
{
private readonly StringBuilder _innerBuilder;
private readonly TextWriter _innerWriter;
public Writer(StringBuilder innerBuilder)
{
_innerBuilder = innerBuilder;
_innerWriter = null;
}
public Writer(TextWriter innerWriter)
{
_innerBuilder = null;
_innerWriter = innerWriter;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(char value)
{
if (_innerBuilder != null)
{
_innerBuilder.Append(value);
}
else
{
_innerWriter.Write(value);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(string value)
{
if (_innerBuilder != null)
{
_innerBuilder.Append(value);
}
else
{
_innerWriter.Write(value);
}
}
}
} }
} }

View File

@ -160,16 +160,16 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
} }
// Writes a scalar value as a percent-encoded sequence of UTF8 bytes, per RFC 3987. // Writes a scalar value as a percent-encoded sequence of UTF8 bytes, per RFC 3987.
protected override void WriteEncodedScalar<T>(T output, Action<T, string> writeString, Action<T, char> writeChar, uint value) protected override void WriteEncodedScalar(ref Writer writer, uint value)
{ {
uint asUtf8 = (uint)UnicodeHelpers.GetUtf8RepresentationForScalarValue(value); uint asUtf8 = (uint)UnicodeHelpers.GetUtf8RepresentationForScalarValue(value);
do do
{ {
char highNibble, lowNibble; char highNibble, lowNibble;
HexUtil.WriteHexEncodedByte((byte)asUtf8, out highNibble, out lowNibble); HexUtil.WriteHexEncodedByte((byte)asUtf8, out highNibble, out lowNibble);
writeChar(output, '%'); writer.Write('%');
writeChar(output, highNibble); writer.Write(highNibble);
writeChar(output, lowNibble); writer.Write(lowNibble);
} while ((asUtf8 >>= 8) != 0); } while ((asUtf8 >>= 8) != 0);
} }
} }