Skip to content

perf: type-bound member accessors and no bool boxing (~10-33% faster object rendering) - #652

Merged
rexm merged 5 commits into
masterfrom
perf/typed-member-accessor
Aug 5, 2026
Merged

perf: type-bound member accessors and no bool boxing (~10-33% faster object rendering)#652
rexm merged 5 commits into
masterfrom
perf/typed-member-accessor

Conversation

@rexm

@rexm rexm commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Two targeted optimizations to the reflection-based member access path (the path used when rendering plain .NET objects, including anonymous types):

  1. Type-bound member accessors. ObjectDescriptors are already cached per type, but the shared ReflectionMemberAccessor re-resolved its per-type accessor table on every member access via a type-keyed LookupSlim lookup. The descriptor's member accessor is now pre-bound to the described type at descriptor creation, so the common case (instance type matches the descriptor) goes straight to the member-name lookup. If the instance type doesn't match the bound type, it falls back to the original shared path — behavior is unchanged.

  2. No boxing for bool property reads. The cached Func<object, object> getters box value-type property values on every read. bool properties — ubiquitous in {{#if}}/{{#unless}}-heavy templates — now return the cached BoxedValues.True/False instances instead of allocating a fresh box per read.

Dictionary-based data paths are untouched. All 1837 tests pass.

Benchmarks

MediumRun (LaunchCount=1, 15 iterations), Apple M4, .NET 10. Baseline is master @ 37ca7b9.

Object-path suites (the target of this change):

Benchmark Case master This PR Δ time master alloc PR alloc
RenderSimple object 856.85 ns 575.79 ns −32.8% 48 B 0 B
RenderNested object, rows=5 6,764.33 ns 5,650.00 ns −16.5% 1192 B 592 B
RenderNested object, rows=20 24.52 us 18.94 us −22.7% 4624 B 2224 B
RenderList N=10, object 2,916.26 ns 2,585.00 ns −11.4% 480 B 0 B
RenderList N=100, object 27.48 us 22.77 us −17.1% 6720 B 1920 B
RenderList N=1000, object 259.14 us 236.41 us −8.8% 71520 B 23520 B
RenderToString clean 13.95 us 12.93 us −7.3% 32.09 KB 30.91 KB
RenderToString html 14.75 us 13.83 us −6.2% 35.31 KB 34.14 KB

Full suite (including neutral paths):

All 30 benchmark cases
Benchmark Case master This PR Δ time master alloc PR alloc
Execution.CallHelperWithoutParameters 44.35 ns 44.08 ns -0.6% NA NA
EndToEnd N=5, dictionary 28.78 us 29.52 us +2.6% NA NA
EndToEnd N=5, object 29.49 us 28.38 us -3.8% NA NA
RenderNested dictionary, rows=5 5,413.42 ns 5,438.00 ns +0.5% 544 B 544 B
RenderNested object, rows=5 6,764.33 ns 5,650.00 ns -16.5% 1192 B 592 B
RenderNested dictionary, rows=20 18.54 us 18.14 us -2.2% 2176 B 2176 B
RenderNested object, rows=20 24.52 us 18.94 us -22.7% 4624 B 2224 B
RenderList N=10, dictionary 2,420.21 ns 2,435.00 ns +0.6% 0 B 0 B
RenderList N=10, object 2,916.26 ns 2,585.00 ns -11.4% 480 B 0 B
RenderList N=100, dictionary 23.42 us 23.20 us -0.9% 1920 B 1920 B
RenderList N=100, object 27.48 us 22.77 us -17.1% 6720 B 1920 B
RenderList N=1000, dictionary 231.70 us 235.50 us +1.6% 23520 B 23520 B
RenderList N=1000, object 259.14 us 236.41 us -8.8% 71520 B 23520 B
LargeArray N=20000 389.13 us 386.69 us -0.6% NA NA
LargeArray N=40000 801.86 us 777.20 us -3.1% NA NA
LargeArray N=80000 1.571 ms 1.561 ms -0.7% NA NA
RenderSimple dictionary 509.96 ns 501.56 ns -1.6% 0 B 0 B
RenderSimple expando 414.36 ns 410.34 ns -1.0% 0 B 0 B
RenderSimple object 856.85 ns 575.79 ns -32.8% 48 B 0 B
Execution.CallHelperWithOneParameter 46.16 ns 45.68 ns -1.0% NA NA
Execution.CallHelperWithTwoParameter 46.45 ns 47.37 ns +2.0% NA NA
Execution.LateCallHelperWithoutParameters 44.11 ns 43.66 ns -1.0% NA NA
Execution.LateCallHelperWithOneParameter 46.17 ns 45.70 ns -1.0% NA NA
Execution.LateCallHelperWithTwoParameter 46.32 ns 47.71 ns +3.0% NA NA
Execution.CallBlockHelperWithoutParameters 47.04 ns 47.44 ns +0.9% NA NA
Execution.CallBlockHelperWithOneParameter 47.65 ns 47.47 ns -0.4% NA NA
Execution.CallBlockHelperWithTwoParameter 48.15 ns 49.04 ns +1.8% NA NA
RenderToString clean 13.95 us 12.93 us -7.3% 32.09 KB 31656 B
RenderToString html 14.75 us 13.83 us -6.2% 35.31 KB 34960 B

Dictionary/expando cases and helper-dispatch (Execution.*) benchmarks are neutral (within ±3% run-to-run noise), as expected — this change only affects the reflection member-access path.

Remaining RenderList allocations at larger N are iterator index boxing beyond the BoxedValues int cache (0–19) — identical for object and dictionary data, and a possible follow-up.

Note: includes the RenderToString benchmark suite commit (also submitted separately as #650) so the numbers above are reproducible from this branch.

🤖 Generated with Claude Code

rexm and others added 4 commits August 4, 2026 22:02
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 <noreply@anthropic.com>
Value-type properties returned through the cached Func<object, object>
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 <noreply@anthropic.com>
…ssor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rexm
rexm enabled auto-merge August 5, 2026 03:45
@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@rexm
rexm merged commit 9ee2d48 into master Aug 5, 2026
7 checks passed
@rexm
rexm deleted the perf/typed-member-accessor branch August 5, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant