Optimize ListStorage in RouteValueDictionary

This commit is contained in:
Ajay Bhargav Baaskaran 2016-07-13 11:04:24 -07:00
parent 6801715daf
commit dde24fbba0
2 changed files with 174 additions and 32 deletions

View File

@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Routing
throw new ArgumentException(message, nameof(values)); throw new ArgumentException(message, nameof(values));
} }
listStorage._inner.Add(kvp); listStorage.Add(kvp);
} }
return; return;
@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Routing
throw new ArgumentException(message, nameof(values)); throw new ArgumentException(message, nameof(values));
} }
listStorage._inner.Add(new KeyValuePair<string, object>(kvp.Key, kvp.Value)); listStorage.Add(new KeyValuePair<string, object>(kvp.Key, kvp.Value));
} }
return; return;
@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Routing
{ {
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
var keys = new string[list.Count]; var keys = new string[list.Count];
for (var i = 0; i < keys.Length; i++) for (var i = 0; i < keys.Length; i++)
{ {
@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Routing
{ {
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
var values = new object[list.Count]; var values = new object[list.Count];
for (var i = 0; i < values.Length; i++) for (var i = 0; i < values.Length; i++)
{ {
@ -223,7 +223,7 @@ namespace Microsoft.AspNetCore.Routing
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
for (var i = 0; i < list.Count; i++) for (var i = 0; i < list.Count; i++)
{ {
if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase))
@ -246,7 +246,7 @@ namespace Microsoft.AspNetCore.Routing
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
list.Clear(); list.Clear();
} }
@ -260,7 +260,7 @@ namespace Microsoft.AspNetCore.Routing
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
for (var i = 0; i < list.Count; i++) for (var i = 0; i < list.Count; i++)
{ {
if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase)) if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase))
@ -305,7 +305,7 @@ namespace Microsoft.AspNetCore.Routing
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
list.CopyTo(array, arrayIndex); list.CopyTo(array, arrayIndex);
} }
@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.Routing
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
for (var i = 0; i < list.Count; i++) for (var i = 0; i < list.Count; i++)
{ {
if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase) && if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase) &&
@ -366,7 +366,7 @@ namespace Microsoft.AspNetCore.Routing
Upgrade(); Upgrade();
var list = ((ListStorage)_storage)._inner; var list = (ListStorage)_storage;
for (var i = 0; i < list.Count; i++) for (var i = 0; i < list.Count; i++)
{ {
if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase))
@ -445,7 +445,7 @@ namespace Microsoft.AspNetCore.Routing
{ {
public abstract int Count { get; } public abstract int Count { get; }
public abstract KeyValuePair<string, object> this[int index] { get; } public abstract KeyValuePair<string, object> this[int index] { get; set; }
public abstract void Upgrade(ref Storage storage); public abstract void Upgrade(ref Storage storage);
@ -458,42 +458,115 @@ namespace Microsoft.AspNetCore.Routing
internal class ListStorage : Storage internal class ListStorage : Storage
{ {
internal readonly List<KeyValuePair<string, object>> _inner; private KeyValuePair<string, object>[] _items;
private int _count;
private static readonly KeyValuePair<string, object>[] _emptyArray = new KeyValuePair<string, object>[0];
public ListStorage() public ListStorage()
{ {
_inner = new List<KeyValuePair<string, object>>(); _items = _emptyArray;
} }
public ListStorage(int capacity) public ListStorage(int capacity)
{ {
_inner = new List<KeyValuePair<string, object>>(capacity); if (capacity == 0)
{
_items = _emptyArray;
}
else
{
_items = new KeyValuePair<string, object>[capacity];
}
} }
public ListStorage(ListStorage other) public ListStorage(ListStorage other)
{ {
// Perf: Don't call the copy constructor, that would box the enumerator. if (other.Count == 0)
_inner = new List<KeyValuePair<string, object>>(other._inner.Capacity);
for (var i = 0; i < other._inner.Count; i++)
{ {
_inner.Add(other._inner[i]); _items = _emptyArray;
}
else
{
_items = new KeyValuePair<string, object>[other.Count];
for (var i = 0; i < other.Count; i++)
{
this.Add(other[i]);
}
} }
} }
public override int Count => _inner.Count; public int Capacity => _items.Length;
public override KeyValuePair<string, object> this[int index] => _inner[index]; public override int Count => _count;
public override KeyValuePair<string, object> this[int index]
{
get
{
if (index < 0 || index >= _count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
return _items[index];
}
set
{
if (index < 0 || index >= _count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
_items[index] = value;
}
}
public void Add(KeyValuePair<string, object> item)
{
if (_count == _items.Length)
{
EnsureCapacity(_count + 1);
}
_items[_count++] = item;
}
public void RemoveAt(int index)
{
_count--;
for (var i = index; i < _count; i++)
{
_items[i] = _items[i + 1];
}
_items[_count] = default(KeyValuePair<string, object>);
}
public void Clear() public void Clear()
{ {
_inner.Clear(); for (var i = 0; i < _count; i++)
{
_items[i] = default(KeyValuePair<string, object>);
}
_count = 0;
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
for (var i = 0; i < _count; i++)
{
array[arrayIndex++] = _items[i];
}
} }
public override bool ContainsKey(string key) public override bool ContainsKey(string key)
{ {
for (var i = 0; i < _inner.Count; i++) for (var i = 0; i < Count; i++)
{ {
var kvp = _inner[i]; var kvp = _items[i];
if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase))
{ {
return true; return true;
@ -505,25 +578,25 @@ namespace Microsoft.AspNetCore.Routing
public override bool TrySetValue(string key, object value) public override bool TrySetValue(string key, object value)
{ {
for (var i = 0; i < _inner.Count; i++) for (var i = 0; i < Count; i++)
{ {
var kvp = _inner[i]; var kvp = _items[i];
if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase))
{ {
_inner[i] = new KeyValuePair<string, object>(key, value); _items[i] = new KeyValuePair<string, object>(key, value);
return true; return true;
} }
} }
_inner.Add(new KeyValuePair<string, object>(key, value)); Add(new KeyValuePair<string, object>(key, value));
return true; return true;
} }
public override bool TryGetValue(string key, out object value) public override bool TryGetValue(string key, out object value)
{ {
for (var i = 0; i < _inner.Count; i++) for (var i = 0; i < Count; i++)
{ {
var kvp = _inner[i]; var kvp = _items[i];
if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase))
{ {
value = kvp.Value; value = kvp.Value;
@ -539,6 +612,18 @@ namespace Microsoft.AspNetCore.Routing
{ {
// Do nothing. // Do nothing.
} }
private void EnsureCapacity(int min)
{
var newLength = _items.Length == 0 ? 4 : _items.Length * 2;
var newItems = new KeyValuePair<string, object>[newLength];
for (var i = 0; i < _count; i++)
{
newItems[i] = _items[i];
}
_items = newItems;
}
} }
internal class PropertyStorage : Storage internal class PropertyStorage : Storage
@ -578,6 +663,11 @@ namespace Microsoft.AspNetCore.Routing
var property = _properties[index]; var property = _properties[index];
return new KeyValuePair<string, object>(property.Name, property.GetValue(_value)); return new KeyValuePair<string, object>(property.Name, property.GetValue(_value));
} }
set
{
// PropertyStorage never sets a value.
throw new NotImplementedException();
}
} }
public override bool TryGetValue(string key, out object value) public override bool TryGetValue(string key, out object value)
@ -665,6 +755,10 @@ namespace Microsoft.AspNetCore.Routing
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
set
{
throw new NotImplementedException();
}
} }
public override bool ContainsKey(string key) public override bool ContainsKey(string key)

View File

@ -53,7 +53,6 @@ namespace Microsoft.AspNetCore.Routing.Tests
var storage = Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage); var storage = Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
var otherStorage = Assert.IsType<RouteValueDictionary.ListStorage>(other._storage); var otherStorage = Assert.IsType<RouteValueDictionary.ListStorage>(other._storage);
Assert.NotSame(otherStorage, storage); Assert.NotSame(otherStorage, storage);
Assert.NotSame(otherStorage._inner, storage._inner);
} }
[Fact] [Fact]
@ -328,7 +327,7 @@ namespace Microsoft.AspNetCore.Routing.Tests
[Fact] [Fact]
public void CreateFromObject_MixedCaseThrows() public void CreateFromObject_MixedCaseThrows()
{ {
// Arrange // Arrange
var obj = new { controller = "Home", Controller = "Home" }; var obj = new { controller = "Home", Controller = "Home" };
var message = var message =
@ -336,7 +335,7 @@ namespace Microsoft.AspNetCore.Routing.Tests
$"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " +
$"case-insensitive comparisons."; $"case-insensitive comparisons.";
// Act & Assert // Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => var exception = Assert.Throws<InvalidOperationException>(() =>
{ {
var dictionary = new RouteValueDictionary(obj); var dictionary = new RouteValueDictionary(obj);
@ -1438,6 +1437,55 @@ namespace Microsoft.AspNetCore.Routing.Tests
Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage); Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
} }
[Fact]
public void ListStorage_DynamicallyAdjustsCapacity()
{
// Arrange
var dict = new RouteValueDictionary();
// Act 1
dict.Add("key", "value");
// Assert 1
var storage = Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
Assert.Equal(4, storage.Capacity);
// Act 2
dict.Add("key2", "value2");
dict.Add("key3", "value3");
dict.Add("key4", "value4");
dict.Add("key5", "value5");
// Assert 2
Assert.Equal(8, storage.Capacity);
}
[Fact]
public void ListStorage_RemoveAt_RearrangesInnerArray()
{
// Arrange
var dict = new RouteValueDictionary();
dict.Add("key", "value");
dict.Add("key2", "value2");
dict.Add("key3", "value3");
// Assert 1
var storage = Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
Assert.Equal(3, storage.Count);
// Act
dict.Remove("key2");
// Assert 2
Assert.Equal(2, storage.Count);
Assert.Equal("key", storage[0].Key);
Assert.Equal("value", storage[0].Value);
Assert.Equal("key3", storage[1].Key);
Assert.Equal("value3", storage[1].Value);
Assert.Throws<ArgumentOutOfRangeException>(() => storage[2]);
}
private class RegularType private class RegularType
{ {
public bool IsAwesome { get; set; } public bool IsAwesome { get; set; }