Optimize RouteValueDictionary, expose concrete type

This change optimizes allocations by RouteValueDictionary based on usage.

First, implement a struct Enumerator, and expose the concrete RVD type
from all extensibility points. We wanted to try and decouple this code
from RVD originally and use IDictionary everywhere. After doing that we've
found that it allocates an unacceptable number of enumerators.

Secondly, optimize copies of RVD for the case where you're copying an RVC
to another (common case). When doing this we can copy the count to get the
right capacity, and copy the entries without allocating an enumerator.

Lastly, optimize RVD for the case where it's a wrapper around a poco
object. We 'upgrade' to a writable full dictionary if you try to write to
it, or call one of a number of APIs that are uncommonly used. We could
produce optimized versions of things like `Keys` and `CopyTo` if necessary
in the future.
This commit is contained in:
Ryan Nowak 2015-11-24 10:58:22 -08:00
parent cad81fa608
commit d4b96b27c0
13 changed files with 282 additions and 166 deletions

View File

@ -311,8 +311,8 @@ namespace Microsoft.AspNet.Routing.Tree
}
private static void MergeValues(
IDictionary<string, object> destination,
IDictionary<string, object> values)
RouteValueDictionary destination,
RouteValueDictionary values)
{
foreach (var kvp in values)
{
@ -328,7 +328,7 @@ namespace Microsoft.AspNet.Routing.Tree
private struct TemplateMatch : IEquatable<TemplateMatch>
{
public TemplateMatch(TreeRouteMatchingEntry entry, IDictionary<string, object> values)
public TemplateMatch(TreeRouteMatchingEntry entry, RouteValueDictionary values)
{
Entry = entry;
Values = values;
@ -336,7 +336,7 @@ namespace Microsoft.AspNet.Routing.Tree
public TreeRouteMatchingEntry Entry { get; }
public IDictionary<string, object> Values { get; }
public RouteValueDictionary Values { get; }
public override bool Equals(object obj)
{
@ -403,7 +403,7 @@ namespace Microsoft.AspNet.Routing.Tree
// required values: { id = "5", action = "Buy", Controller = "CoolProducts" }
//
// result: { id = "5", action = "Buy" }
var inputValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
var inputValues = new RouteValueDictionary();
foreach (var kvp in context.Values)
{
if (entry.RequiredLinkValues.ContainsKey(kvp.Key))

View File

@ -72,7 +72,7 @@ namespace Microsoft.AspNet.Routing
/// <summary>
/// Gets the set of values produced by routes on the current routing path.
/// </summary>
public IDictionary<string, object> Values
public RouteValueDictionary Values
{
get
{

View File

@ -4,8 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Routing
{
@ -19,14 +18,11 @@ namespace Microsoft.AspNet.Routing
/// </summary>
internal static readonly IReadOnlyDictionary<string, object> Empty = new RouteValueDictionary();
private readonly Dictionary<string, object> _dictionary;
/// <summary>
/// Creates an empty <see cref="RouteValueDictionary"/>.
/// </summary>
public RouteValueDictionary()
{
_dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
@ -43,46 +39,63 @@ namespace Microsoft.AspNet.Routing
/// Only public instance non-index properties are considered.
/// </remarks>
public RouteValueDictionary(object values)
: this()
{
if (values != null)
var otherDictionary = values as RouteValueDictionary;
if (otherDictionary != null)
{
var keyValuePairCollection = values as IEnumerable<KeyValuePair<string, object>>;
if (keyValuePairCollection != null)
if (otherDictionary.InnerDictionary != null)
{
foreach (var kvp in keyValuePairCollection)
InnerDictionary = new Dictionary<string, object>(
otherDictionary.InnerDictionary.Count,
StringComparer.OrdinalIgnoreCase);
foreach (var kvp in otherDictionary.InnerDictionary)
{
Add(kvp.Key, kvp.Value);
InnerDictionary[kvp.Key] = kvp.Value;
}
return;
}
var type = values.GetType();
var allProperties = type.GetRuntimeProperties();
// This is done to support 'new' properties that hide a property on a base class
var orderedByDeclaringType = allProperties.OrderBy(p => p.DeclaringType == type ? 0 : 1);
foreach (var property in orderedByDeclaringType)
else if (otherDictionary.Properties != null)
{
if (property.GetMethod != null &&
property.GetMethod.IsPublic &&
!property.GetMethod.IsStatic &&
property.GetIndexParameters().Length == 0)
{
var value = property.GetValue(values);
if (ContainsKey(property.Name) && property.DeclaringType != type)
{
// This is a hidden property, ignore it.
}
else
{
Add(property.Name, value);
}
}
Properties = otherDictionary.Properties;
Value = otherDictionary.Value;
return;
}
else
{
return;
}
}
var keyValuePairCollection = values as IEnumerable<KeyValuePair<string, object>>;
if (keyValuePairCollection != null)
{
InnerDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in keyValuePairCollection)
{
InnerDictionary[kvp.Key] = kvp.Value;
}
return;
}
if (values != null)
{
Properties = PropertyHelper.GetVisibleProperties(values);
Value = values;
return;
}
}
private Dictionary<string, object> InnerDictionary { get; set; }
private PropertyHelper[] Properties { get; }
private object Value { get; }
/// <inheritdoc />
public object this[string key]
{
@ -94,7 +107,7 @@ namespace Microsoft.AspNet.Routing
}
object value;
_dictionary.TryGetValue(key, out value);
TryGetValue(key, out value);
return value;
}
@ -105,7 +118,8 @@ namespace Microsoft.AspNet.Routing
throw new ArgumentNullException(nameof(key));
}
_dictionary[key] = value;
EnsureWritable();
InnerDictionary[key] = value;
}
}
@ -115,47 +129,21 @@ namespace Microsoft.AspNet.Routing
/// <remarks>
/// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
/// </remarks>
public IEqualityComparer<string> Comparer
{
get
{
return _dictionary.Comparer;
}
}
public IEqualityComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
/// <inheritdoc />
public int Count
{
get
{
return _dictionary.Count;
}
}
public int Count => InnerDictionary?.Count ?? Properties?.Length ?? 0;
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.IsReadOnly
{
get
{
return ((ICollection<KeyValuePair<string, object>>)_dictionary).IsReadOnly;
}
}
bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
/// <inheritdoc />
public Dictionary<string, object>.KeyCollection Keys
public ICollection<string> Keys
{
get
{
return _dictionary.Keys;
}
}
/// <inheritdoc />
ICollection<string> IDictionary<string, object>.Keys
{
get
{
return _dictionary.Keys;
EnsureWritable();
return InnerDictionary.Keys;
}
}
@ -163,25 +151,18 @@ namespace Microsoft.AspNet.Routing
{
get
{
return _dictionary.Keys;
EnsureWritable();
return InnerDictionary.Keys;
}
}
/// <inheritdoc />
public Dictionary<string, object>.ValueCollection Values
public ICollection<object> Values
{
get
{
return _dictionary.Values;
}
}
/// <inheritdoc />
ICollection<object> IDictionary<string, object>.Values
{
get
{
return _dictionary.Values;
EnsureWritable();
return InnerDictionary.Values;
}
}
@ -189,14 +170,16 @@ namespace Microsoft.AspNet.Routing
{
get
{
return _dictionary.Values;
EnsureWritable();
return InnerDictionary.Values;
}
}
/// <inheritdoc />
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{
((ICollection<KeyValuePair<string, object>>)_dictionary).Add(item);
EnsureWritable();
((ICollection<KeyValuePair<string, object>>)InnerDictionary).Add(item);
}
/// <inheritdoc />
@ -207,19 +190,22 @@ namespace Microsoft.AspNet.Routing
throw new ArgumentNullException(nameof(key));
}
_dictionary.Add(key, value);
EnsureWritable();
InnerDictionary.Add(key, value);
}
/// <inheritdoc />
public void Clear()
{
_dictionary.Clear();
EnsureWritable();
InnerDictionary.Clear();
}
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{
return ((ICollection<KeyValuePair<string, object>>)_dictionary).Contains(item);
EnsureWritable();
return ((ICollection<KeyValuePair<string, object>>)InnerDictionary).Contains(item);
}
/// <inheritdoc />
@ -230,7 +216,27 @@ namespace Microsoft.AspNet.Routing
throw new ArgumentNullException(nameof(key));
}
return _dictionary.ContainsKey(key);
if (InnerDictionary != null)
{
return InnerDictionary.ContainsKey(key);
}
else if (Properties != null)
{
for (var i = 0; i < Properties.Length; i++)
{
var property = Properties[i];
if (Comparer.Equals(property.Name, key))
{
return true;
}
}
return false;
}
else
{
return false;
}
}
/// <inheritdoc />
@ -243,31 +249,33 @@ namespace Microsoft.AspNet.Routing
throw new ArgumentNullException(nameof(array));
}
((ICollection<KeyValuePair<string, object>>)_dictionary).CopyTo(array, arrayIndex);
EnsureWritable();
((ICollection<KeyValuePair<string, object>>)InnerDictionary).CopyTo(array, arrayIndex);
}
/// <inheritdoc />
public Dictionary<string, object>.Enumerator GetEnumerator()
public Enumerator GetEnumerator()
{
return _dictionary.GetEnumerator();
return new Enumerator(this);
}
/// <inheritdoc />
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
return _dictionary.GetEnumerator();
return GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return _dictionary.GetEnumerator();
return GetEnumerator();
}
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
{
return ((ICollection<KeyValuePair<string, object>>)_dictionary).Remove(item);
EnsureWritable();
return ((ICollection<KeyValuePair<string, object>>)InnerDictionary).Remove(item);
}
/// <inheritdoc />
@ -278,7 +286,8 @@ namespace Microsoft.AspNet.Routing
throw new ArgumentNullException(nameof(key));
}
return _dictionary.Remove(key);
EnsureWritable();
return InnerDictionary.Remove(key);
}
/// <inheritdoc />
@ -289,7 +298,115 @@ namespace Microsoft.AspNet.Routing
throw new ArgumentNullException(nameof(key));
}
return _dictionary.TryGetValue(key, out value);
if (InnerDictionary != null)
{
return InnerDictionary.TryGetValue(key, out value);
}
else if (Properties != null)
{
for (var i = 0; i < Properties.Length; i++)
{
var property = Properties[i];
if (Comparer.Equals(property.Name, key))
{
value = property.ValueGetter(Value);
return true;
}
}
value = null;
return false;
}
else
{
value = null;
return false;
}
}
private void EnsureWritable()
{
if (InnerDictionary == null && Properties == null)
{
InnerDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
else if (InnerDictionary == null)
{
InnerDictionary = new Dictionary<string, object>(Properties.Length, StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < Properties.Length; i++)
{
var property = Properties[i];
InnerDictionary.Add(property.Property.Name, property.ValueGetter(Value));
}
}
}
public struct Enumerator : IEnumerator<KeyValuePair<string, object>>
{
private readonly RouteValueDictionary _dictionary;
private int _index;
private Dictionary<string, object>.Enumerator _enumerator;
public Enumerator(RouteValueDictionary dictionary)
{
if (dictionary == null)
{
throw new ArgumentNullException();
}
_dictionary = dictionary;
Current = default(KeyValuePair<string, object>);
_index = -1;
_enumerator = _dictionary.InnerDictionary == null ?
default(Dictionary<string, object>.Enumerator) :
_dictionary.InnerDictionary.GetEnumerator();
}
public KeyValuePair<string, object> Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose()
{
}
public bool MoveNext()
{
if (_dictionary?.InnerDictionary != null)
{
if (_enumerator.MoveNext())
{
Current = _enumerator.Current;
return true;
}
}
else if (_dictionary?.Properties != null)
{
_index++;
if (_index < _dictionary.Properties.Length)
{
var property = _dictionary.Properties[_index];
var value = property.ValueGetter(_dictionary.Value);
Current = new KeyValuePair<string, object>(property.Name, value);
return true;
}
}
Current = default(KeyValuePair<string, object>);
return false;
}
public void Reset()
{
Current = default(KeyValuePair<string, object>);
_index = -1;
_enumerator = _dictionary?.InnerDictionary == null ?
default(Dictionary<string, object>.Enumerator) :
_dictionary.InnerDictionary.GetEnumerator();
}
}
}
}

View File

@ -28,10 +28,9 @@ namespace Microsoft.AspNet.Routing.Template
}
// Step 1: Get the list of values we're going to try to use to match and generate this URI
public TemplateValuesResult GetValues(IDictionary<string, object> ambientValues,
IDictionary<string, object> values)
public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary values)
{
var context = new TemplateBindingContext(_defaults, values);
var context = new TemplateBindingContext(_defaults);
// Find out which entries in the URI are valid for the URI we want to generate.
// If the URI had ordered parameters a="1", b="2", c="3" and the new values
@ -171,7 +170,7 @@ namespace Microsoft.AspNet.Routing.Template
}
// Step 2: If the route is a match generate the appropriate URI
public string BindValues(IDictionary<string, object> acceptedValues)
public string BindValues(RouteValueDictionary acceptedValues)
{
var context = new UriBuildingContext();
@ -355,15 +354,8 @@ namespace Microsoft.AspNet.Routing.Template
private readonly RouteValueDictionary _acceptedValues;
private readonly RouteValueDictionary _filters;
public TemplateBindingContext(
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> values)
public TemplateBindingContext(IReadOnlyDictionary<string, object> defaults)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
_defaults = defaults;
_acceptedValues = new RouteValueDictionary();

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Routing.Template
public RouteTemplate Template { get; private set; }
public IDictionary<string, object> Match(PathString path)
public RouteValueDictionary Match(PathString path)
{
var i = 0;
var pathTokenizer = new PathTokenizer(path);

View File

@ -317,8 +317,8 @@ namespace Microsoft.AspNet.Routing.Template
}
private static void MergeValues(
IDictionary<string, object> destination,
IDictionary<string, object> values)
RouteValueDictionary destination,
RouteValueDictionary values)
{
foreach (var kvp in values)
{

View File

@ -1,8 +1,6 @@
// 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.Collections.Generic;
namespace Microsoft.AspNet.Routing.Template
{
/// <summary>
@ -13,7 +11,7 @@ namespace Microsoft.AspNet.Routing.Template
/// <summary>
/// The set of values that will appear in the URL.
/// </summary>
public IDictionary<string, object> AcceptedValues { get; set; }
public RouteValueDictionary AcceptedValues { get; set; }
/// <summary>
/// The set of values that that were supplied for URL generation.
@ -26,6 +24,6 @@ namespace Microsoft.AspNet.Routing.Template
/// Implicit (ambient) values which are invalidated due to changes in values lexically earlier in the
/// route template are excluded from this set.
/// </remarks>
public IDictionary<string, object> CombinedValues { get; set; }
public RouteValueDictionary CombinedValues { get; set; }
}
}

View File

@ -8,17 +8,19 @@ namespace Microsoft.AspNet.Routing
{
public class VirtualPathContext
{
public VirtualPathContext(HttpContext httpContext,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values)
public VirtualPathContext(
HttpContext httpContext,
RouteValueDictionary ambientValues,
RouteValueDictionary values)
: this(httpContext, ambientValues, values, null)
{
}
public VirtualPathContext(HttpContext context,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values,
string routeName)
public VirtualPathContext(
HttpContext context,
RouteValueDictionary ambientValues,
RouteValueDictionary values,
string routeName)
{
Context = context;
AmbientValues = ambientValues;
@ -26,16 +28,16 @@ namespace Microsoft.AspNet.Routing
RouteName = routeName;
}
public string RouteName { get; private set; }
public string RouteName { get; }
public IDictionary<string, object> ProvidedValues { get; set; }
public IDictionary<string, object> AmbientValues { get; private set; }
public RouteValueDictionary AmbientValues { get; }
public HttpContext Context { get; private set; }
public HttpContext Context { get; }
public bool IsBound { get; set; }
public IDictionary<string, object> Values { get; private set; }
public RouteValueDictionary Values { get; }
}
}

View File

@ -2,7 +2,6 @@
// 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 Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Routing
@ -13,7 +12,7 @@ namespace Microsoft.AspNet.Routing
/// </summary>
public class VirtualPathData
{
private readonly IDictionary<string, object> _dataTokens;
private RouteValueDictionary _dataTokens;
/// <summary>
/// Initializes a new instance of the <see cref="VirtualPathData"/> class.
@ -21,7 +20,7 @@ namespace Microsoft.AspNet.Routing
/// <param name="router">The object that is used to generate the URL.</param>
/// <param name="virtualPath">The generated URL.</param>
public VirtualPathData(IRouter router, string virtualPath)
: this(router, virtualPath, dataTokens: new RouteValueDictionary())
: this(router, virtualPath, dataTokens: null)
{
}
@ -34,7 +33,7 @@ namespace Microsoft.AspNet.Routing
public VirtualPathData(
IRouter router,
string virtualPath,
IDictionary<string, object> dataTokens)
RouteValueDictionary dataTokens)
: this(router, CreatePathString(virtualPath), dataTokens)
{
}
@ -48,7 +47,7 @@ namespace Microsoft.AspNet.Routing
public VirtualPathData(
IRouter router,
PathString virtualPath,
IDictionary<string, object> dataTokens)
RouteValueDictionary dataTokens)
{
if (router == null)
{
@ -57,23 +56,23 @@ namespace Microsoft.AspNet.Routing
Router = router;
VirtualPath = virtualPath;
_dataTokens = new RouteValueDictionary();
if (dataTokens != null)
{
foreach (var dataToken in dataTokens)
{
_dataTokens.Add(dataToken.Key, dataToken.Value);
}
}
_dataTokens = dataTokens == null ? null : new RouteValueDictionary(dataTokens);
}
/// <summary>
/// Gets the collection of custom values for the <see cref="Router"/>.
/// </summary>
public IDictionary<string, object> DataTokens
public RouteValueDictionary DataTokens
{
get { return _dataTokens; }
get
{
if (_dataTokens == null)
{
_dataTokens = new RouteValueDictionary();
}
return _dataTokens;
}
}
/// <summary>

View File

@ -9,21 +9,25 @@
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.Routing.DecisionTree.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.OptionsModel": "1.0.0-*"
"dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.Routing.DecisionTree.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.OptionsModel": "1.0.0-*",
"Microsoft.Extensions.PropertyHelper.Sources": {
"type": "build",
"version": "1.0.0-*"
}
},
"frameworks": {
"net451": {},
"net451": { },
"dotnet5.4": {
"dependencies": {
"System.Text.RegularExpressions": "4.0.11-*"

View File

@ -165,7 +165,11 @@ namespace Microsoft.AspNet.Routing.Tests
// Act & Assert
ExceptionAssert.Throws<ArgumentException>(
() => new RouteValueDictionary(obj),
() =>
{
var dictionary = new RouteValueDictionary(obj);
dictionary.Add("Hi", "There");
},
expected);
}

View File

@ -127,7 +127,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void Binding_WithEmptyAndNull_DefaultValues(
string template,
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> values,
RouteValueDictionary values,
string expected)
{
// Arrange
@ -284,8 +284,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
public void GetVirtualPathWithMultiSegmentWithOptionalParam(
string template,
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values,
RouteValueDictionary ambientValues,
RouteValueDictionary values,
string expected)
{
// Arrange
@ -1086,8 +1086,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
private static void RunTest(
string template,
IReadOnlyDictionary<string, object> defaults,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values,
RouteValueDictionary ambientValues,
RouteValueDictionary values,
string expected)
{
// Arrange

View File

@ -1658,8 +1658,8 @@ namespace Microsoft.AspNet.Routing.Template
}
private static VirtualPathContext CreateVirtualPathContext(
IDictionary<string, object> values,
IDictionary<string, object> ambientValues)
RouteValueDictionary values,
RouteValueDictionary ambientValues)
{
var context = new Mock<HttpContext>(MockBehavior.Strict);
context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))