Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit

Permalink
Improve perf of property name lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
steveharter committed Sep 10, 2019
1 parent bca5498 commit a3c8a26
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 82 deletions.
191 changes: 112 additions & 79 deletions src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace System.Text.Json
internal sealed partial class JsonClassInfo
{
// The length of the property name embedded in the key (in bytes).
private const int PropertyNameKeyLength = 6;
private const int PropertyNameKeyLength = 7;

// The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache.
private const int PropertyNameCountCacheThreshold = 64;
Expand Down Expand Up @@ -259,83 +259,123 @@ private JsonPropertyInfo GetPropertyWithUniqueAttribute(Type attributeType, Dict

public JsonPropertyInfo GetProperty(ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame)
{
JsonPropertyInfo info = null;

// Keep a local copy of the cache in case it changes by another thread.
PropertyRef[] localPropertyRefsSorted = _propertyRefsSorted;

ulong key = GetKey(propertyName);

// If there is an existing cache, then use it.
if (localPropertyRefsSorted != null)
{
ulong key = GetKey(propertyName);

// Start with the current property index, and then go forwards\backwards.
int propertyIndex = frame.PropertyIndex;

int count = localPropertyRefsSorted.Length;
int iForward = Math.Min(propertyIndex, count);
int iBackward = iForward - 1;

while (iForward < count || iBackward >= 0)
for (;;)
{
if (iForward < count)
{
if (TryIsPropertyRefEqual(localPropertyRefsSorted[iForward], propertyName, key, ref info))
PropertyRef propertyRef = localPropertyRefsSorted[iForward];
if (key == propertyRef.Key)
{
return info;
if (propertyName.Length <= PropertyNameKeyLength ||
// We compare the whole name, although we could skip the first 7 bytes (but it's not any faster)
propertyName.SequenceEqual(propertyRef.Info.Name))
{
return propertyRef.Info;
}
}
++iForward;
}

if (iBackward >= 0)
if (iBackward >= 0)
{
propertyRef = localPropertyRefsSorted[iBackward];
// This path is the same code as above; copied here instead of creating a method since this is a hot path.
if (key == propertyRef.Key)
{
if (propertyName.Length <= PropertyNameKeyLength ||
propertyName.SequenceEqual(propertyRef.Info.Name))
{
return propertyRef.Info;
}
}

--iBackward;
}
}
else if (iBackward >= 0)
{
if (TryIsPropertyRefEqual(localPropertyRefsSorted[iBackward], propertyName, key, ref info))
// This path is the same code as above; copied here instead of creating a method since this is a hot path.
PropertyRef propertyRef = localPropertyRefsSorted[iBackward];
if (key == propertyRef.Key)
{
return info;
if (propertyName.Length <= PropertyNameKeyLength ||
propertyName.SequenceEqual(propertyRef.Info.Name))
{
return propertyRef.Info;
}
}

--iBackward;
}
else
{
// Property was not found.
break;
}
}
}

// No cached item was found. Try the main list which has all of the properties.

string stringPropertyName = JsonHelpers.Utf8GetString(propertyName);
if (PropertyCache.TryGetValue(stringPropertyName, out info))
PropertyCache.TryGetValue(stringPropertyName, out JsonPropertyInfo info);

// Three code paths to get here:
// 1) info == null. Property not found.
// 2) key == info.PropertyNameKey. Exact match found.
// 3) key != info.PropertyNameKey. Match found due to case insensitivity.
Debug.Assert(info == null || key == info.PropertyNameKey || Options.PropertyNameCaseInsensitive);

if (info == null)
{
// Check if we should add this to the cache.
// Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache.
int count;
if (localPropertyRefsSorted != null)
{
count = localPropertyRefsSorted.Length;
}
else
info = JsonPropertyInfo.s_missingProperty;
}

// Check if we should add this to the cache.
// Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache.
int cacheCount;
if (localPropertyRefsSorted != null)
{
cacheCount = localPropertyRefsSorted.Length;
}
else
{
cacheCount = 0;
}

// Do a quick check for the stable (after warm-up) case.
if (cacheCount < PropertyNameCountCacheThreshold)
{
// Do a slower check for the warm-up case.
if (frame.PropertyRefCache != null)
{
count = 0;
cacheCount += frame.PropertyRefCache.Count;
}

// Do a quick check for the stable (after warm-up) case.
if (count < PropertyNameCountCacheThreshold)
// Check again to append the cache up to the threshold.
if (cacheCount < PropertyNameCountCacheThreshold)
{
// Do a slower check for the warm-up case.
if (frame.PropertyRefCache != null)
if (frame.PropertyRefCache == null)
{
count += frame.PropertyRefCache.Count;
frame.PropertyRefCache = new List<PropertyRef>();
}

// Check again to append the cache up to the threshold.
if (count < PropertyNameCountCacheThreshold)
{
if (frame.PropertyRefCache == null)
{
frame.PropertyRefCache = new List<PropertyRef>();
}

ulong key = info.PropertyNameKey;
PropertyRef propertyRef = new PropertyRef(key, info);
frame.PropertyRefCache.Add(propertyRef);
}
PropertyRef propertyRef = new PropertyRef(key, info);
frame.PropertyRefCache.Add(propertyRef);
}
}

Expand All @@ -360,60 +400,52 @@ private Dictionary<string, JsonPropertyInfo> CreatePropertyCache(int capacity)

public JsonPropertyInfo PolicyProperty { get; private set; }

private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan<byte> propertyName, ulong key, ref JsonPropertyInfo info)
{
if (key == propertyRef.Key)
{
if (propertyName.Length <= PropertyNameKeyLength ||
// We compare the whole name, although we could skip the first 6 bytes (but it's likely not any faster)
propertyName.SequenceEqual(propertyRef.Info.Name))
{
info = propertyRef.Info;
return true;
}
}

return false;
}

private static bool IsPropertyRefEqual(ref PropertyRef propertyRef, PropertyRef other)
{
if (propertyRef.Key == other.Key)
{
if (propertyRef.Info.Name.Length <= PropertyNameKeyLength ||
propertyRef.Info.Name.AsSpan().SequenceEqual(other.Info.Name.AsSpan()))
{
return true;
}
}

return false;
}

/// <summary>
/// Get a key from the property name.
/// If property name length is 7 or less then this key can be used for equality since
/// the first 7 bytes are encoded along with the length.
/// </summary>
public static ulong GetKey(ReadOnlySpan<byte> propertyName)
{
ulong key;
int length = propertyName.Length;

// Embed the propertyName in the first 6 bytes of the key.
if (length > 3)
// Embed the propertyName in the first 7 bytes of the key.
if (length > 7)
{
key = MemoryMarshal.Read<ulong>(propertyName);

// Clear the last byte so it can hold the length.
// Be consistent by using shift operators instead of & to avoid endianness issues.
key = key << 0x8 >> 0x8;
}
else if (length > 3)
{
key = MemoryMarshal.Read<uint>(propertyName);
if (length > 4)

if (length == 7)
{
key |= (ulong)propertyName[4] << 32;
key |= (ulong)propertyName[4] << 0x20
| (ulong)propertyName[5] << 0x28
| (ulong)propertyName[6] << 0x30;
}
if (length > 5)
else if (length == 6)
{
key |= (ulong)propertyName[5] << 40;
key |= (ulong)propertyName[4] << 0x20
| (ulong)propertyName[5] << 0x28;
}
else if (length == 5)
{
key |= (ulong)propertyName[4] << 0x20;
}
}
else if (length > 1)
{
key = MemoryMarshal.Read<ushort>(propertyName);
if (length > 2)

if (length == 3)
{
key |= (ulong)propertyName[2] << 16;
key |= (ulong)propertyName[2] << 0x10;
}
}
else if (length == 1)
Expand All @@ -426,8 +458,9 @@ public static ulong GetKey(ReadOnlySpan<byte> propertyName)
key = 0;
}

// Embed the propertyName length in the last two bytes.
key |= (ulong)propertyName.Length << 48;
// Embed the length in the last byte.
key |= (ulong)Math.Min(propertyName.Length, 0xFF) << 0x38;

return key;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ private static void HandlePropertyName(
}

JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current);
if (jsonPropertyInfo == null)
if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty)
{
JsonPropertyInfo dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty;
if (dataExtProperty == null)
Expand Down Expand Up @@ -94,9 +94,10 @@ private static void HandlePropertyName(
state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray;
}
}

state.Current.PropertyIndex++;
}

// Increment the PropertyIndex so JsonClassInfo.GetProperty() starts with the next property.
state.Current.PropertyIndex++;
}
}

Expand Down
82 changes: 82 additions & 0 deletions src/System.Text.Json/tests/Serialization/PropertyNameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,87 @@ public static void ExtensionDataDictionarySerialize_DoesNotHonor()
Assert.False(obj.MyOverflow.ContainsKey("key1"));
Assert.Equal(1, obj.MyOverflow["Key1"].GetInt32());
}

private class ClassWithPropertyNamePermutations
{
public int a { get; set; }
public int aa { get; set; }
public int aaa { get; set; }
public int aaaa { get; set; }
public int aaaaa { get; set; }
public int aaaaaa { get; set; }

// 7 characters - caching code only keys up to 7.
public int aaaaaaa { get; set; }
public int aaaaaab { get; set; }

// 8 characters.
public int aaaaaaaa { get; set; }
public int aaaaaaab { get; set; }

// 9 characters - caching code doesn't differentiate past this.
public int aaaaaaaaa { get; set; }
public int aaaaaaaab { get; set; }
}

[Fact]
public static void CachingKeys()
{
ClassWithPropertyNamePermutations obj = new ClassWithPropertyNamePermutations
{
a = 1,
aa = 2,
aaa = 3,
aaaa = 4,
aaaaa = 5,
aaaaaa = 6,
aaaaaaa = 7,
aaaaaab = 7,
aaaaaaaa = 8,
aaaaaaab = 8,
aaaaaaaaa = 9,
aaaaaaaab = 9,
};

string json = JsonSerializer.Serialize(obj);
obj = JsonSerializer.Deserialize<ClassWithPropertyNamePermutations>(json);

Assert.Equal(130, json.Length);

Assert.Equal(1, obj.a);
Assert.Equal(2, obj.aa);
Assert.Equal(3, obj.aaa);
Assert.Equal(4, obj.aaaa);
Assert.Equal(5, obj.aaaaa);
Assert.Equal(6, obj.aaaaaa);
Assert.Equal(7, obj.aaaaaaa);
Assert.Equal(7, obj.aaaaaab);
Assert.Equal(8, obj.aaaaaaaa);
Assert.Equal(8, obj.aaaaaaab);
Assert.Equal(9, obj.aaaaaaaaa);
Assert.Equal(9, obj.aaaaaaaab);
}

[Theory]
[InlineData(0x1)]
[InlineData(0x10)]
[InlineData(0x100)]
[InlineData(0x1000)]
[InlineData(0x10000)]
public static void LongPropertyNames(int propertyLength)
{
// Although the CLR may limit member length to 1023, the serializer doesn't have a hard limit.

string val = new string('v', propertyLength);
string json = @"{""" + val + @""":1}";

EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);

Assert.True(obj.MyOverflow.ContainsKey(val));

json = JsonSerializer.Serialize(obj);
Assert.Contains(val, json);
}
}

public class OverridePropertyNameDesignTime_TestClass
Expand Down Expand Up @@ -359,3 +440,4 @@ public class EmptyClassWithExtensionProperty
public IDictionary<string, JsonElement> MyOverflow { get; set; }
}
}

0 comments on commit a3c8a26

Please sign in to comment.