Skip to content

perf: defer eager allocations in the mock-creation path#803

Merged
vbreuss merged 3 commits into
mainfrom
perf/lazy-mock-creation
Jun 21, 2026
Merged

perf: defer eager allocations in the mock-creation path#803
vbreuss merged 3 commits into
mainfrom
perf/lazy-mock-creation

Conversation

@vbreuss

@vbreuss vbreuss commented Jun 20, 2026

Copy link
Copy Markdown
Member

Empty-mock creation eagerly allocated several infrastructure objects that a mock which is only created (never set up, invoked, or verified) never needs. Defer them all to first use:

  • MockSetups and the scenario state are now lazy in MockRegistry; the Scenario getter no longer allocates on the invocation hot path, and the method-lookup read paths short-circuit to Array.Empty when no setups have been registered.
  • FastMockInteractions._verifiedLock is allocated only on the verification path.
  • The whole FastMockInteractions store is now built lazily by MockRegistry from a stored member count (new public MockRegistry(MockBehavior, int, object?[]?) ctor); the generator passes MemberCount instead of constructing the store up front. HttpClient's shared-store factory path stays eager.
  • _setupsByMemberIdLock (taken only by setup-registration paths) is allocated lazily too, removing the last eager allocation.

Empty interface-mock creation drops from ~440 B / ~51 ns (7 heap objects) to ~160 B / ~18 ns (2 heap objects: the mock and its MockRegistry).

The full create→setup→invoke→verify workflow benchmarks (method/property/indexer/event/callback) show no time regressions and slightly lower allocations, since each workflow no longer pays for the collaborators it doesn't touch.

Updates the public-API and generator-output snapshots accordingly.

Empty-mock creation eagerly allocated several infrastructure objects that a
mock which is only created (never set up, invoked, or verified) never needs.
Defer them to first use:

- MockSetups and the scenario state are now lazy in MockRegistry; the Scenario
  getter no longer allocates on the invocation hot path, and the method-lookup
  read paths short-circuit to Array.Empty when no setups have been registered.
- FastMockInteractions._verifiedLock is allocated only on the verification path.
- The whole FastMockInteractions store is now built lazily by MockRegistry from a
  stored member count (new public MockRegistry(MockBehavior, int, object?[]?)
  ctor); the generator passes MemberCount instead of constructing the store up
  front. HttpClient's shared-store factory path stays eager.

CreateMock for an empty interface mock drops from ~440 B / ~51 ns to
~184 B / ~18 ns (3 heap objects instead of 7).

Updates the public-API and generator-output snapshots accordingly.
@vbreuss vbreuss self-assigned this Jun 20, 2026
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

Test Results

    24 files  ±0      24 suites  ±0   8m 58s ⏱️ +42s
 4 171 tests ±0   4 169 ✅ ±0  2 💤 ±0  0 ❌ ±0 
26 845 runs  ±0  26 841 ✅ ±0  4 💤 ±0  0 ❌ ±0 

Results for commit 517f20e. ± Comparison against base commit a6a4978.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.59GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Callback Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 312.9 ns 4.40 ns 4.12 ns 1.01 1.68 KB 1.07
Mockolate 311.3 ns 6.74 ns 6.30 ns 1.00 1.57 KB 1.00
Imposter 490.0 ns 11.85 ns 11.09 ns 1.57 2.38 KB 1.52
TUnitMocks 487.2 ns 3.49 ns 2.73 ns 1.57 1.99 KB 1.27
Moq 98,669.4 ns 398.03 ns 332.38 ns 317.06 8.88 KB 5.66
NSubstitute 4,537.4 ns 23.71 ns 22.18 ns 14.58 7.74 KB 4.93
FakeItEasy 5,057.4 ns 21.74 ns 20.34 ns 16.25 6.81 KB 4.33
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Indexer N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 958.4 ns 42.67 ns 39.91 ns 0.59 3.82 KB 1.01
Mockolate 1 1,635.8 ns 29.66 ns 27.74 ns 1.00 3.77 KB 1.00
Imposter 1 848.4 ns 6.21 ns 5.81 ns 0.52 5.16 KB 1.37
Moq 1 172,975.3 ns 1,469.23 ns 1,302.43 ns 105.77 20.37 KB 5.41
NSubstitute 1 9,133.3 ns 63.64 ns 59.53 ns 5.58 12.84 KB 3.41
FakeItEasy 1 10,364.9 ns 68.72 ns 64.28 ns 6.34 13.63 KB 3.62
baseline* 10 2,502.3 ns 38.51 ns 36.03 ns 0.98 4.88 KB 1.01
Mockolate 10 2,546.9 ns 27.66 ns 25.88 ns 1.00 4.82 KB 1.00
Imposter 10 2,045.1 ns 52.85 ns 44.13 ns 0.80 7.97 KB 1.65
Moq 10 184,695.9 ns 1,003.81 ns 838.23 ns 72.52 29.89 KB 6.20
NSubstitute 10 21,879.0 ns 181.19 ns 169.48 ns 8.59 25.63 KB 5.32
FakeItEasy 10 21,105.1 ns 353.34 ns 330.51 ns 8.29 32.97 KB 6.84
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Property N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 552.0 ns 13.77 ns 12.88 ns 0.98 2.47 KB 1.02
Mockolate 1 565.9 ns 23.41 ns 21.89 ns 1.00 2.41 KB 1.00
Imposter 1 498.2 ns 12.83 ns 11.37 ns 0.88 3.13 KB 1.29
TUnitMocks 1 474.6 ns 9.70 ns 9.08 ns 0.84 1.64 KB 0.68
Moq 1 12,346.7 ns 119.50 ns 111.78 ns 21.85 10.39 KB 4.30
NSubstitute 1 7,664.5 ns 60.71 ns 53.82 ns 13.56 11.45 KB 4.74
FakeItEasy 1 8,539.8 ns 132.40 ns 117.37 ns 15.11 11.24 KB 4.66
baseline* 10 1,071.0 ns 8.26 ns 7.73 ns 0.99 2.96 KB 1.02
Mockolate 10 1,084.8 ns 37.29 ns 34.88 ns 1.00 2.91 KB 1.00
Imposter 10 1,170.1 ns 38.01 ns 35.56 ns 1.08 4.67 KB 1.61
TUnitMocks 10 1,663.5 ns 43.61 ns 40.79 ns 1.53 3.94 KB 1.35
Moq 10 19,436.0 ns 222.34 ns 207.98 ns 17.93 18.28 KB 6.29
NSubstitute 10 17,600.9 ns 91.87 ns 85.94 ns 16.24 21.08 KB 7.25
FakeItEasy 10 21,122.4 ns 304.19 ns 269.66 ns 19.49 30.81 KB 10.60
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Event Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 314.6 ns 3.88 ns 3.63 ns 1.04 1.78 KB 1.05
Mockolate 301.1 ns 8.01 ns 7.50 ns 1.00 1.7 KB 1.00
Imposter 1,379.9 ns 41.82 ns 39.12 ns 4.59 8.8 KB 5.17
TUnitMocks 196.6 ns 4.39 ns 3.89 ns 0.65 1.34 KB 0.79
Moq 16,699.3 ns 54.95 ns 42.90 ns 55.50 12.51 KB 7.34
NSubstitute 5,913.2 ns 52.36 ns 48.98 ns 19.65 9.05 KB 5.31
FakeItEasy 216,286.4 ns 2,077.74 ns 1,841.86 ns 718.82 15.26 KB 8.96
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.79GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 389.7 ns 8.75 ns 8.19 ns 0.94 2.04 KB 1.06
Mockolate 1 415.4 ns 11.27 ns 10.54 ns 1.00 1.93 KB 1.00
Imposter 1 674.3 ns 9.68 ns 8.58 ns 1.62 4.04 KB 2.09
TUnitMocks 1 590.9 ns 8.71 ns 8.14 ns 1.42 2.02 KB 1.04
Moq 1 139,413.6 ns 985.64 ns 921.96 ns 335.84 14.74 KB 7.64
NSubstitute 1 5,978.5 ns 18.09 ns 16.92 ns 14.40 9.06 KB 4.70
FakeItEasy 1 5,939.3 ns 38.22 ns 35.75 ns 14.31 8.06 KB 4.18
baseline* 10 711.0 ns 13.55 ns 12.67 ns 0.83 2.25 KB 1.05
Mockolate 10 857.0 ns 5.86 ns 5.48 ns 1.00 2.14 KB 1.00
Imposter 10 1,249.3 ns 26.26 ns 24.56 ns 1.46 5.52 KB 2.58
TUnitMocks 10 1,777.4 ns 10.98 ns 8.57 ns 2.07 3.73 KB 1.74
Moq 10 144,360.9 ns 531.88 ns 444.14 ns 168.46 18.64 KB 8.71
NSubstitute 10 8,630.9 ns 20.15 ns 18.85 ns 10.07 11.52 KB 5.38
FakeItEasy 10 9,351.9 ns 43.37 ns 38.44 ns 10.91 15.42 KB 7.20
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.74GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

CreateMock Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 67.59 ns 3.276 ns 3.064 ns 3.66 440 B 2.75
Mockolate 18.49 ns 0.444 ns 0.371 ns 1.00 160 B 1.00
Imposter 297.18 ns 6.758 ns 6.321 ns 16.08 2248 B 14.05
TUnitMocks 37.94 ns 0.815 ns 0.763 ns 2.05 200 B 1.25
Moq 1,468.68 ns 3.935 ns 3.286 ns 79.48 2096 B 13.10
NSubstitute 2,109.76 ns 19.724 ns 17.485 ns 114.17 5048 B 31.55
FakeItEasy 2,010.09 ns 49.593 ns 46.389 ns 108.78 2763 B 17.27

baseline* rows show the corresponding Mockolate benchmark from the most recent successful main branch build with results, for regression comparison.

_setupsByMemberIdLock is only taken by the setup-registration paths
(AppendTo*/PublishProperty*), so a mock that is only created — and never has a
setup registered — never needs it. Allocate it lazily like the other deferred
collaborators. This removes the last eager allocation from empty-mock creation,
which now allocates just two heap objects (the mock and its MockRegistry):
~160 B / ~14 ns.
@vbreuss vbreuss force-pushed the perf/lazy-mock-creation branch from fd49edf to 2e5fbdc Compare June 20, 2026 15:26
@sonarqubecloud

Copy link
Copy Markdown

@vbreuss vbreuss merged commit 7f1ccd5 into main Jun 21, 2026
17 checks passed
@vbreuss vbreuss deleted the perf/lazy-mock-creation branch June 21, 2026 05:26
github-actions Bot added a commit that referenced this pull request Jun 21, 2026
github-actions Bot added a commit that referenced this pull request Jun 21, 2026
@github-actions

Copy link
Copy Markdown

This is addressed in release v3.3.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant