Add vectorized il-emit trie jump table

Add new futuristic jump table. Remove old experimental jump tables since
this is much much better.
This commit is contained in:
Ryan Nowak 2018-08-27 14:52:16 -07:00
Родитель 4e9e33a223
Коммит 3511c8cef0
15 изменённых файлов: 1291 добавлений и 530 удалений

Просмотреть файл

@ -18,6 +18,6 @@
<PackageSigningCertName>MicrosoftNuGet</PackageSigningCertName>
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<LangVersion>7.2</LangVersion>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
</Project>

Просмотреть файл

@ -1,172 +0,0 @@
// 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;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing.Matching
{
// An optimized jump table that trades a small amount of additional memory for
// hash-table like performance.
//
// The optimization here is to use the first character of the known entries
// as a 'key' in the hash table in the space of A-Z. This gives us a maximum
// of 26 buckets (hence the reduced memory)
internal class AsciiKeyedJumpTable : JumpTable
{
public static bool TryCreate(
int defaultDestination,
int exitDestination,
List<(string text, int destination)> entries,
out JumpTable result)
{
result = null;
// First we group string by their uppercase letter. If we see a string
// that starts with a non-ASCII letter
var map = new Dictionary<char, List<(string text, int destination)>>();
for (var i = 0; i < entries.Count; i++)
{
if (entries[i].text.Length == 0)
{
return false;
}
if (!IsAscii(entries[i].text))
{
return false;
}
var first = ToUpperAscii(entries[i].text[0]);
if (first < 'A' || first > 'Z')
{
// Not a letter
return false;
}
if (!map.TryGetValue(first, out var matches))
{
matches = new List<(string text, int destination)>();
map.Add(first, matches);
}
matches.Add(entries[i]);
}
var next = 0;
var ordered = new(string text, int destination)[entries.Count];
var indexes = new int[26 * 2];
for (var i = 0; i < 26; i++)
{
indexes[i * 2] = next;
var length = 0;
if (map.TryGetValue((char)('A' + i), out var matches))
{
length += matches.Count;
for (var j = 0; j < matches.Count; j++)
{
ordered[next++] = matches[j];
}
}
indexes[i * 2 + 1] = length;
}
result = new AsciiKeyedJumpTable(defaultDestination, exitDestination, ordered, indexes);
return true;
}
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly (string text, int destination)[] _entries;
private readonly int[] _indexes;
private AsciiKeyedJumpTable(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries,
int[] indexes)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
_entries = entries;
_indexes = indexes;
}
public override int GetDestination(string path, PathSegment segment)
{
if (segment.Length == 0)
{
return _exitDestination;
}
var c = path[segment.Start];
if (!IsAscii(c))
{
return _defaultDestination;
}
c = ToUpperAscii(c);
if (c < 'A' || c > 'Z')
{
// Character is non-ASCII or not a letter. Since we know that all of the entries are ASCII
// and begin with a letter this is not a match.
return _defaultDestination;
}
var offset = (c - 'A') * 2;
var start = _indexes[offset];
var length = _indexes[offset + 1];
var entries = _entries;
for (var i = start; i < start + length; i++)
{
var text = entries[i].text;
if (segment.Length == text.Length &&
string.Compare(
path,
segment.Start,
text,
0,
segment.Length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return entries[i].destination;
}
}
return _defaultDestination;
}
internal static bool IsAscii(char c)
{
// ~0x7F is a bit mask that checks for bits that won't be set in an ASCII character.
// ASCII only uses the lowest 7 bits.
return (c & ~0x7F) == 0;
}
internal static bool IsAscii(string text)
{
for (var i = 0; i < text.Length; i++)
{
var c = text[i];
if (!IsAscii(c))
{
return false;
}
}
return true;
}
internal static char ToUpperAscii(char c)
{
// 0x5F can be used to convert a character to uppercase ascii (assuming it's a letter).
// This works because lowercase ASCII chars are exactly 32 less than their uppercase
// counterparts.
return (char)(c & 0x5F);
}
}
}

Просмотреть файл

@ -1,197 +0,0 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.AspNetCore.Routing.Matching
{
internal class CustomHashTableJumpTable : JumpTable
{
// Similar to HashHelpers list of primes, but truncated. We don't expect
// incredibly large numbers to be useful here.
private static readonly int[] Primes = new int[]
{
3, 7, 11, 17, 23, 29, 37, 47, 59,
71, 89, 107, 131, 163, 197, 239, 293,
353, 431, 521, 631, 761, 919, 1103, 1327,
1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839,
7013, 8419, 10103,
};
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly int _prime;
private readonly int[] _buckets;
private readonly Entry[] _entries;
public CustomHashTableJumpTable(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
var map = new Dictionary<int, List<(string text, int destination)>>();
for (var i = 0; i < entries.Length; i++)
{
var key = GetKey(entries[i].text, new PathSegment(0, entries[i].text.Length));
if (!map.TryGetValue(key, out var matches))
{
matches = new List<(string text, int destination)>();
map.Add(key, matches);
}
matches.Add(entries[i]);
}
_prime = GetPrime(map.Count);
_buckets = new int[_prime + 1];
_entries = new Entry[map.Sum(kvp => kvp.Value.Count)];
var next = 0;
foreach (var group in map.GroupBy(kvp => kvp.Key % _prime).OrderBy(g => g.Key))
{
_buckets[group.Key] = next;
foreach (var array in group)
{
for (var i = 0; i < array.Value.Count; i++)
{
_entries[next++] = new Entry(array.Value[i].text, array.Value[i].destination);
}
}
}
Debug.Assert(next == _entries.Length);
_buckets[_prime] = next;
var last = 0;
for (var i = 0; i < _buckets.Length; i++)
{
if (_buckets[i] == 0)
{
_buckets[i] = last;
}
else
{
last = _buckets[i];
}
}
}
public int Find(int key)
{
return key % _prime;
}
private static int GetPrime(int capacity)
{
for (int i = 0; i < Primes.Length; i++)
{
int prime = Primes[i];
if (prime >= capacity)
{
return prime;
}
}
return Primes[Primes.Length - 1];
}
public override int GetDestination(string path, PathSegment segment)
{
if (segment.Length == 0)
{
return _exitDestination;
}
var key = GetKey(path, segment);
var index = Find(key);
var start = _buckets[index];
var end = _buckets[index + 1];
var entries = _entries.AsSpan(start, end - start);
for (var i = 0; i < entries.Length; i++)
{
var text = entries[i].Text;
if (text.Length == segment.Length &&
string.Compare(
path,
segment.Start,
text,
0,
segment.Length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return entries[i].Destination;
}
}
return _defaultDestination;
}
// Builds a hashcode from segment (first four characters converted to 8bit ASCII)
private static unsafe int GetKey(string path, PathSegment segment)
{
fixed (char* p = path)
{
switch (path.Length)
{
case 0:
{
return 0;
}
case 1:
{
return
((*(p + segment.Start + 0) & 0x5F) << (0 * 8));
}
case 2:
{
return
((*(p + segment.Start + 0) & 0x5F) << (0 * 8)) |
((*(p + segment.Start + 1) & 0x5F) << (1 * 8));
}
case 3:
{
return
((*(p + segment.Start + 0) & 0x5F) << (0 * 8)) |
((*(p + segment.Start + 1) & 0x5F) << (1 * 8)) |
((*(p + segment.Start + 2) & 0x5F) << (2 * 8));
}
default:
{
return
((*(p + segment.Start + 0) & 0x5F) << (0 * 8)) |
((*(p + segment.Start + 1) & 0x5F) << (1 * 8)) |
((*(p + segment.Start + 2) & 0x5F) << (2 * 8)) |
((*(p + segment.Start + 3) & 0x5F) << (3 * 8));
}
}
}
}
private readonly struct Entry
{
public readonly string Text;
public readonly int Destination;
public Entry(string text, int destination)
{
Text = text;
Destination = destination;
}
}
}
}

Просмотреть файл

@ -1,112 +0,0 @@
// 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;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Routing.Matching
{
internal class DictionaryLookupJumpTable : JumpTable
{
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly Dictionary<int, (string text, int destination)[]> _store;
public DictionaryLookupJumpTable(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
var map = new Dictionary<int, List<(string text, int destination)>>();
for (var i = 0; i < entries.Length; i++)
{
var key = GetKey(entries[i].text.AsSpan());
if (!map.TryGetValue(key, out var matches))
{
matches = new List<(string text, int destination)>();
map.Add(key, matches);
}
matches.Add(entries[i]);
}
_store = map.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray());
}
public override int GetDestination(string path, PathSegment segment)
{
if (segment.Length == 0)
{
return _exitDestination;
}
var key = GetKey(path.AsSpan(segment.Start, segment.Length));
if (_store.TryGetValue(key, out var entries))
{
for (var i = 0; i < entries.Length; i++)
{
var text = entries[i].text;
if (text.Length == segment.Length &&
string.Compare(
path,
segment.Start,
text,
0,
segment.Length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return entries[i].destination;
}
}
}
return _defaultDestination;
}
private static int GetKey(string path, PathSegment segment)
{
return GetKey(path.AsSpan(segment.Start, segment.Length));
}
/// builds a key from the last byte of length + first 3 characters of text (converted to ascii)
private static int GetKey(ReadOnlySpan<char> span)
{
var length = (byte)(span.Length & 0xFF);
byte c0, c1, c2;
switch (length)
{
case 0:
{
return 0;
}
case 1:
{
c0 = (byte)(span[0] & 0x5F);
return (length << 24) | (c0 << 16);
}
case 2:
{
c0 = (byte)(span[0] & 0x5F);
c1 = (byte)(span[1] & 0x5F);
return (length << 24) | (c0 << 16) | (c1 << 8);
}
default:
{
c0 = (byte)(span[0] & 0x5F);
c1 = (byte)(span[1] & 0x5F);
c2 = (byte)(span[2] & 0x5F);
return (length << 24) | (c0 << 16) | (c1 << 8) | c2;
}
}
}
}
}

Просмотреть файл

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matching
@ -15,12 +14,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
private JumpTable _linearSearch;
private JumpTable _dictionary;
private JumpTable _ascii;
private JumpTable _dictionaryLookup;
private JumpTable _customHashTable;
private JumpTable _trie;
private JumpTable _vectorTrie;
// All factors of 100 to support sampling
[Params(2, 4, 5, 10, 25)]
[Params(2, 5, 10, 25, 50, 100)]
public int Count;
[GlobalSetup]
@ -48,9 +46,8 @@ namespace Microsoft.AspNetCore.Routing.Matching
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
AsciiKeyedJumpTable.TryCreate(0, -1, entries, out _ascii);
_dictionaryLookup = new DictionaryLookupJumpTable(0, -1, entries.ToArray());
_customHashTable = new CustomHashTableJumpTable(0, -1, entries.ToArray());
_trie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: false, _dictionary);
_vectorTrie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: true, _dictionary);
}
// This baseline is similar to SingleEntryJumpTable. We just want
@ -67,15 +64,24 @@ namespace Microsoft.AspNetCore.Routing.Matching
var @string = strings[i];
var segment = segments[i];
destination = segment.Length == 0 ? -1 :
segment.Length != @string.Length ? 1 :
string.Compare(
if (segment.Length == 0)
{
destination = -1;
}
else if (segment.Length != @string.Length)
{
destination = 1;
}
else
{
destination = string.Compare(
@string,
segment.Start,
@string,
0,
@string.Length,
segment.Length,
StringComparison.OrdinalIgnoreCase);
}
}
return destination;
@ -112,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
[Benchmark(OperationsPerInvoke = 100)]
public int Ascii()
public int Trie()
{
var strings = _strings;
var segments = _segments;
@ -120,14 +126,14 @@ namespace Microsoft.AspNetCore.Routing.Matching
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _ascii.GetDestination(strings[i], segments[i]);
destination = _trie.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 100)]
public int DictionaryLookup()
public int VectorTrie()
{
var strings = _strings;
var segments = _segments;
@ -135,22 +141,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _dictionaryLookup.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 100)]
public int CustomHashTable()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _customHashTable.GetDestination(strings[i], segments[i]);
destination = _vectorTrie.GetDestination(strings[i], segments[i]);
}
return destination;
@ -164,7 +155,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
var guid = Guid.NewGuid().ToString();
// Between 5 and 36 characters
var text = guid.Substring(0, Math.Max(5, Math.Min(count, 36)));
var text = guid.Substring(0, Math.Max(5, Math.Min(i, 36)));
if (char.IsDigit(text[0]))
{
// Convert first character to a letter.

Просмотреть файл

@ -2,20 +2,30 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matching
{
public class JumpTableSingleEntryBenchmark
{
private JumpTable _table;
private JumpTable _implementation;
private JumpTable _prototype;
private JumpTable _trie;
private JumpTable _vectorTrie;
private string[] _strings;
private PathSegment[] _segments;
[GlobalSetup]
public void Setup()
{
_table = new SingleEntryJumpTable(0, -1, "hello-world", 1);
_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);
_vectorTrie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: true, _implementation);
_strings = new string[]
{
"index/foo/2",
@ -40,21 +50,30 @@ namespace Microsoft.AspNetCore.Routing.Matching
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
int destination = 0;
for (int i = 0; i < strings.Length; i++)
{
var @string = strings[i];
var segment = segments[i];
destination = segment.Length == 0 ? -1 :
segment.Length != 11 ? 1 :
string.Compare(
if (segment.Length == 0)
{
destination = -1;
}
else if (segment.Length != "hello-world".Length)
{
destination = 1;
}
else
{
destination = string.Compare(
@string,
segment.Start,
"hello-world",
0,
segment.Length,
StringComparison.OrdinalIgnoreCase);
}
}
return destination;
@ -67,12 +86,234 @@ namespace Microsoft.AspNetCore.Routing.Matching
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
for (int i = 0; i < strings.Length; i++)
{
destination = _table.GetDestination(strings[i], segments[i]);
destination = _implementation.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 5)]
public int Prototype()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (int i = 0; i < strings.Length; i++)
{
destination = _prototype.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 5)]
public int Trie()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (int i = 0; i < strings.Length; i++)
{
destination = _trie.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 5)]
public int VectorTrie()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (int i = 0; i < strings.Length; i++)
{
destination = _vectorTrie.GetDestination(strings[i], segments[i]);
}
return destination;
}
private class SingleEntryAsciiVectorizedJumpTable : JumpTable
{
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly string _text;
private readonly int _destination;
private readonly ulong[] _values;
private readonly int _residue0Lower;
private readonly int _residue0Upper;
private readonly int _residue1Lower;
private readonly int _residue1Upper;
private readonly int _residue2Lower;
private readonly int _residue2Upper;
public SingleEntryAsciiVectorizedJumpTable(
int defaultDestination,
int exitDestination,
string text,
int destination)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
_text = text;
_destination = destination;
int length = text.Length;
ReadOnlySpan<char> span = text.ToLowerInvariant().AsSpan();
ref byte p = ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(span));
_values = new ulong[length / 4];
for (int i = 0; i < length / 4; i++)
{
_values[i] = Unsafe.ReadUnaligned<ulong>(ref p);
p = Unsafe.Add(ref p, 64);
}
switch (length % 4)
{
case 1:
{
var c = Unsafe.ReadUnaligned<char>(ref p);
_residue0Lower = char.ToLowerInvariant(c);
_residue0Upper = char.ToUpperInvariant(c);
break;
}
case 2:
{
var c = Unsafe.ReadUnaligned<char>(ref p);
_residue0Lower = char.ToLowerInvariant(c);
_residue0Upper = char.ToUpperInvariant(c);
p = Unsafe.Add(ref p, 2);
c = Unsafe.ReadUnaligned<char>(ref p);
_residue1Lower = char.ToLowerInvariant(c);
_residue1Upper = char.ToUpperInvariant(c);
break;
}
case 3:
{
var c = Unsafe.ReadUnaligned<char>(ref p);
_residue0Lower = char.ToLowerInvariant(c);
_residue0Upper = char.ToUpperInvariant(c);
p = Unsafe.Add(ref p, 2);
c = Unsafe.ReadUnaligned<char>(ref p);
_residue1Lower = char.ToLowerInvariant(c);
_residue1Upper = char.ToUpperInvariant(c);
p = Unsafe.Add(ref p, 2);
c = Unsafe.ReadUnaligned<char>(ref p);
_residue2Lower = char.ToLowerInvariant(c);
_residue2Upper = char.ToUpperInvariant(c);
break;
}
}
}
public override int GetDestination(string path, PathSegment segment)
{
int length = segment.Length;
ReadOnlySpan<char> span = path.AsSpan(segment.Start, length);
ref byte p = ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(span));
int i = 0;
while (length > 3)
{
var value = Unsafe.ReadUnaligned<ulong>(ref p);
if ((value & ~0x007F007F007F007FUL) == 0)
{
return _defaultDestination;
}
ulong ulongLowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL);
ulong ulongUpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL);
ulong ulongCombinedIndicator = (ulongLowerIndicator ^ ulongUpperIndicator) & 0x0080008000800080UL;
ulong mask = (ulongCombinedIndicator) >> 2;
value ^= mask;
if (value != _values[i])
{
return _defaultDestination;
}
i++;
length -= 4;
p = ref Unsafe.Add(ref p, 64);
}
switch (length)
{
case 1:
{
char c = Unsafe.ReadUnaligned<char>(ref p);
if (c != _residue0Lower && c != _residue0Upper)
{
return _defaultDestination;
}
break;
}
case 2:
{
char c = Unsafe.ReadUnaligned<char>(ref p);
if (c != _residue0Lower && c != _residue0Upper)
{
return _defaultDestination;
}
p = ref Unsafe.Add(ref p, 2);
c = Unsafe.ReadUnaligned<char>(ref p);
if (c != _residue1Lower && c != _residue1Upper)
{
return _defaultDestination;
}
break;
}
case 3:
{
char c = Unsafe.ReadUnaligned<char>(ref p);
if (c != _residue0Lower && c != _residue0Upper)
{
return _defaultDestination;
}
p = ref Unsafe.Add(ref p, 2);
c = Unsafe.ReadUnaligned<char>(ref p);
if (c != _residue1Lower && c != _residue1Upper)
{
return _defaultDestination;
}
p = ref Unsafe.Add(ref p, 2);
c = Unsafe.ReadUnaligned<char>(ref p);
if (c != _residue2Lower && c != _residue2Upper)
{
return _defaultDestination;
}
break;
}
}
return _destination;
}
}
}
}

Просмотреть файл

@ -38,6 +38,8 @@
<MoqPackageVersion>4.7.49</MoqPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
<SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
<SystemReflectionEmitLightweightPackageVersion>4.3.0</SystemReflectionEmitLightweightPackageVersion>
<XunitAnalyzersPackageVersion>0.10.0</XunitAnalyzersPackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>

Просмотреть файл

@ -0,0 +1,588 @@
// 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 System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Microsoft.AspNetCore.Routing.Matching
{
internal static class ILEmitTrieFactory
{
// 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 static Func<string, int, int, int> Create(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries,
bool? vectorize)
{
var method = new DynamicMethod(
"GetDestination",
typeof(int),
new[] { typeof(string), typeof(int), typeof(int), });
GenerateMethodBody(method.GetILGenerator(), defaultDestination, exitDestination, entries, vectorize);
#if IL_EMIT_SAVE_ASSEMBLY
SaveAssembly(method.GetILGenerator(), defaultDestination, exitDestination, entries, vectorize);
#endif
return (Func<string, int, int, int>)method.CreateDelegate(typeof(Func<string, int, int, int>));
}
private static void GenerateMethodBody(
ILGenerator il,
int defaultDestination,
int exitDestination,
(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);
// See comments on Locals for details
var locals = new Locals(il, vectorize.Value);
// See comments on Labels for details
var labels = new Labels()
{
ReturnDefault = il.DefineLabel(),
ReturnNotAscii = il.DefineLabel(),
};
// See comments on Methods for details
var methods = Methods.Instance;
// Initializing top-level locals - this is similar to...
// ReadOnlySpan<char> span = arg0.AsSpan(arg1, arg2);
// ref byte p = ref Unsafe.As<char, byte>(MemoryMarshal.GetReference<char>(span))
// arg0.AsSpan(arg1, arg2)
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Call, methods.AsSpan);
// ReadOnlySpan<char> = ...
il.Emit(OpCodes.Stloc, locals.Span);
// MemoryMarshal.GetReference<char>(span)
il.Emit(OpCodes.Ldloc, locals.Span);
il.Emit(OpCodes.Call, methods.GetReference);
// Unsafe.As<char, byte>(...)
il.Emit(OpCodes.Call, methods.As);
// ref byte p = ...
il.Emit(OpCodes.Stloc_0, locals.P);
var groups = entries.GroupBy(e => e.text.Length).ToArray();
for (var i = 0; i < groups.Length; i++)
{
var group = groups[i];
// Similar to 'if (length != X) { ... }
var inside = il.DefineLabel();
var next = il.DefineLabel();
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldc_I4, group.Key);
il.Emit(OpCodes.Beq, inside);
il.Emit(OpCodes.Br, next);
// Process the group
il.MarkLabel(inside);
EmitTable(il, group.ToArray(), 0, group.Key, locals, labels, methods);
il.MarkLabel(next);
}
// Exit point - we end up here when the text doesn't match
il.MarkLabel(labels.ReturnDefault);
il.Emit(OpCodes.Ldc_I4, defaultDestination);
il.Emit(OpCodes.Ret);
// Exit point - we end up here with the text contains non-ASCII text
il.MarkLabel(labels.ReturnNotAscii);
il.Emit(OpCodes.Ldc_I4, NotAscii);
il.Emit(OpCodes.Ret);
}
private static void EmitTable(
ILGenerator il,
(string text, int destination)[] entries,
int index,
int length,
Locals locals,
Labels labels,
Methods methods)
{
// We've reached the end of the string.
if (index == length)
{
EmitReturnDestination(il, entries);
return;
}
// If 4 or more characters remain, and we're vectorizing, we should process 4 characters at a time.
if (length - index >= 4 && locals.UInt64Value != null)
{
EmitVectorizedTable(il, entries, index, length, locals, labels, methods);
return;
}
// Fall back to processing a character at a time.
EmitSingleCharacterTable(il, entries, index, length, locals, labels, methods);
}
private static void EmitVectorizedTable(
ILGenerator il,
(string text, int destination)[] entries,
int index,
int length,
Locals locals,
Labels labels,
Methods methods)
{
// Emits code similar to:
//
// uint64Value = Unsafe.ReadUnaligned<ulong>(ref p);
// p = ref Unsafe.Add(ref p, 8);
//
// if ((uint64Value & ~0x007F007F007F007FUL) == 0)
// {
// return NotAscii;
// }
// uint64LowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL);
// uint64UpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL);
// ulong temp1 = uint64LowerIndicator ^ uint64UpperIndicator
// ulong temp2 = temp1 & 0x0080008000800080UL;
// ulong temp3 = (temp2) >> 2;
// uint64Value = uint64Value ^ temp3;
//
// This is a vectorized non-branching technique for processing 4 utf16 characters
// at a time inside a single uint64.
//
// Similar to:
// https://github.com/GrabYourPitchforks/coreclr/commit/a3c1df25c4225995ffd6b18fd0fc39d6b81fd6a5#diff-d89b6ca07ea349899e45eed5f688a7ebR81
//
// Basically we need to check if the text is non-ASCII first and bail if it is.
// The rest of the steps will convert the text to lowercase by checking all characters
// at a time to see if they are in the A-Z range, that's where 0x0041 and 0x005B come in.
// IMPORTANT
//
// If you are modifying this code, be aware that the easiest way to make a mistake is by
// getting the set of casts wrong doing something like:
//
// il.Emit(OpCodes.Ldc_I8, ~0x007F007F007F007FUL);
//
// The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve
// an overload that does an undesirable conversion (for instance convering ulong to float).
//
// IMPORTANT
// Unsafe.ReadUnaligned<ulong>(ref p)
il.Emit(OpCodes.Ldloc, locals.P);
il.Emit(OpCodes.Call, methods.ReadUnalignedUInt64);
// uint64Value = ...
il.Emit(OpCodes.Stloc, locals.UInt64Value);
// Unsafe.Add(ref p, 8)
il.Emit(OpCodes.Ldloc, locals.P);
il.Emit(OpCodes.Ldc_I4, 8); // 8 bytes were read
il.Emit(OpCodes.Call, methods.Add);
// p = ref ...
il.Emit(OpCodes.Stloc, locals.P);
// if ((uint64Value & ~0x007F007F007F007FUL) == 0)
// {
// goto: NotAscii;
// }
il.Emit(OpCodes.Ldloc, locals.UInt64Value);
il.Emit(OpCodes.Ldc_I8, unchecked((long)~0x007F007F007F007FUL));
il.Emit(OpCodes.And);
il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii);
// uint64Value + (0x0080008000800080UL - 0x0041004100410041UL)
il.Emit(OpCodes.Ldloc, locals.UInt64Value);
il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x0041004100410041UL)));
il.Emit(OpCodes.Add);
// uint64LowerIndicator = ...
il.Emit(OpCodes.Stloc, locals.UInt64LowerIndicator);
// value + (0x0080008000800080UL - 0x005B005B005B005BUL)
il.Emit(OpCodes.Ldloc, locals.UInt64Value);
il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x005B005B005B005BUL)));
il.Emit(OpCodes.Add);
// uint64UpperIndicator = ...
il.Emit(OpCodes.Stloc, locals.UInt64UpperIndicator);
// ulongLowerIndicator ^ ulongUpperIndicator
il.Emit(OpCodes.Ldloc, locals.UInt64LowerIndicator);
il.Emit(OpCodes.Ldloc, locals.UInt64UpperIndicator);
il.Emit(OpCodes.Xor);
// ... & 0x0080008000800080UL
il.Emit(OpCodes.Ldc_I8, unchecked((long)0x0080008000800080UL));
il.Emit(OpCodes.And);
// ... >> 2;
il.Emit(OpCodes.Ldc_I4, 2);
il.Emit(OpCodes.Shr_Un);
// ... ^ uint64Value
il.Emit(OpCodes.Ldloc, locals.UInt64Value);
il.Emit(OpCodes.Xor);
// uint64Value = ...
il.Emit(OpCodes.Stloc, locals.UInt64Value);
// Now we generate an 'if' ladder with an entry for each of the unique 64 bit sections
// of the text.
var groups = entries.GroupBy(e => GetUInt64Key(e.text, index));
foreach (var group in groups)
{
// if (uint64Value == 0x.....) { ... }
var next = il.DefineLabel();
il.Emit(OpCodes.Ldloc, locals.UInt64Value);
il.Emit(OpCodes.Ldc_I8, unchecked((long)group.Key));
il.Emit(OpCodes.Bne_Un, next);
// Process the group
EmitTable(il, group.ToArray(), index + 4, length, locals, labels, methods);
il.MarkLabel(next);
}
// goto: defaultDestination
il.Emit(OpCodes.Br, labels.ReturnDefault);
}
private static void EmitSingleCharacterTable(
ILGenerator il,
(string text, int destination)[] entries,
int index,
int length,
Locals locals,
Labels labels,
Methods methods)
{
// See the vectorized code path for a much more thorough explanation.
// IMPORTANT
//
// If you are modifying this code, be aware that the easiest way to make a mistake is by
// getting the set of casts wrong doing something like:
//
// il.Emit(OpCodes.Ldc_I4, ~0x007F);
//
// The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve
// an overload that does an undesirable conversion (for instance convering ulong to float).
//
// IMPORTANT
// Unsafe.ReadUnaligned<ushort>(ref p)
il.Emit(OpCodes.Ldloc, locals.P);
il.Emit(OpCodes.Call, methods.ReadUnalignedUInt16);
// uint16Value = ...
il.Emit(OpCodes.Stloc, locals.UInt16Value);
// Unsafe.Add(ref p, 2)
il.Emit(OpCodes.Ldloc, locals.P);
il.Emit(OpCodes.Ldc_I4, 2); // 2 bytes were read
il.Emit(OpCodes.Call, methods.Add);
// p = ref ...
il.Emit(OpCodes.Stloc, locals.P);
// if ((uInt16Value & ~0x007FUL) == 0)
// {
// goto: NotAscii;
// }
il.Emit(OpCodes.Ldloc, locals.UInt16Value);
il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)~0x007F)));
il.Emit(OpCodes.And);
il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii);
// Since we're handling a single character at a time, it's easier to just
// generate an 'if' with two comparisons instead of doing complicated conversion
// logic.
// Now we generate an 'if' ladder with an entry for each of the unique
// characters in the group.
var groups = entries.GroupBy(e => GetUInt16Key(e.text, index));
foreach (var group in groups)
{
// if (uInt16Value == 'A' || uint16Value == 'a') { ... }
var next = il.DefineLabel();
var inside = il.DefineLabel();
il.Emit(OpCodes.Ldloc, locals.UInt16Value);
il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)group.Key)));
il.Emit(OpCodes.Beq, inside);
var upper = (ushort)char.ToUpperInvariant((char)group.Key);
if (upper != group.Key)
{
il.Emit(OpCodes.Ldloc, locals.UInt16Value);
il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)upper)));
il.Emit(OpCodes.Beq, inside);
}
il.Emit(OpCodes.Br, next);
// Process the group
il.MarkLabel(inside);
EmitTable(il, group.ToArray(), index + 1, length, locals, labels, methods);
il.MarkLabel(next);
}
// goto: defaultDestination
il.Emit(OpCodes.Br, labels.ReturnDefault);
}
public static void EmitReturnDestination(ILGenerator il, (string text, int destination)[] entries)
{
Debug.Assert(entries.Length == 1, "We should have a single entry");
il.Emit(OpCodes.Ldc_I4, entries[0].destination);
il.Emit(OpCodes.Ret);
}
private static ulong GetUInt64Key(string text, int index)
{
Debug.Assert(index + 4 <= text.Length);
var span = text.ToLowerInvariant().AsSpan(index);
ref var p = ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(span));
return Unsafe.ReadUnaligned<ulong>(ref p);
}
private static ushort GetUInt16Key(string text, int index)
{
Debug.Assert(index + 1 <= text.Length);
return (ushort)char.ToLowerInvariant(text[index]);
}
// We require a special build-time define since this is a testing/debugging
// feature that will litter the app directory with assemblies.
#if IL_EMIT_SAVE_ASSEMBLY
private static void SaveAssembly(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries,
bool? vectorize)
{
var assemblyName = "Microsoft.AspNetCore.Routing.ILEmitTrie" + DateTime.Now.Ticks;
var fileName = assemblyName + ".dll";
var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.RunAndSave);
var module = assembly.DefineDynamicModule(assemblyName, fileName);
var type = module.DefineType("ILEmitTrie");
var method = type.DefineMethod(
"GetDestination",
MethodAttributes.Public | MethodAttributes.Static,
CallingConventions.Standard,
typeof(int),
new [] { typeof(string), typeof(int), typeof(int), };
GenerateMethodBody(method.GetILGenerator(), defaultDestination, exitDestination, entries, vectorize);
type.CreateTypeInfo();
assembly.Save(fileName);
}
#endif
private class Locals
{
public Locals(ILGenerator il, bool vectorize)
{
P = il.DeclareLocal(typeof(byte).MakeByRefType());
Span = il.DeclareLocal(typeof(ReadOnlySpan<char>));
UInt16Value = il.DeclareLocal(typeof(ushort));
if (vectorize)
{
UInt64Value = il.DeclareLocal(typeof(ulong));
UInt64LowerIndicator = il.DeclareLocal(typeof(ulong));
UInt64UpperIndicator = il.DeclareLocal(typeof(ulong));
}
}
/// <summary>
/// Holds current character when processing a character at a time.
/// </summary>
public LocalBuilder UInt16Value { get; set; }
/// <summary>
/// Holds current character when processing 4 characters at a time.
/// </summary>
public LocalBuilder UInt64Value { get; set; }
/// <summary>
/// Used to covert casing. See comments where it's used.
/// </summary>
public LocalBuilder UInt64LowerIndicator { get; set; }
/// <summary>
/// Used to covert casing. See comments where it's used.
/// </summary>
public LocalBuilder UInt64UpperIndicator { get; set; }
/// <summary>
/// Holds a 'ref byte' reference to the current character (in bytes).
/// </summary>
public LocalBuilder P { get; set; }
/// <summary>
/// Holds the relevant portion of the path as a Span[byte].
/// </summary>
public LocalBuilder Span { get; set; }
}
private class Labels
{
/// <summary>
/// Label to goto that will return the default destination (not a match).
/// </summary>
public Label ReturnDefault { get; set; }
/// <summary>
/// Label to goto that will return a sentinel value for non-ascii text.
/// </summary>
public Label ReturnNotAscii { get; set; }
}
private class Methods
{
public static readonly Methods Instance = new Methods();
private Methods()
{
// Can't use GetMethod because the parameter is a generic method parameters.
Add = typeof(Unsafe)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.Name == nameof(Unsafe.Add))
.Where(m => m.GetGenericArguments().Length == 1)
.Where(m => m.GetParameters().Length == 2)
.FirstOrDefault()
?.MakeGenericMethod(typeof(byte));
if (Add == null)
{
throw new InvalidOperationException("Failed to find Unsafe.Add{T}(ref T, int)");
}
// Can't use GetMethod because the parameter is a generic method parameters.
As = typeof(Unsafe)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.Name == nameof(Unsafe.As))
.Where(m => m.GetGenericArguments().Length == 2)
.Where(m => m.GetParameters().Length == 1)
.FirstOrDefault()
?.MakeGenericMethod(typeof(char), typeof(byte));
if (Add == null)
{
throw new InvalidOperationException("Failed to find Unsafe.As{TFrom, TTo}(ref TFrom)");
}
AsSpan = typeof(MemoryExtensions).GetMethod(
nameof(MemoryExtensions.AsSpan),
BindingFlags.Public | BindingFlags.Static,
binder: null,
new[] { typeof(string), typeof(int), typeof(int), },
modifiers: null);
if (AsSpan == null)
{
throw new InvalidOperationException("Failed to find MemoryExtensions.AsSpan(string, int, int)");
}
// Can't use GetMethod because the parameter is a generic method parameters.
GetReference = typeof(MemoryMarshal)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.Name == nameof(MemoryMarshal.GetReference))
.Where(m => m.GetGenericArguments().Length == 1)
.Where(m => m.GetParameters().Length == 1)
// Disambiguate between ReadOnlySpan<> and Span<> - this method is overloaded.
.Where(m => m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>))
.FirstOrDefault()
?.MakeGenericMethod(typeof(char));
if (GetReference == null)
{
throw new InvalidOperationException("Failed to find MemoryMarshal.GetReference{T}(ReadOnlySpan{T})");
}
ReadUnalignedUInt64 = typeof(Unsafe).GetMethod(
nameof(Unsafe.ReadUnaligned),
BindingFlags.Public | BindingFlags.Static,
binder: null,
new[] { typeof(byte).MakeByRefType(), },
modifiers: null)
.MakeGenericMethod(typeof(ulong));
if (ReadUnalignedUInt64 == null)
{
throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)");
}
ReadUnalignedUInt16 = typeof(Unsafe).GetMethod(
nameof(Unsafe.ReadUnaligned),
BindingFlags.Public | BindingFlags.Static,
binder: null,
new[] { typeof(byte).MakeByRefType(), },
modifiers: null)
.MakeGenericMethod(typeof(ushort));
if (ReadUnalignedUInt16 == null)
{
throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)");
}
}
/// <summary>
/// <see cref="Unsafe.Add{T}(ref T, int)"/> - Add[ref byte]
/// </summary>
public MethodInfo Add { get; }
/// <summary>
/// <see cref="Unsafe.As{TFrom, TTo}(ref TFrom)"/> - As[char, byte]
/// </summary>
public MethodInfo As { get; }
/// <summary>
/// <see cref="MemoryExtensions.AsSpan(string, int, int)"/>
/// </summary>
public MethodInfo AsSpan { get; }
/// <summary>
/// <see cref="MemoryMarshal.GetReference{T}(ReadOnlySpan{T})"/> - GetReference[char]
/// </summary>
public MethodInfo GetReference { get; }
/// <summary>
/// <see cref="Unsafe.ReadUnaligned{T}(ref byte)"/> - ReadUnaligned[ulong]
/// </summary>
public MethodInfo ReadUnalignedUInt64 { get; }
/// <summary>
/// <see cref="Unsafe.ReadUnaligned{T}(ref byte)"/> - ReadUnaligned[ushort]
/// </summary>
public MethodInfo ReadUnalignedUInt16 { get; }
}
}
}
#endif

Просмотреть файл

@ -0,0 +1,102 @@
// 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 System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Routing.Matching
{
// Uses generated IL to implement the JumpTable contract. This approach requires
// a fallback jump table for two reasons:
// 1. We compute the IL lazily to avoid taking up significant time when processing a request
// 2. The generated IL only supports ASCII in the URL path
internal class ILEmitTrieJumpTable : JumpTable
{
private const int NotAscii = int.MinValue;
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly (string text, int destination)[] _entries;
private readonly bool? _vectorize;
private readonly JumpTable _fallback;
// Used to protect the initialization of the compiled delegate
private object _lock;
private bool _initializing;
private Task _task;
// Will be replaced at runtime by the generated code.
//
// Internal for testing
internal Func<string, PathSegment, int> _getDestination;
public ILEmitTrieJumpTable(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries,
bool? vectorize,
JumpTable fallback)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
_entries = entries;
_vectorize = vectorize;
_fallback = fallback;
_getDestination = FallbackGetDestination;
}
public override int GetDestination(string path, PathSegment segment)
{
return _getDestination(path, segment);
}
private int FallbackGetDestination(string path, PathSegment segment)
{
if (path.Length == 0)
{
return _exitDestination;
}
// We only hit this code path if the IL delegate is still initializing.
LazyInitializer.EnsureInitialized(ref _task, ref _initializing, ref _lock, InitializeILDelegateAsync);
return _fallback.GetDestination(path, segment);
}
// Internal for testing
internal async Task InitializeILDelegateAsync()
{
// Offload the creation of the IL delegate to the thread pool.
await Task.Run(() =>
{
InitializeILDelegate();
});
}
// Internal for testing
internal void InitializeILDelegate()
{
var generated = ILEmitTrieFactory.Create(_defaultDestination, _exitDestination, _entries, _vectorize);
_getDestination = (string path, PathSegment segment) =>
{
if (segment.Length == 0)
{
return _exitDestination;
}
var result = generated(path, segment.Start, segment.Length);
if (result == ILEmitTrieFactory.NotAscii)
{
result = _fallback.GetDestination(path, segment);
}
return result;
};
}
}
}
#endif

Просмотреть файл

@ -38,25 +38,65 @@ namespace Microsoft.AspNetCore.Routing.Matching
throw new InvalidOperationException(message);
}
// The JumpTable implementation is chosen based on the number of entries. Right
// now this is simple and minimal.
// The JumpTable implementation is chosen based on the number of entries.
//
// Basically the concerns that we're juggling here are that different implementations
// make sense depending on the characteristics of the entries.
//
// On netcoreapp we support IL generation of optimized tries that is much faster
// than anything we can do with string.Compare or dictionaries. However the IL emit
// strategy requires us to produce a fallback jump table - see comments on the class.
// We have an optimized fast path for zero entries since we don't have to
// do any string comparisons.
if (_entries.Count == 0)
{
return new ZeroEntryJumpTable(DefaultDestination, ExitDestination);
}
// The IL Emit jump table is not faster for a single entry
if (_entries.Count == 1)
{
var entry = _entries[0];
return new SingleEntryJumpTable(DefaultDestination, ExitDestination, entry.text, entry.destination);
}
if (_entries.Count < 10)
// We choose a hard upper bound of 100 as the limit for when we switch to a dictionary
// over a trie. The reason is that while the dictionary has a bigger constant factor,
// it is O(1) vs a trie which is O(M * log(N)). Our perf testing shows that the trie
// is better for ~90 entries based on all of Azure's route table. Anything above 100 edges
// we'd consider to be a very very large node, and so while we don't think anyone will
// have a node this large in practice, we want to make sure the performance is reasonable
// for any size.
//
// Additionally if we're on 32bit, the scalability is worse, so switch to the dictionary at 50
// entries.
var threshold = IntPtr.Size == 8 ? 100 : 50;
if (_entries.Count >= threshold)
{
return new LinearSearchJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
return new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
}
return new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
// If we have more than a single string, the IL emit strategy is the fastest - but we need to decide
// what do for the fallback case.
JumpTable fallback;
// Based on our testing a linear search is still faster than a dictionary at ten entries.
if (_entries.Count <= 10)
{
fallback = new LinearSearchJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
}
else
{
fallback = new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
}
#if IL_EMIT
return new ILEmitTrieJumpTable(DefaultDestination, ExitDestination, _entries.ToArray(), vectorize: null, fallback);
#else
return fallback;
#endif
}
}
}

Просмотреть файл

@ -4,13 +4,25 @@
Commonly used types:
Microsoft.AspNetCore.Routing.Route
Microsoft.AspNetCore.Routing.RouteCollection</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;routing</PackageTags>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<!--
RefEmit is supported in netcoreapp.
The ability to save compiled assemblies is for testing and debugging, not shipped in the product.
-->
<ILEmit Condition="'$(TargetFramework)'!='netstandard2.0'">true</ILEmit>
<ILEmitSaveAssemblies Condition="'$(ILEmitSaveAssemblies)'==''">false</ILEmitSaveAssemblies>
<DefineConstants Condition="'$(ILEmit)'=='true'">IL_EMIT;$(DefineConstants)</DefineConstants>
<DefineConstants Condition="'$(ILEmitSaveAssemblies)'=='true'">IL_EMIT_SAVE_ASSEMBLIES;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\shared\Microsoft.AspNetCore.Routing.DecisionTree.Sources\**\*.cs" />
</ItemGroup>
@ -26,5 +38,7 @@ Microsoft.AspNetCore.Routing.RouteCollection</Description>
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="$(MicrosoftExtensionsObjectPoolPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.PropertyHelper.Sources" Version="$(MicrosoftExtensionsPropertyHelperSourcesPackageVersion)" PrivateAssets="All" />
<PackageReference Condition="'$(ILEmitBackendSaveAssemblies)'=='true'" Include="System.Reflection.Emit" Version="$(SystemReflectionEmitPackageVersion)" PrivateAssets="All" />
<PackageReference Condition="'$(ILEmit)'=='true'" Include="System.Reflection.Emit.Lightweight" Version="$(SystemReflectionEmitPackageVersion)" PrivateAssets="All" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,228 @@
// 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 Moq;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matching
{
// We get a lot of good coverage of basics since this implementation is used
// as the default in many cases. The tests here are focused on details of the
// implementation (boundaries, casing, non-ASCII).
public abstract class ILEmitTreeJumpTableTestBase : MultipleEntryJumpTableTest
{
public abstract bool Vectorize { get; }
internal override JumpTable CreateTable(
int defaultDestination,
int exitDestination,
params (string text, int destination)[] entries)
{
var fallback = new DictionaryJumpTable(defaultDestination, exitDestination, entries);
var table = new ILEmitTrieJumpTable(defaultDestination, exitDestination, entries, Vectorize, fallback);
table.InitializeILDelegate();
return table;
}
[Fact] // Not calling CreateTable here because we want to test the initialization
public async Task InitializeILDelegateAsync_ReplacesDelegate()
{
// Arrange
var table = new ILEmitTrieJumpTable(0, -1, new[] { ("hi", 1), }, Vectorize, Mock.Of<JumpTable>());
var original = table._getDestination;
// Act
await table.InitializeILDelegateAsync();
// Assert
Assert.NotSame(original, table._getDestination);
}
// Tests that we can detect non-ASCII characters and use the fallback jump table.
// Testing different indices since that affects which part of the code is running.
// \u007F = lowest non-ASCII character
// \uFFFF = highest non-ASCII character
[Theory]
// non-ASCII character in first section non-vectorized comparisons
[InlineData("he\u007F", "he\u007Flo-world", 0, 3)]
[InlineData("he\uFFFF", "he\uFFFFlo-world", 0, 3)]
[InlineData("e\u007F", "he\u007Flo-world", 1, 2)]
[InlineData("e\uFFFF", "he\uFFFFlo-world", 1, 2)]
[InlineData("\u007F", "he\u007Flo-world", 2, 1)]
[InlineData("\uFFFF", "he\uFFFFlo-world", 2, 1)]
// non-ASCII character in first section vectorized comparions
[InlineData("hel\u007F", "hel\u007Fo-world", 0, 4)]
[InlineData("hel\uFFFF", "hel\uFFFFo-world", 0, 4)]
[InlineData("el\u007Fo", "hel\u007Fo-world", 1, 4)]
[InlineData("el\uFFFFo", "hel\uFFFFo-world", 1, 4)]
[InlineData("l\u007Fo-", "hel\u007Fo-world", 2, 4)]
[InlineData("l\uFFFFo-", "hel\uFFFFo-world", 2, 4)]
[InlineData("\u007Fo-w", "hel\u007Fo-world", 3, 4)]
[InlineData("\uFFFFo-w", "hel\uFFFFo-world", 3, 4)]
// non-ASCII character in second section non-vectorized comparisons
[InlineData("hello-\u007F", "hello-\u007Forld", 0, 7)]
[InlineData("hello-\uFFFF", "hello-\uFFFForld", 0, 7)]
[InlineData("ello-\u007F", "hello-\u007Forld", 1, 6)]
[InlineData("ello-\uFFFF", "hello-\uFFFForld", 1, 6)]
[InlineData("llo-\u007F", "hello-\u007Forld", 2, 5)]
[InlineData("llo-\uFFFF", "hello-\uFFFFForld", 2, 5)]
// non-ASCII character in first section vectorized comparions
[InlineData("hello-w\u007F", "hello-w\u007Forld", 0, 8)]
[InlineData("hello-w\uFFFF", "hello-w\uFFFForld", 0, 8)]
[InlineData("ello-w\u007Fo", "hello-w\u007Forld", 1, 8)]
[InlineData("ello-w\uFFFFo", "hello-w\uFFFForld", 1, 8)]
[InlineData("llo-w\u007For", "hello-w\u007Forld", 2, 8)]
[InlineData("llo-w\uFFFFor", "hello-w\uFFFForld", 2, 8)]
[InlineData("lo-w\u007Forl", "hello-w\u007Forld", 3, 8)]
[InlineData("lo-w\uFFFForl", "hello-w\uFFFForld", 3, 8)]
public void GetDestination_Found_IncludesNonAsciiCharacters(string entry, string path, int start, int length)
{
// Makes it easy to spot invalid tests
Assert.Equal(entry.Length, length);
Assert.Equal(entry, path.Substring(start, length), ignoreCase: true);
// Arrange
var table = CreateTable(0, -1, new[] { (entry, 1), });
var segment = new PathSegment(start, length);
// Act
var result = table.GetDestination(path, segment);
// Assert
Assert.Equal(1, result);
}
// Tests for difference in casing with ASCII casing rules. Verifies our case
// manipulation algorthm is correct.
//
// We convert from upper case to lower
// 'A' and 'a' are 32 bits apart at the low end
// 'Z' and 'z' are 32 bits apart at the high end
[Theory]
// character in first section non-vectorized comparisons
[InlineData("heA", "healo-world", 0, 3)]
[InlineData("heZ", "hezlo-world", 0, 3)]
[InlineData("eA", "healo-world", 1, 2)]
[InlineData("eZ", "hezlo-world", 1, 2)]
[InlineData("A", "healo-world", 2, 1)]
[InlineData("Z", "hezlo-world", 2, 1)]
// character in first section vectorized comparions
[InlineData("helA", "helao-world", 0, 4)]
[InlineData("helZ", "helzo-world", 0, 4)]
[InlineData("elAo", "helao-world", 1, 4)]
[InlineData("elZo", "helzo-world", 1, 4)]
[InlineData("lAo-", "helao-world", 2, 4)]
[InlineData("lZo-", "helzo-world", 2, 4)]
[InlineData("Ao-w", "helao-world", 3, 4)]
[InlineData("Zo-w", "helzo-world", 3, 4)]
// character in second section non-vectorized comparisons
[InlineData("hello-A", "hello-aorld", 0, 7)]
[InlineData("hello-Z", "hello-zorld", 0, 7)]
[InlineData("ello-A", "hello-aorld", 1, 6)]
[InlineData("ello-Z", "hello-zorld", 1, 6)]
[InlineData("llo-A", "hello-aorld", 2, 5)]
[InlineData("llo-Z", "hello-zForld", 2, 5)]
// character in first section vectorized comparions
[InlineData("hello-wA", "hello-waorld", 0, 8)]
[InlineData("hello-wZ", "hello-wzorld", 0, 8)]
[InlineData("ello-wAo", "hello-waorld", 1, 8)]
[InlineData("ello-wZo", "hello-wzorld", 1, 8)]
[InlineData("llo-wAor", "hello-waorld", 2, 8)]
[InlineData("llo-wZor", "hello-wzorld", 2, 8)]
[InlineData("lo-wAorl", "hello-waorld", 3, 8)]
[InlineData("lo-wZorl", "hello-wzorld", 3, 8)]
public void GetDestination_Found_IncludesCharactersWithCasingDifference(string entry, string path, int start, int length)
{
// Makes it easy to spot invalid tests
Assert.Equal(entry.Length, length);
Assert.Equal(entry, path.Substring(start, length), ignoreCase: true);
// Arrange
var table = CreateTable(0, -1, new[] { (entry, 1), });
var segment = new PathSegment(start, length);
// Act
var result = table.GetDestination(path, segment);
// Assert
Assert.Equal(1, result);
}
// Tests for difference in casing with ASCII casing rules. Verifies our case
// manipulation algorthm is correct.
//
// We convert from upper case to lower
// '@' and '`' are 32 bits apart at the low end
// '[' and '}' are 32 bits apart at the high end
//
// How to understand these tests:
// "an @ should not be converted to a ` since it is out of range"
[Theory]
// character in first section non-vectorized comparisons
[InlineData("he@", "he`lo-world", 0, 3)]
[InlineData("he[", "he{lo-world", 0, 3)]
[InlineData("e@", "he`lo-world", 1, 2)]
[InlineData("e[", "he{lo-world", 1, 2)]
[InlineData("@", "he`lo-world", 2, 1)]
[InlineData("[", "he{lo-world", 2, 1)]
// character in first section vectorized comparions
[InlineData("hel@", "hel`o-world", 0, 4)]
[InlineData("hel[", "hel{o-world", 0, 4)]
[InlineData("el@o", "hel`o-world", 1, 4)]
[InlineData("el[o", "hel{o-world", 1, 4)]
[InlineData("l@o-", "hel`o-world", 2, 4)]
[InlineData("l[o-", "hel{o-world", 2, 4)]
[InlineData("@o-w", "hel`o-world", 3, 4)]
[InlineData("[o-w", "hel{o-world", 3, 4)]
// character in second section non-vectorized comparisons
[InlineData("hello-@", "hello-`orld", 0, 7)]
[InlineData("hello-[", "hello-{orld", 0, 7)]
[InlineData("ello-@", "hello-`orld", 1, 6)]
[InlineData("ello-[", "hello-{orld", 1, 6)]
[InlineData("llo-@", "hello-`orld", 2, 5)]
[InlineData("llo-[", "hello-{Forld", 2, 5)]
// character in first section vectorized comparions
[InlineData("hello-w@", "hello-w`orld", 0, 8)]
[InlineData("hello-w[", "hello-w{orld", 0, 8)]
[InlineData("ello-w@o", "hello-w`orld", 1, 8)]
[InlineData("ello-w[o", "hello-w{orld", 1, 8)]
[InlineData("llo-w@or", "hello-w`orld", 2, 8)]
[InlineData("llo-w[or", "hello-w{orld", 2, 8)]
[InlineData("lo-w@orl", "hello-w`orld", 3, 8)]
[InlineData("lo-w[orl", "hello-w{orld", 3, 8)]
public void GetDestination_NotFound_IncludesCharactersWithCasingDifference(string entry, string path, int start, int length)
{
// Makes it easy to spot invalid tests
Assert.Equal(entry.Length, length);
Assert.NotEqual(entry, path.Substring(start, length));
// Arrange
var table = CreateTable(0, -1, new[] { (entry, 1), });
var segment = new PathSegment(start, length);
// Act
var result = table.GetDestination(path, segment);
// Assert
Assert.Equal(0, result);
}
}
}
#endif

Просмотреть файл

@ -0,0 +1,12 @@
// 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
namespace Microsoft.AspNetCore.Routing.Matching
{
public class NonVectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase
{
public override bool Vectorize => false;
}
}
#endif

Просмотреть файл

@ -0,0 +1,14 @@
// 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
namespace Microsoft.AspNetCore.Routing.Matching
{
public class VectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase
{
// We can still run the vectorized implementation on 32 bit, we just
// don't expect it to be performant - it will still be correct.
public override bool Vectorize => true;
}
}
#endif

Просмотреть файл

@ -3,10 +3,19 @@
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<RootNamespace>Microsoft.AspNetCore.Routing</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<!--
RefEmit is supported in netcoreapp. We test on .NET Framework, but we don't support RefEmit in the product
on .NET Framework.
-->
<ILEmit Condition="'$(TargetFramework)'=='netcoreapp2.2'">true</ILEmit>
<DefineConstants Condition="'$(ILEmit)'=='true'">IL_EMIT;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@ -19,6 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
<PackageReference Condition="'$(ILEmit)'=='true'" Include="System.Reflection.Emit.Lightweight" Version="$(SystemReflectionEmitPackageVersion)" PrivateAssets="All" />
</ItemGroup>
</Project>