From 0dd3a494638a50de0fc302e234215ba60ac8857a Mon Sep 17 00:00:00 2001 From: Levi B Date: Thu, 12 Feb 2015 11:13:44 -0800 Subject: [PATCH] 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. --- .../Encoders/HtmlEncoder.cs | 24 ++-- .../Encoders/JavaScriptStringEncoder.cs | 40 +++---- .../Encoders/UnicodeEncoderBase.cs | 107 ++++++++++-------- .../Encoders/UrlEncoder.cs | 8 +- 4 files changed, 94 insertions(+), 85 deletions(-) diff --git a/src/Microsoft.AspNet.WebUtilities/Encoders/HtmlEncoder.cs b/src/Microsoft.AspNet.WebUtilities/Encoders/HtmlEncoder.cs index 30a3da320e..14af7db2c6 100644 --- a/src/Microsoft.AspNet.WebUtilities/Encoders/HtmlEncoder.cs +++ b/src/Microsoft.AspNet.WebUtilities/Encoders/HtmlEncoder.cs @@ -117,17 +117,17 @@ namespace Microsoft.AspNet.WebUtilities.Encoders } // Writes a scalar value as an HTML-encoded entity. - protected override void WriteEncodedScalar(T output, Action writeString, Action writeChar, uint value) + protected override void WriteEncodedScalar(ref Writer writer, uint value) { - if (value == (uint)'\"') { writeString(output, """); } - else if (value == (uint)'&') { writeString(output, "&"); } - else if (value == (uint)'<') { writeString(output, "<"); } - else if (value == (uint)'>') { writeString(output, ">"); } - else { WriteEncodedScalarAsNumericEntity(output, writeChar, value); } + if (value == (uint)'\"') { writer.Write("""); } + else if (value == (uint)'&') { writer.Write("&"); } + else if (value == (uint)'<') { writer.Write("<"); } + else if (value == (uint)'>') { writer.Write(">"); } + else { WriteEncodedScalarAsNumericEntity(ref writer, value); } } // Writes a scalar value as an HTML-encoded numeric entity. - private static void WriteEncodedScalarAsNumericEntity(T output, Action 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(';'); } } } diff --git a/src/Microsoft.AspNet.WebUtilities/Encoders/JavaScriptStringEncoder.cs b/src/Microsoft.AspNet.WebUtilities/Encoders/JavaScriptStringEncoder.cs index 460cae57cd..39d7cef34f 100644 --- a/src/Microsoft.AspNet.WebUtilities/Encoders/JavaScriptStringEncoder.cs +++ b/src/Microsoft.AspNet.WebUtilities/Encoders/JavaScriptStringEncoder.cs @@ -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 output, Action writeString, Action 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 output, Action 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 output, Action 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)); } } } diff --git a/src/Microsoft.AspNet.WebUtilities/Encoders/UnicodeEncoderBase.cs b/src/Microsoft.AspNet.WebUtilities/Encoders/UnicodeEncoderBase.cs index d370fbe4f6..7b6c0ae50e 100644 --- a/src/Microsoft.AspNet.WebUtilities/Encoders/UnicodeEncoderBase.cs +++ b/src/Microsoft.AspNet.WebUtilities/Encoders/UnicodeEncoderBase.cs @@ -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 _appendCharToStringBuilderStub = PrepareDelegate((Action)AppendToStringBuilder); - private static readonly Action _appendCharToTextWriterStub = PrepareDelegate((Action)AppendToTextWriter); - private static readonly Action _appendStringToStringBuilderStub = PrepareDelegate((Action)AppendToStringBuilder); - private static readonly Action _appendStringToTextWriterStub = PrepareDelegate((Action)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 output, Action writeString, Action 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 @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 output, Action writeString, Action writeChar, uint value) where T : class; + protected abstract void WriteEncodedScalar(ref Writer writer, uint value); + + /// + /// 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. + /// + 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); + } + } + } } } diff --git a/src/Microsoft.AspNet.WebUtilities/Encoders/UrlEncoder.cs b/src/Microsoft.AspNet.WebUtilities/Encoders/UrlEncoder.cs index 2eda7cb52e..1a7b71799c 100644 --- a/src/Microsoft.AspNet.WebUtilities/Encoders/UrlEncoder.cs +++ b/src/Microsoft.AspNet.WebUtilities/Encoders/UrlEncoder.cs @@ -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 output, Action writeString, Action 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); } }