зеркало из https://github.com/aspnet/Routing.git
Add experimental JumpTables
Also reduced the count of entries of the max iteration to 25. The main issue that we're trying to solve right now is which approach is the best with a small number of entries. Going up to 100 takes a loooong time, and all of the dictionary-based approaches scale well above 10 or so entries.
This commit is contained in:
Родитель
9e114b547d
Коммит
ef6f326188
|
@ -18,5 +18,6 @@
|
|||
<PackageSigningCertName>MicrosoftNuGet</PackageSigningCertName>
|
||||
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
// 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.Matchers
|
||||
{
|
||||
// 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 unsafe 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
// 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.Matchers
|
||||
{
|
||||
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 unsafe 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// 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.Matchers
|
||||
{
|
||||
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,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
|
@ -14,9 +15,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
private JumpTable _linearSearch;
|
||||
private JumpTable _dictionary;
|
||||
private JumpTable _ascii;
|
||||
private JumpTable _dictionaryLookup;
|
||||
private JumpTable _customHashTable;
|
||||
|
||||
// All factors of 100 to support sampling
|
||||
[Params(2, 5, 10, 25, 50, 100)]
|
||||
[Params(2, 4, 5, 10, 25)]
|
||||
public int Count;
|
||||
|
||||
[GlobalSetup]
|
||||
|
@ -44,6 +48,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
|
||||
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
|
||||
Debug.Assert(AsciiKeyedJumpTable.TryCreate(0, -1, entries, out _ascii));
|
||||
_dictionaryLookup = new DictionaryLookupJumpTable(0, -1, entries.ToArray());
|
||||
_customHashTable = new CustomHashTableJumpTable(0, -1, entries.ToArray());
|
||||
}
|
||||
|
||||
// This baseline is similar to SingleEntryJumpTable. We just want
|
||||
|
@ -104,6 +111,51 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 100)]
|
||||
public int Ascii()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
var destination = 0;
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
{
|
||||
destination = _ascii.GetDestination(strings[i], segments[i]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 100)]
|
||||
public int DictionaryLookup()
|
||||
{
|
||||
var strings = _strings;
|
||||
var segments = _segments;
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
private static string[] GetStrings(int count)
|
||||
{
|
||||
var strings = new string[count];
|
||||
|
|
Загрузка…
Ссылка в новой задаче