From caf4374f836de0fb973f1165aa7221112ea598bf Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 30 Apr 2019 16:13:03 +0100 Subject: [PATCH] Speed up PathStringHelper (#9709) --- .../src/Internal/PathStringHelper.cs | 74 ++++++++++++------- src/Http/Http.Abstractions/src/PathString.cs | 50 ++++++++----- 2 files changed, 80 insertions(+), 44 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs index b6cebb2b9c..39a077f912 100644 --- a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs +++ b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs @@ -1,47 +1,67 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Diagnostics; +using System.Runtime.CompilerServices; + namespace Microsoft.AspNetCore.Http.Internal { - internal class PathStringHelper + internal static class PathStringHelper { - private static bool[] ValidPathChars = { - false, false, false, false, false, false, false, false, // 0x00 - 0x07 - false, false, false, false, false, false, false, false, // 0x08 - 0x0F - false, false, false, false, false, false, false, false, // 0x10 - 0x17 - false, false, false, false, false, false, false, false, // 0x18 - 0x1F - false, true, false, false, true, false, true, true, // 0x20 - 0x27 - true, true, true, true, true, true, true, true, // 0x28 - 0x2F - true, true, true, true, true, true, true, true, // 0x30 - 0x37 - true, true, true, true, false, true, false, false, // 0x38 - 0x3F - true, true, true, true, true, true, true, true, // 0x40 - 0x47 - true, true, true, true, true, true, true, true, // 0x48 - 0x4F - true, true, true, true, true, true, true, true, // 0x50 - 0x57 - true, true, true, false, false, false, false, true, // 0x58 - 0x5F - false, true, true, true, true, true, true, true, // 0x60 - 0x67 - true, true, true, true, true, true, true, true, // 0x68 - 0x6F - true, true, true, true, true, true, true, true, // 0x70 - 0x77 - true, true, true, false, false, false, true, false, // 0x78 - 0x7F + // uint[] bits uses 1 cache line (Array info + 16 bytes) + // bool[] would use 3 cache lines (Array info + 128 bytes) + // So we use 128 bits rather than 128 bytes/bools + private static readonly uint[] ValidPathChars = { + 0b_0000_0000__0000_0000__0000_0000__0000_0000, // 0x00 - 0x1F + 0b_0010_1111__1111_1111__1111_1111__1101_0010, // 0x20 - 0x3F + 0b_1000_0111__1111_1111__1111_1111__1111_1111, // 0x40 - 0x5F + 0b_0100_0111__1111_1111__1111_1111__1111_1110, // 0x60 - 0x7F }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsValidPathChar(char c) { - return c < ValidPathChars.Length && ValidPathChars[c]; + // Use local array and uint .Length compare to elide the bounds check on array access + var validChars = ValidPathChars; + var i = (int)c; + + // Array is in chunks of 32 bits, so get offset by dividing by 32 + var offset = i >> 5; // i / 32; + // Significant bit position is the remainder of the above calc; i % 32 => i & 31 + var significantBit = 1u << (i & 31); + + // Check offset in bounds and check if significant bit set + return (uint)offset < (uint)validChars.Length && + ((validChars[offset] & significantBit) != 0); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsPercentEncodedChar(string str, int index) { - return index < str.Length - 2 - && str[index] == '%' - && IsHexadecimalChar(str[index + 1]) - && IsHexadecimalChar(str[index + 2]); + var len = (uint)str.Length; + if (str[index] == '%' && index < len - 2) + { + return AreFollowingTwoCharsHex(str, index); + } + + return false; } - public static bool IsHexadecimalChar(char c) + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool AreFollowingTwoCharsHex(string str, int index) { - return ('0' <= c && c <= '9') - || ('A' <= c && c <= 'F') - || ('a' <= c && c <= 'f'); + Debug.Assert(index < str.Length - 2); + + var c1 = str[index + 1]; + var c2 = str[index + 2]; + return IsHexadecimalChar(c1) && IsHexadecimalChar(c2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHexadecimalChar(char c) + { + // Between 0 - 9 or uppercased between A - F + return (uint)(c - '0') <= 9 || (uint)((c & ~0x20) - 'A') <= ('F' - 'A'); } } } diff --git a/src/Http/Http.Abstractions/src/PathString.cs b/src/Http/Http.Abstractions/src/PathString.cs index 702307db8f..94b4da4101 100644 --- a/src/Http/Http.Abstractions/src/PathString.cs +++ b/src/Http/Http.Abstractions/src/PathString.cs @@ -16,8 +16,6 @@ namespace Microsoft.AspNetCore.Http [TypeConverter(typeof(PathStringConverter))] public readonly struct PathString : IEquatable { - private static readonly char[] splitChar = { '/' }; - /// /// Represents the empty path. This field is read-only. /// @@ -75,27 +73,46 @@ namespace Microsoft.AspNetCore.Http return string.Empty; } + var value = _value; + var i = 0; + for (; i < value.Length; i++) + { + if (!PathStringHelper.IsValidPathChar(value[i]) || PathStringHelper.IsPercentEncodedChar(value, i)) + { + break; + } + } + + if (i < value.Length) + { + return ToEscapedUriComponent(value, i); + } + + return value; + } + + private static string ToEscapedUriComponent(string value, int i) + { StringBuilder buffer = null; var start = 0; - var count = 0; + var count = i; var requiresEscaping = false; - var i = 0; - while (i < _value.Length) + while (i < value.Length) { - var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(_value, i); - if (PathStringHelper.IsValidPathChar(_value[i]) || isPercentEncodedChar) + var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(value, i); + if (PathStringHelper.IsValidPathChar(value[i]) || isPercentEncodedChar) { if (requiresEscaping) { // the current segment requires escape if (buffer == null) { - buffer = new StringBuilder(_value.Length * 3); + buffer = new StringBuilder(value.Length * 3); } - buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); + buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); requiresEscaping = false; start = i; @@ -120,10 +137,10 @@ namespace Microsoft.AspNetCore.Http // the current segment doesn't require escape if (buffer == null) { - buffer = new StringBuilder(_value.Length * 3); + buffer = new StringBuilder(value.Length * 3); } - buffer.Append(_value, start, count); + buffer.Append(value, start, count); requiresEscaping = true; start = i; @@ -135,9 +152,9 @@ namespace Microsoft.AspNetCore.Http } } - if (count == _value.Length && !requiresEscaping) + if (count == value.Length && !requiresEscaping) { - return _value; + return value; } else { @@ -145,16 +162,16 @@ namespace Microsoft.AspNetCore.Http { if (buffer == null) { - buffer = new StringBuilder(_value.Length * 3); + buffer = new StringBuilder(value.Length * 3); } if (requiresEscaping) { - buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); + buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); } else { - buffer.Append(_value, start, count); + buffer.Append(value, start, count); } } @@ -162,7 +179,6 @@ namespace Microsoft.AspNetCore.Http } } - /// /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any /// value that is not a path.