diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs index e1e3a745e9..690c1788b8 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Routing.Matching { _implementation = new SingleEntryJumpTable(0, -1, "hello-world", 1); _prototype = new SingleEntryAsciiVectorizedJumpTable(0, -2, "hello-world", 1); - _trie = new ILEmitTrieJumpTable(0, -1, new [] { ("hello-world", 1), }, vectorize: false, _implementation); + _trie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: false, _implementation); _vectorTrie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: true, _implementation); _strings = new string[] @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Routing.Matching var segments = _segments; var destination = 0; - for (int i = 0; i < strings.Length; i++) + for (var i = 0; i < strings.Length; i++) { destination = _trie.GetDestination(strings[i], segments[i]); } @@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Routing.Matching var segments = _segments; var destination = 0; - for (int i = 0; i < strings.Length; i++) + for (var i = 0; i < strings.Length; i++) { destination = _vectorTrie.GetDestination(strings[i], segments[i]); } @@ -165,17 +165,16 @@ namespace Microsoft.AspNetCore.Routing.Matching _text = text; _destination = destination; - int length = text.Length; - ReadOnlySpan span = text.ToLowerInvariant().AsSpan(); - ref byte p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + var length = text.Length; + var span = text.ToLowerInvariant().AsSpan(); + ref var p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); _values = new ulong[length / 4]; - for (int i = 0; i < length / 4; i++) + for (var i = 0; i < length / 4; i++) { _values[i] = Unsafe.ReadUnaligned(ref p); p = Unsafe.Add(ref p, 64); } - switch (length % 4) { case 1: @@ -224,11 +223,11 @@ namespace Microsoft.AspNetCore.Routing.Matching public override int GetDestination(string path, PathSegment segment) { - int length = segment.Length; - ReadOnlySpan span = path.AsSpan(segment.Start, length); - ref byte p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + var length = segment.Length; + var span = path.AsSpan(segment.Start, length); + ref var p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); - int i = 0; + var i = 0; while (length > 3) { var value = Unsafe.ReadUnaligned(ref p); @@ -238,10 +237,10 @@ namespace Microsoft.AspNetCore.Routing.Matching return _defaultDestination; } - ulong ulongLowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL); - ulong ulongUpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL); - ulong ulongCombinedIndicator = (ulongLowerIndicator ^ ulongUpperIndicator) & 0x0080008000800080UL; - ulong mask = (ulongCombinedIndicator) >> 2; + var ulongLowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL); + var ulongUpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL); + var ulongCombinedIndicator = (ulongLowerIndicator ^ ulongUpperIndicator) & 0x0080008000800080UL; + var mask = (ulongCombinedIndicator) >> 2; value ^= mask; @@ -259,7 +258,7 @@ namespace Microsoft.AspNetCore.Routing.Matching { case 1: { - char c = Unsafe.ReadUnaligned(ref p); + var c = Unsafe.ReadUnaligned(ref p); if (c != _residue0Lower && c != _residue0Upper) { return _defaultDestination; @@ -270,7 +269,7 @@ namespace Microsoft.AspNetCore.Routing.Matching case 2: { - char c = Unsafe.ReadUnaligned(ref p); + var c = Unsafe.ReadUnaligned(ref p); if (c != _residue0Lower && c != _residue0Upper) { return _defaultDestination; @@ -288,7 +287,7 @@ namespace Microsoft.AspNetCore.Routing.Matching case 3: { - char c = Unsafe.ReadUnaligned(ref p); + var c = Unsafe.ReadUnaligned(ref p); if (c != _residue0Lower && c != _residue0Upper) { return _defaultDestination; diff --git a/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs b/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs index f3400238c7..3b3528932f 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs @@ -16,8 +16,11 @@ namespace Microsoft.AspNetCore.Routing.Matching { // The algorthm we use only works for ASCII text. If we find non-ASCII text in the input // we need to reject it and let is be processed with a fallback technique. - public const int NotAscii = Int32.MinValue; + public const int NotAscii = int.MinValue; + // Creates a Func of (string path, int start, int length) => destination + // Not using PathSegment here because we don't want to mess with visibility checks and + // generating IL without it is easier. public static Func Create( int defaultDestination, int exitDestination, @@ -38,6 +41,21 @@ namespace Microsoft.AspNetCore.Routing.Matching return (Func)method.CreateDelegate(typeof(Func)); } + // Internal for testing + internal static bool ShouldVectorize((string text, int destination)[] entries) + { + // There's no value in vectorizing the computation if we're on 32bit or + // if no string is long enough. We do the vectorized comparison with uint64 ulongs + // which isn't beneficial if they don't map to the native size of the CPU. The + // vectorized algorithm introduces additional overhead for casing. + + // Vectorize by default on 64bit (allow override for testing) + return (IntPtr.Size == 8) && + + // Don't vectorize if all of the strings are small (prevents allocating unused locals) + entries.Any(e => e.text.Length >= 4); + } + private static void GenerateMethodBody( ILGenerator il, int defaultDestination, @@ -45,16 +63,8 @@ namespace Microsoft.AspNetCore.Routing.Matching (string text, int destination)[] entries, bool? vectorize) { - // There's no value in vectorizing the computation if we're on 32bit or - // if no string is long enough. We do the vectorized comparison with uint64 ulongs - // which isn't beneficial if they don't map to the native size of the CPU. The - // vectorized algorithm introduces additional overhead for casing. - // - // Vectorize by default on 64bit (allow override for testing) - vectorize = vectorize ?? (IntPtr.Size == 8); - // Don't vectorize if all of the strings are small (prevents allocating unused locals) - vectorize &= entries.Any(e => e.text.Length >= 4); + vectorize = vectorize ?? ShouldVectorize(entries); // See comments on Locals for details var locals = new Locals(il, vectorize.Value); @@ -428,32 +438,32 @@ namespace Microsoft.AspNetCore.Routing.Matching /// /// Holds current character when processing a character at a time. /// - public LocalBuilder UInt16Value { get; set; } + public LocalBuilder UInt16Value { get; } /// /// Holds current character when processing 4 characters at a time. /// - public LocalBuilder UInt64Value { get; set; } + public LocalBuilder UInt64Value { get; } /// /// Used to covert casing. See comments where it's used. /// - public LocalBuilder UInt64LowerIndicator { get; set; } - + public LocalBuilder UInt64LowerIndicator { get; } + /// /// Used to covert casing. See comments where it's used. /// - public LocalBuilder UInt64UpperIndicator { get; set; } + public LocalBuilder UInt64UpperIndicator { get; } /// /// Holds a 'ref byte' reference to the current character (in bytes). /// - public LocalBuilder P { get; set; } + public LocalBuilder P { get; } /// /// Holds the relevant portion of the path as a Span[byte]. /// - public LocalBuilder Span { get; set; } + public LocalBuilder Span { get; } } private class Labels @@ -471,6 +481,8 @@ namespace Microsoft.AspNetCore.Routing.Matching private class Methods { + // Caching because the methods won't change, if we're being called once we're likely to + // be called again. public static readonly Methods Instance = new Methods(); private Methods() diff --git a/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs index 2bd5951aca..3bd29c9695 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs @@ -54,6 +54,8 @@ namespace Microsoft.AspNetCore.Routing.Matching return _getDestination(path, segment); } + // Used when we haven't yet initialized the IL trie. We defer compilation of the IL for startup + // performance. private int FallbackGetDestination(string path, PathSegment segment) { if (path.Length == 0) diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs new file mode 100644 index 0000000000..738646dc11 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs @@ -0,0 +1,53 @@ +// 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. + +#if IL_EMIT +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + public class ILEmitTrieFactoryTest + { + // We never vectorize on 32bit, so that's part of the test. + [Fact] + public void ShouldVectorize_ReturnsTrue_ForLargeEnoughStrings() + { + // Arrange + var is64Bit = IntPtr.Size == 8; + var expected = is64Bit; + + var entries = new[] + { + ("foo", 0), + ("badr", 0), + ("", 0), + }; + + // Act + var actual = ILEmitTrieFactory.ShouldVectorize(entries); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void ShouldVectorize_ReturnsFalseForSmallStrings() + { + // Arrange + var entries = new[] + { + ("foo", 0), + ("sma", 0), + ("", 0), + }; + + // Act + var actual = ILEmitTrieFactory.ShouldVectorize(entries); + + // Assert + Assert.False(actual); + } + } +} +#endif