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.
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;"); }
else if (value == (uint)'&') { writeString(output, "&amp;"); }
else if (value == (uint)'<') { writeString(output, "&lt;"); }
else if (value == (uint)'>') { writeString(output, "&gt;"); }
else { WriteEncodedScalarAsNumericEntity(output, writeChar, value); }
if (value == (uint)'\"') { writer.Write("&quot;"); }
else if (value == (uint)'&') { writer.Write("&amp;"); }
else if (value == (uint)'<') { writer.Write("&lt;"); }
else if (value == (uint)'>') { writer.Write("&gt;"); }
else { WriteEncodedScalarAsNumericEntity(ref writer, value); }
}
// 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
char* chars = stackalloc char[8 /* "FFFFFFFF" */];
@ -141,15 +141,15 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
} while (value != 0);
// Finally, write out the HTML-encoded scalar value.
writeChar(output, '&');
writeChar(output, '#');
writeChar(output, 'x');
writer.Write('&');
writer.Write('#');
writer.Write('x');
Debug.Assert(numCharsWritten > 0, "At least one character should've been written.");
do
{
writeChar(output, chars[--numCharsWritten]);
writer.Write(chars[--numCharsWritten]);
} 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
// 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
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.
// 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.
// See UnicodeEncoderBase ctor comments for more info.
if (value == (uint)'\b') { writeString(output, @"\b"); }
else if (value == (uint)'\t') { writeString(output, @"\t"); }
else if (value == (uint)'\n') { writeString(output, @"\n"); }
else if (value == (uint)'\f') { writeString(output, @"\f"); }
else if (value == (uint)'\r') { writeString(output, @"\r"); }
else if (value == (uint)'/') { writeString(output, @"\/"); }
else if (value == (uint)'\\') { writeString(output, @"\\"); }
else { WriteEncodedScalarAsNumericEntity(output, writeChar, value); }
if (value == (uint)'\b') { writer.Write(@"\b"); }
else if (value == (uint)'\t') { writer.Write(@"\t"); }
else if (value == (uint)'\n') { writer.Write(@"\n"); }
else if (value == (uint)'\f') { writer.Write(@"\f"); }
else if (value == (uint)'\r') { writer.Write(@"\r"); }
else if (value == (uint)'/') { writer.Write(@"\/"); }
else if (value == (uint)'\\') { writer.Write(@"\\"); }
else { WriteEncodedScalarAsNumericEntity(ref writer, value); }
}
// 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))
{
// Convert this back to UTF-16 and write out both characters.
char leadingSurrogate, trailingSurrogate;
UnicodeHelpers.GetUtf16SurrogatePairFromAstralScalarValue((int)value, out leadingSurrogate, out trailingSurrogate);
WriteEncodedSingleCharacter(output, writeChar, leadingSurrogate);
WriteEncodedSingleCharacter(output, writeChar, trailingSurrogate);
WriteEncodedSingleCharacter(ref writer, leadingSurrogate);
WriteEncodedSingleCharacter(ref writer, trailingSurrogate);
}
else
{
// 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.
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.");
// Encode this as 6 chars "\uFFFF".
writeChar(output, '\\');
writeChar(output, 'u');
writeChar(output, HexUtil.IntToChar(value >> 12));
writeChar(output, HexUtil.IntToChar((value >> 8) & 0xFU));
writeChar(output, HexUtil.IntToChar((value >> 4) & 0xFU));
writeChar(output, HexUtil.IntToChar(value & 0xFU));
writer.Write('\\');
writer.Write('u');
writer.Write(HexUtil.IntToChar(value >> 12));
writer.Write(HexUtil.IntToChar((value >> 8) & 0xFU));
writer.Write(HexUtil.IntToChar((value >> 4) & 0xFU));
writer.Write(HexUtil.IntToChar(value & 0xFU));
}
}
}

View File

@ -11,12 +11,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
{
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.
private readonly uint[] _allowedCharsBitmap = new uint[0x10000 / 32];
@ -77,26 +71,6 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
_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)
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,
// then begin encoding from the last (potentially requiring encoding) part of the input string.
StringBuilder builder = new StringBuilder(input, 0, idxOfFirstCharWhichRequiresEncoding, sbCapacity);
Writer writer = new Writer(builder);
fixed (char* pInput = input)
{
EncodeCore(builder, _appendStringToStringBuilderStub, _appendCharToStringBuilderStub, &pInput[idxOfFirstCharWhichRequiresEncoding], (uint)numCharsWhichMayRequireEncoding);
EncodeCore(ref writer, &pInput[idxOfFirstCharWhichRequiresEncoding], (uint)numCharsWhichMayRequireEncoding);
}
return builder.ToString();
}
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)
{
@ -232,7 +208,7 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
if (UnicodeHelpers.IsSupplementaryCodePoint(nextScalar))
{
// 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.
input += 2;
@ -246,11 +222,11 @@ namespace Microsoft.AspNet.WebUtilities.Encoders
char c = (char)nextScalar;
if (IsCharacterAllowed(c))
{
writeChar(output, c);
writer.Write(c);
}
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;
}
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)
{
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.
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);
do
{
char highNibble, lowNibble;
HexUtil.WriteHexEncodedByte((byte)asUtf8, out highNibble, out lowNibble);
writeChar(output, '%');
writeChar(output, highNibble);
writeChar(output, lowNibble);
writer.Write('%');
writer.Write(highNibble);
writer.Write(lowNibble);
} while ((asUtf8 >>= 8) != 0);
}
}