Skip to content

Conversation

@h3xds1nz
Copy link
Member

@h3xds1nz h3xds1nz commented Aug 10, 2024

Description

Optimizes name lookup of DependencyProperty, this way all name-lookups are allocation free as the key is now struct and is created on stack everytime. Since name is looked up when registering property as well, this optimizes that case too.

  • Hashtable is replaced with Dictionary<FromNameKey, DependencyProperty>, offering perf benefits.
  • Thread-safety concerns do not apply, all operations on the dictionary are under the same lock.
  • FromNameKey is now struct instead of class, which removes any heap/gen0 allocations on lookups.
  • The Synchronized field for locking is now System.Threading.Lock instead of object (slight perf improvement).
  • Since key is a struct now, the memory footprint of the whole collection will be lower.

No locking

Method name ownerType Mean [ns] Error [ns] StdDev [ns] Code Size [B] Gen0 Allocated
Original BaseClass BaseClass 28.09 ns 0.155 ns 0.137 ns 1,923 B 0.0024 40 B
PR__EDIT BaseClass BaseClass 9.23 ns 0.083 ns 0.077 ns 1,334 B - -
Original BaseClass ChildClass 58.71 ns 0.502 ns 0.811 ns 2,029 B 0.0024 40 B
PR__EDIT BaseClass ChildClass 33.88 ns 0.463 ns 0.432 ns 1,655 B - -
Original BaseClass ChildClass4 99.79 ns 0.999 ns 0.935 ns 2,036 B 0.0024 40 B
PR__EDIT BaseClass ChildClass4 73.89 ns 1.213 ns 1.135 ns 1,637 B - -

With locking (object/Lock)

Method name ownerType Mean [ns] Error [ns] StdDev [ns] Code Size [B] Gen0 Allocated
Original BaseClass BaseClass 33.69 ns 0.422 ns 0.374 ns 2,071 B 0.0024 40 B
PR__EDIT BaseClass BaseClass 14.42 ns 0.155 ns 0.145 ns 1,705 B - -
Original BaseClass ChildClass 73.03 ns 0.648 ns 0.606 ns 2,167 B 0.0024 40 B
PR__EDIT BaseClass ChildClass 45.95 ns 0.540 ns 0.505 ns 1,991 B - -
Original BaseClass ChildClass4 132.80 ns 1.390 ns 1.300 ns 2,138 B 0.0024 40 B
PR__EDIT BaseClass ChildClass4 94.16 ns 0.977 ns 1.200 ns 1,989 B - -

Benchmark code

Benchmark code (without types)
public class Descendant : BaseClass { }
public class ChildClass : Descendant { }
public class ChildClass2 : ChildClass { }
public class ChildClass3 : ChildClass2 { }
public class ChildClass4 : ChildClass3 { }

public class FakeDependencyProperty { }

private static Hashtable propertyFromNameOld = new();
private static object SynchronizedOld = new object();

private static readonly Dictionary<FromNameKey, FakeDependencyProperty> propertyFromName = new();
private static readonly Lock Synchronized = new();

[GlobalSetup]
public void Setup()
{
    propertyFromNameOld.Add(new FromNameKeyOld("BaseClass", typeof(BaseClass)), new FakeDependencyProperty());
    propertyFromNameOld.Add(new FromNameKeyOld("Descendant", typeof(Descendant)), new FakeDependencyProperty());
    propertyFromNameOld.Add(new FromNameKeyOld("ChildClass", typeof(ChildClass)), new FakeDependencyProperty());
    propertyFromNameOld.Add(new FromNameKeyOld("ChildClass4", typeof(ChildClass4)), new FakeDependencyProperty());

    propertyFromName.Add(new FromNameKey("BaseClass", typeof(BaseClass)), new FakeDependencyProperty());
    propertyFromName.Add(new FromNameKey("Descendant", typeof(Descendant)), new FakeDependencyProperty());
    propertyFromName.Add(new FromNameKey("ChildClass", typeof(ChildClass)), new FakeDependencyProperty());
    propertyFromName.Add(new FromNameKey("ChildClass4", typeof(ChildClass4)), new FakeDependencyProperty());

}

public static IEnumerable<object[]> ArgumentParams()
{
    yield return new object[] { "BaseClass", typeof(BaseClass) };
    yield return new object[] { "BaseClass", typeof(ChildClass) };
    yield return new object[] { "BaseClass", typeof(ChildClass4) };
}

[Benchmark]
[ArgumentsSource(nameof(ArgumentParams))]
public FakeDependencyProperty Original(string name, Type ownerType)
{
    FakeDependencyProperty dp = null;
    FromNameKeyOld key = new FromNameKeyOld(name, ownerType);

    while ((dp == null) && (ownerType != null))
    {
        // Locate property
        key.UpdateNameKey(ownerType);
        lock (SynchronizedOld)
        {
            dp = (FakeDependencyProperty)propertyFromNameOld[key];
        }

        ownerType = ownerType.BaseType;
    }

    return dp;
}

[Benchmark]
[ArgumentsSource(nameof(ArgumentParams))]
public FakeDependencyProperty PR__EDIT(string name, Type ownerType)
{
    FakeDependencyProperty dp = null;

    while ((dp == null) && (ownerType != null))
    {
        // Locate property
        FromNameKey key = new FromNameKey(name, ownerType);
        lock (Synchronized)
        {
            if (propertyFromName.TryGetValue(key, out dp))
                return dp;
        }

        ownerType = ownerType.BaseType;
    }

    return dp;
}

Customer Impact

Given how heavily DependencyProperty is used, this is a decent (often times more than 50%) perf speed improvement, and also the overall memory usage is reduced.

Regression

No.

Testing

Local build, sample app run.

Risk

Low, the swap is safe.

Microsoft Reviewers: Open in CodeFlow

@h3xds1nz h3xds1nz requested review from a team as code owners August 10, 2024 19:22
@dotnet-policy-service dotnet-policy-service bot added PR metadata: Label to tag PRs, to facilitate with triage Community Contribution A label for all community Contributions labels Aug 10, 2024
@h3xds1nz h3xds1nz changed the title Optimize DependencyProperty name lookup/registration performance, remove allocations Optimize DependencyProperty lookup/registration performance, remove allocs Aug 10, 2024
@harshit7962
Copy link
Member

@h3xds1nz Thank you for your active contributions.

@h3xds1nz
Copy link
Member Author

h3xds1nz commented Oct 1, 2024

@harshit7962 Happy to contribute, thank you.

@github-actions github-actions bot locked and limited conversation to collaborators Nov 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Community Contribution A label for all community Contributions PR metadata: Label to tag PRs, to facilitate with triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants