Merge pull request #597 from aspnet/rynowak/jumptable2
Add experimental JumpTables to benchmarks
This commit is contained in:
commit
5e1f99faaf
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in New Issue