From 4114703e8333a3b73585ca23ed4677184efea7a8 Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Tue, 4 Aug 2026 22:02:10 -0400 Subject: [PATCH 1/2] perf: bind reflection member accessor to its descriptor's type Plain-object descriptors are cached per type, but ReflectionMemberAccessor re-resolved its per-type accessor table on every member access via a type-keyed lookup. The descriptor's member accessor is now pre-bound to the described type, so the common case (instance type matches the descriptor) goes straight to the member-name lookup. Mismatched instance types fall back to the original shared path. Co-Authored-By: Claude Fable 5 --- .../ReflectionMemberAccessor.cs | 59 +++++++++++++++++-- .../ObjectDescriptorProvider.cs | 2 +- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs index c7bb12e6..dfc31b49 100644 --- a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs @@ -29,6 +29,34 @@ public bool TryGetValue(object instance, ChainSegment memberName, out object val var instanceType = instance.GetType(); if (TryGetValueImpl(instance, instanceType, memberName, out value)) return true; + return TryGetValueByAlias(instance, instanceType, memberName, out value); + } + + /// + /// Creates an accessor pre-bound to so that per-access + /// type-descriptor lookup is skipped when the instance type matches. + /// + internal IMemberAccessor CreateBoundAccessor(Type type) => new BoundMemberAccessor(this, type, GetTypeDescriptor(type)); + + private RawObjectTypeDescriptor GetTypeDescriptor(Type instanceType) + { + if (!_descriptors.TryGetValue(instanceType, out var deferredValue)) + { + deferredValue = _descriptors.GetOrAdd(instanceType, DescriptorsValueFactory); + } + + return deferredValue.Value; + } + + private bool TryGetValueImpl(object instance, Type instanceType, ChainSegment memberName, out object value) + { + var accessor = GetTypeDescriptor(instanceType).GetOrCreateAccessor(memberName); + value = accessor?.Invoke(instance); + return accessor != null; + } + + private bool TryGetValueByAlias(object instance, Type instanceType, ChainSegment memberName, out object value) + { for (var index = 0; index < _aliasProviders.Count; index++) { if (_aliasProviders[index].TryGetMemberByAlias(instance, instanceType, memberName, out value)) @@ -39,16 +67,35 @@ public bool TryGetValue(object instance, ChainSegment memberName, out object val return false; } - private bool TryGetValueImpl(object instance, Type instanceType, ChainSegment memberName, out object value) + private sealed class BoundMemberAccessor : IMemberAccessor { - if (!_descriptors.TryGetValue(instanceType, out var deferredValue)) + private readonly ReflectionMemberAccessor _owner; + private readonly Type _type; + private readonly RawObjectTypeDescriptor _typeDescriptor; + + public BoundMemberAccessor(ReflectionMemberAccessor owner, Type type, RawObjectTypeDescriptor typeDescriptor) { - deferredValue = _descriptors.GetOrAdd(instanceType, DescriptorsValueFactory); + _owner = owner; + _type = type; + _typeDescriptor = typeDescriptor; } - var accessor = deferredValue.Value.GetOrCreateAccessor(memberName); - value = accessor?.Invoke(instance); - return accessor != null; + public bool TryGetValue(object instance, ChainSegment memberName, out object value) + { + if (!ReferenceEquals(instance.GetType(), _type)) + { + return _owner.TryGetValue(instance, memberName, out value); + } + + var accessor = _typeDescriptor.GetOrCreateAccessor(memberName); + if (accessor != null) + { + value = accessor.Invoke(instance); + return true; + } + + return _owner.TryGetValueByAlias(instance, _type, memberName, out value); + } } private sealed class RawObjectTypeDescriptor diff --git a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs index f377d631..f9867738 100644 --- a/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs +++ b/source/Handlebars/ObjectDescriptors/ObjectDescriptorProvider.cs @@ -25,7 +25,7 @@ public bool TryGetDescriptor(Type type, out ObjectDescriptor value) { value = new ObjectDescriptor( type, - _reflectionMemberAccessor, + _reflectionMemberAccessor.CreateBoundAccessor(type), GetProperties, self => new ObjectIterator(self), dependencies: _membersCache From f1f539f70ad22a1241c1434f481e5b0e209a3776 Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Tue, 4 Aug 2026 22:03:57 -0400 Subject: [PATCH 2/2] perf: avoid boxing bool property values in reflection getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Value-type properties returned through the cached Func getters box on every access. For bool properties — common in {{#if}}/ {{#unless}}-heavy templates — return the cached BoxedValues.True/False instances instead of allocating a fresh box per read. Co-Authored-By: Claude Fable 5 --- .../MemberAccessors/ReflectionMemberAccessor.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs index dfc31b49..c55ff553 100644 --- a/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs +++ b/source/Handlebars/MemberAccessors/ReflectionMemberAccessor.cs @@ -159,6 +159,15 @@ private static Func CreateGetDelegate(PropertyInfo pr { // Value types must be passed by reference var @delegate = (ValueTypeGetterDelegate)property.GetMethod.CreateDelegate(typeof(ValueTypeGetterDelegate)); + if (@delegate is ValueTypeGetterDelegate boolValueTypeGetter) + { + return o => + { + var to = (T)o; + return boolValueTypeGetter(ref to) ? BoxedValues.True : BoxedValues.False; + }; + } + return o => { var to = (T)o; @@ -168,6 +177,11 @@ private static Func CreateGetDelegate(PropertyInfo pr else { var @delegate = (Func) property.GetMethod.CreateDelegate(typeof(Func)); + if (@delegate is Func boolGetter) + { + return o => boolGetter((T) o) ? BoxedValues.True : BoxedValues.False; + } + return o => (object) @delegate((T) o); } }