* Productize JumpTable
This commit is contained in:
Ryan Nowak 2018-07-12 23:28:51 -07:00 коммит произвёл GitHub
Родитель 0cf972cc43
Коммит 7209cab5e9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 803 добавлений и 67 удалений

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

@ -0,0 +1,134 @@
// 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 BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class JumpTableMultipleEntryBenchmark
{
private string[] _strings;
private PathSegment[] _segments;
private JumpTable _linearSearch;
private JumpTable _dictionary;
// All factors of 100 to support sampling
[Params(2, 5, 10, 25, 50, 100)]
public int Count;
[GlobalSetup]
public void Setup()
{
_strings = GetStrings(100);
_segments = new PathSegment[100];
for (var i = 0; i < _strings.Length; i++)
{
_segments[i] = new PathSegment(0, _strings[i].Length);
}
var samples = new int[Count];
for (var i = 0; i < samples.Length; i++)
{
samples[i] = i * (_strings.Length / Count);
}
var entries = new List<(string text, int _)>();
for (var i = 0; i < samples.Length; i++)
{
entries.Add((_strings[samples[i]], i));
}
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
}
// This baseline is similar to SingleEntryJumpTable. We just want
// something stable to compare against.
[Benchmark(Baseline = true, OperationsPerInvoke = 100)]
public int Baseline()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
var @string = strings[i];
var segment = segments[i];
destination = segment.Length == 0 ? -1 :
segment.Length != @string.Length ? 1 :
string.Compare(
@string,
segment.Start,
@string,
0,
@string.Length,
StringComparison.OrdinalIgnoreCase);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 100)]
public int LinearSearch()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _linearSearch.GetDestination(strings[i], segments[i]);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 100)]
public int Dictionary()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _dictionary.GetDestination(strings[i], segments[i]);
}
return destination;
}
private static string[] GetStrings(int count)
{
var strings = new string[count];
for (var i = 0; i < count; i++)
{
var guid = Guid.NewGuid().ToString();
// Between 5 and 36 characters
var text = guid.Substring(0, Math.Max(5, Math.Min(count, 36)));
if (char.IsDigit(text[0]))
{
// Convert first character to a letter.
text = ((char)(text[0] + ('G' - '0'))) + text.Substring(1);
}
if (i % 2 == 0)
{
// Lowercase half of them
text = text.ToLowerInvariant();
}
strings[i] = text;
}
return strings;
}
}
}

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

@ -0,0 +1,78 @@
// 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 BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class JumpTableSingleEntryBenchmark
{
private JumpTable _table;
private string[] _strings;
private PathSegment[] _segments;
[GlobalSetup]
public void Setup()
{
_table = new SingleEntryJumpTable(0, -1, "hello-world", 1);
_strings = new string[]
{
"index/foo/2",
"index/hello-world1/2",
"index/hello-world/2",
"index//2",
"index/hillo-goodbye/2",
};
_segments = new PathSegment[]
{
new PathSegment(6, 3),
new PathSegment(6, 12),
new PathSegment(6, 11),
new PathSegment(6, 0),
new PathSegment(6, 13),
};
}
[Benchmark(Baseline = true, OperationsPerInvoke = 5)]
public int Baseline()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var 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(
@string,
segment.Start,
"hello-world",
0,
segment.Length,
StringComparison.OrdinalIgnoreCase);
}
return destination;
}
[Benchmark(OperationsPerInvoke = 5)]
public int Implementation()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _table.GetDestination(strings[i], segments[i]);
}
return destination;
}
}
}

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

@ -0,0 +1,66 @@
// 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 BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class JumpTableZeroEntryBenchmark
{
private JumpTable _table;
private string[] _strings;
private PathSegment[] _segments;
[GlobalSetup]
public void Setup()
{
_table = new ZeroEntryJumpTable(0, -1);
_strings = new string[]
{
"index/foo/2",
"index/hello-world1/2",
"index/hello-world/2",
"index//2",
"index/hillo-goodbye/2",
};
_segments = new PathSegment[]
{
new PathSegment(6, 3),
new PathSegment(6, 12),
new PathSegment(6, 11),
new PathSegment(6, 0),
new PathSegment(6, 13),
};
}
[Benchmark(Baseline=true, OperationsPerInvoke = 5)]
public int Baseline()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = segments[i].Length == 0 ? -1 : 0;
}
return destination;
}
[Benchmark(OperationsPerInvoke = 5)]
public int Implementation()
{
var strings = _strings;
var segments = _segments;
var destination = 0;
for (var i = 0; i < strings.Length; i++)
{
destination = _table.GetDestination(strings[i], segments[i]);
}
return destination;
}
}
}

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

@ -0,0 +1,68 @@
// 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;
using System.Text;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class DictionaryJumpTable : JumpTable
{
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly Dictionary<string, int> _dictionary;
public DictionaryJumpTable(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
_dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < entries.Length; i++)
{
_dictionary.Add(entries[i].text, entries[i].destination);
}
}
public override int GetDestination(string path, PathSegment segment)
{
if (segment.Length == 0)
{
return _exitDestination;
}
var text = path.Substring(segment.Start, segment.Length);
if (_dictionary.TryGetValue(text, out var destination))
{
return destination;
}
return _defaultDestination;
}
public override string DebuggerToString()
{
var builder = new StringBuilder();
builder.Append("{ ");
builder.Append(string.Join(", ", _dictionary.Select(kvp => $"{kvp.Key}: {kvp.Value}")));
builder.Append("$+: ");
builder.Append(_defaultDestination);
builder.Append(", ");
builder.Append("$0: ");
builder.Append(_defaultDestination);
builder.Append(" }");
return builder.ToString();
}
}
}

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

@ -0,0 +1,18 @@
// 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.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Matchers
{
[DebuggerDisplay("{DebuggerToString(),nq}")]
internal abstract class JumpTable
{
public abstract int GetDestination(string path, PathSegment segment);
public virtual string DebuggerToString()
{
return GetType().Name;
}
}
}

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

@ -0,0 +1,62 @@
// 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
{
internal class JumpTableBuilder
{
public static readonly int InvalidDestination = -1;
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
// The destination state when none of the text entries match.
public int DefaultDestination { get; set; } = InvalidDestination;
// The destination state for a zero-length segment. This is a special
// case because parameters don't match a zero-length segment.
public int ExitDestination { get; set; } = InvalidDestination;
public void AddEntry(string text, int destination)
{
_entries.Add((text, destination));
}
public JumpTable Build()
{
if (DefaultDestination == InvalidDestination)
{
var message = $"{nameof(DefaultDestination)} is not set. Please report this as a bug.";
throw new InvalidOperationException(message);
}
if (ExitDestination == InvalidDestination)
{
var message = $"{nameof(ExitDestination)} is not set. Please report this as a bug.";
throw new InvalidOperationException(message);
}
// The JumpTable implementation is chosen based on the number of entries. Right
// now this is simple and minimal.
if (_entries.Count == 0)
{
return new ZeroEntryJumpTable(DefaultDestination, ExitDestination);
}
if (_entries.Count == 1)
{
var entry = _entries[0];
return new SingleEntryJumpTable(DefaultDestination, ExitDestination, entry.text, entry.destination);
}
if (_entries.Count < 10)
{
return new LinearSearchJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
}
return new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
}
}
}

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

@ -0,0 +1,72 @@
// 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.Linq;
using System.Text;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class LinearSearchJumpTable : JumpTable
{
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly (string text, int destination)[] _entries;
public LinearSearchJumpTable(
int defaultDestination,
int exitDestination,
(string text, int destination)[] entries)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
_entries = entries;
}
public override int GetDestination(string path, PathSegment segment)
{
if (segment.Length == 0)
{
return _exitDestination;
}
var entries = _entries;
for (var i = 0; i < entries.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;
}
public override string DebuggerToString()
{
var builder = new StringBuilder();
builder.Append("{ ");
builder.Append(string.Join(", ", _entries.Select(e => $"{e.text}: {e.destination}")));
builder.Append("$+: ");
builder.Append(_defaultDestination);
builder.Append(", ");
builder.Append("$0: ");
builder.Append(_defaultDestination);
builder.Append(" }");
return builder.ToString();
}
}
}

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

@ -0,0 +1,54 @@
// 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;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class SingleEntryJumpTable : JumpTable
{
private readonly int _defaultDestination;
private readonly int _exitDestination;
private readonly string _text;
private readonly int _destination;
public SingleEntryJumpTable(
int defaultDestination,
int exitDestination,
string text,
int destination)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
_text = text;
_destination = destination;
}
public override int GetDestination(string path, PathSegment segment)
{
if (segment.Length == 0)
{
return _exitDestination;
}
if (segment.Length == _text.Length &&
string.Compare(
path,
segment.Start,
_text,
0,
segment.Length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return _destination;
}
return _defaultDestination;
}
public override string DebuggerToString()
{
return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}";
}
}
}

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

@ -0,0 +1,27 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class ZeroEntryJumpTable : JumpTable
{
private readonly int _defaultDestination;
private readonly int _exitDestination;
public ZeroEntryJumpTable(int defaultDestination, int exitDestination)
{
_defaultDestination = defaultDestination;
_exitDestination = exitDestination;
}
public unsafe override int GetDestination(string path, PathSegment segment)
{
return segment.Length == 0 ? _exitDestination : _defaultDestination;
}
public override string DebuggerToString()
{
return $"{{ $+: {_defaultDestination}, $0: {_exitDestination} }}";
}
}
}

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

@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
for (var i = 0; i < count; i++)
{
current = states[current].Transitions.GetDestination(buffer, i, path);
current = states[current].Transitions.GetDestination(path, buffer[i]);
}
var matches = new List<(Endpoint, RouteValueDictionary)>();
@ -88,64 +88,5 @@ namespace Microsoft.AspNetCore.Routing.Matchers
public Endpoint Endpoint;
public string[] Parameters;
}
public abstract class JumpTable
{
public unsafe abstract int GetDestination(PathSegment* segments, int depth, string path);
}
public class JumpTableBuilder
{
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
public int Depth { get; set; }
public int Exit { get; set; }
public void AddEntry(string text, int destination)
{
_entries.Add((text, destination));
}
public JumpTable Build()
{
return new SimpleJumpTable(Depth, Exit, _entries.ToArray());
}
}
private class SimpleJumpTable : JumpTable
{
private readonly (string text, int destination)[] _entries;
private readonly int _depth;
private readonly int _exit;
public SimpleJumpTable(int depth, int exit, (string text, int destination)[] entries)
{
_depth = depth;
_exit = exit;
_entries = entries;
}
public unsafe override int GetDestination(PathSegment* segments, int depth, string path)
{
for (var i = 0; i < _entries.Length; i++)
{
var segment = segments[depth];
if (segment.Length == _entries[i].text.Length &&
string.Compare(
path,
segment.Start,
_entries[i].text,
0,
segment.Length,
StringComparison.OrdinalIgnoreCase) == 0)
{
return _entries[i].destination;
}
}
return _exit;
}
}
}
}

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

@ -109,13 +109,18 @@ namespace Microsoft.AspNetCore.Routing.Matchers
var exit = states.Count;
states.Add(new State() { IsAccepting = false, Matches = Array.Empty<Candidate>(), });
tables.Add(new JumpTableBuilder() { Exit = exit, });
tables.Add(new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, });
for (var i = 0; i < tables.Count; i++)
{
if (tables[i].Exit == -1)
if (tables[i].DefaultDestination == JumpTableBuilder.InvalidDestination)
{
tables[i].Exit = exit;
tables[i].DefaultDestination = exit;
}
if (tables[i].ExitDestination == JumpTableBuilder.InvalidDestination)
{
tables[i].ExitDestination = exit;
}
}
@ -158,7 +163,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
IsAccepting = node.Matches.Count > 0,
});
var table = new JumpTableBuilder() { Depth = node.Depth, };
var table = new JumpTableBuilder();
tables.Add(table);
foreach (var kvp in node.Literals)
@ -172,13 +177,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
table.AddEntry(kvp.Key, transition);
}
var exitIndex = -1;
var defaultIndex = -1;
if (node.Literals.TryGetValue("*", out var exit))
{
exitIndex = AddNode(exit, states, tables);
defaultIndex = AddNode(exit, states, tables);
}
table.Exit = exitIndex;
table.DefaultDestination = defaultIndex;
return index;
}

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

@ -0,0 +1,16 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class DictionaryJumpTableTest : MultipleEntryJumpTableTest
{
internal override JumpTable CreateTable(
int defaultDestination,
int exitDestination,
params (string text, int destination)[] entries)
{
return new DictionaryJumpTable(defaultDestination, exitDestination, entries);
}
}
}

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

@ -0,0 +1,17 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class LinearSearchJumpTableTest : MultipleEntryJumpTableTest
{
internal override JumpTable CreateTable(
int defaultDestination,
int existDestination,
params (string text, int destination)[] entries)
{
return new LinearSearchJumpTable(defaultDestination, existDestination, entries);
}
}
}

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

@ -0,0 +1,80 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public abstract class MultipleEntryJumpTableTest
{
internal abstract JumpTable CreateTable(
int defaultDestination,
int exitDestination,
params (string text, int destination)[] entries);
[Fact]
public void GetDestination_ZeroLengthSegment_JumpsToExit()
{
// Arrange
var table = CreateTable(0, 1, ("text", 2));
// Act
var result = table.GetDestination("ignored", new PathSegment(0, 0));
// Assert
Assert.Equal(1, result);
}
[Fact]
public void GetDestination_NonMatchingSegment_JumpsToDefault()
{
// Arrange
var table = CreateTable(0, 1, ("text", 2));
// Act
var result = table.GetDestination("text", new PathSegment(1, 2));
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetDestination_SegmentMatchingText_JumpsToDestination()
{
// Arrange
var table = CreateTable(0, 1, ("text", 2));
// Act
var result = table.GetDestination("some-text", new PathSegment(5, 4));
// Assert
Assert.Equal(2, result);
}
[Fact]
public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination()
{
// Arrange
var table = CreateTable(0, 1, ("text", 2));
// Act
var result = table.GetDestination("some-tExt", new PathSegment(5, 4));
// Assert
Assert.Equal(2, result);
}
[Fact]
public void GetDestination_SegmentMatchingTextIgnoreCase_MultipleEntries()
{
// Arrange
var table = CreateTable(0, 1, ("tezt", 2), ("text", 3));
// Act
var result = table.GetDestination("some-tExt", new PathSegment(5, 4));
// Assert
Assert.Equal(3, result);
}
}
}

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

@ -0,0 +1,62 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class SingleEntryJumpTableTest
{
[Fact]
public void GetDestination_ZeroLengthSegment_JumpsToExit()
{
// Arrange
var table = new SingleEntryJumpTable(0, 1, "text", 2);
// Act
var result = table.GetDestination("ignored", new PathSegment(0, 0));
// Assert
Assert.Equal(1, result);
}
[Fact]
public void GetDestination_NonMatchingSegment_JumpsToDefault()
{
// Arrange
var table = new SingleEntryJumpTable(0, 1, "text", 2);
// Act
var result = table.GetDestination("text", new PathSegment(1, 2));
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetDestination_SegmentMatchingText_JumpsToDestination()
{
// Arrange
var table = new SingleEntryJumpTable(0, 1, "text", 2);
// Act
var result = table.GetDestination("some-text", new PathSegment(5, 4));
// Assert
Assert.Equal(2, result);
}
[Fact]
public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination()
{
// Arrange
var table = new SingleEntryJumpTable(0, 1, "text", 2);
// Act
var result = table.GetDestination("some-tExt", new PathSegment(5, 4));
// Assert
Assert.Equal(2, result);
}
}
}

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

@ -0,0 +1,36 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class ZeroEntryJumpTableTest
{
[Fact]
public void GetDestination_ZeroLengthSegment_JumpsToExit()
{
// Arrange
var table = new ZeroEntryJumpTable(0, 1);
// Act
var result = table.GetDestination("ignored", new PathSegment(0, 0));
// Assert
Assert.Equal(1, result);
}
[Fact]
public void GetDestination_SegmentWithLength_JumpsToDefault()
{
// Arrange
var table = new ZeroEntryJumpTable(0, 1);
// Act
var result = table.GetDestination("ignored", new PathSegment(0, 1));
// Assert
Assert.Equal(0, result);
}
}
}