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:
Ryan Nowak 2018-07-12 00:40:22 -07:00
Родитель 9e114b547d
Коммит ef6f326188
5 изменённых файлов: 535 добавлений и 1 удалений

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

@ -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];