perf: replace Activator.CreateInstance with direct constructor in ApiResponse.Create#2071
Merged
ChrisPulman merged 2 commits intoreactiveui:mainfrom Mar 26, 2026
Conversation
added 2 commits
March 27, 2026 01:25
Add net9.0 alongside net8.0 as a benchmark target framework to enable performance measurement on the latest .NET runtime.
…Response.Create Replace the reflection-based `Activator.CreateInstance(typeof(ApiResponse<TBody>), ...)` with a direct `new ApiResponse<TBody>(...)` constructor call, and change the `content` parameter type from `object?` to `TBody?` to eliminate boxing for value types. This removes per-call reflection overhead on every ApiResponse<T> creation, which is the second most common return type pattern in Refit usage (found in 76+ public repos). Benchmark results (BenchmarkDotNet v0.14.0, .NET 9.0.14, AMD Ryzen 7 7700): | Scenario | Before (μs) | After (μs) | Δ Latency | Before (KB) | After (KB) | Δ Alloc | |-----------------------|-------------|------------|-----------|-------------|------------|---------| | OK/Get/STJ | 6.137 | 5.949 | -3.1% | 9.84 | 9.52 | -3.3% | | OK/Get/Newtonsoft | 9.105 | 8.986 | -1.3% | 19.52 | 19.20 | -1.6% | | OK/Post/STJ | 6.732 | 6.669 | -0.9% | 10.89 | 10.56 | -3.0% | | OK/Post/Newtonsoft | 14.204 | 13.814 | -2.7% | 32.03 | 31.72 | -1.0% | | Error/Get/STJ | 2.634 | 2.350 | -10.8% | 12.66 | 12.33 | -2.6% | | Error/Get/Newtonsoft | 2.549 | 2.358 | -7.5% | 12.66 | 12.33 | -2.6% | | Error/Post/STJ | 3.273 | 3.111 | -4.9% | 13.70 | 13.38 | -2.3% | | Error/Post/Newtonsoft | 7.561 | 7.362 | -2.6% | 25.16 | 24.84 | -1.3% | Average: -3.1% latency, -1.9% allocations across all scenarios. Error path sees the largest gains (-10.8%) since the reflection overhead is a larger fraction of total execution time.
a6c0161 to
8d089b1
Compare
ChrisPulman
approved these changes
Mar 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replace the reflection-based
Activator.CreateInstance(typeof(ApiResponse<TBody>), ...)with a directnew ApiResponse<TBody>(...)constructor call inApiResponse.Create<T, TBody>(), and change thecontentparameter type fromobject?toTBody?to eliminate boxing for value types.This is a small, focused change that removes per-call reflection overhead on every
ApiResponse<T>creation.Why ApiResponse?
GitHub code search shows
ApiResponse<T>is the second most common return type in Refit interfaces (after plainTask<T>):Task<ApiResponse<T>>found in 76+ unique repos (search cap hit at 100 file matches)Task<IApiResponse<T>>found in 47+ unique reposThe typical Refit user profile (from GitHub search):
AddRefitClient<T>()with DI (97 repos)Task<T>andTask<ApiResponse<T>>What Changed
Refit/ApiResponse.cs— 2 changes in theCreatemethod:Activator.CreateInstance(typeof(ApiResponse<TBody>), ...)→new ApiResponse<TBody>(...)object? content→TBody? contentRefit/RequestBuilderImplementation.cs— 1 change at the call site:null→default(required becauseTBody?for unconstrained generics doesn't acceptnullliteral for value types)Benchmark Results
BenchmarkDotNet v0.14.0, .NET 9.0.14, AMD Ryzen 7 7700 (8C/16T)
Baseline (before)
After optimization
Per-scenario improvement
The error path sees the largest gains (-10.8%) since the reflection overhead is a larger fraction of total execution time on shorter code paths.
Consistent ~328 bytes/call allocation reduction across all scenarios from eliminating the boxing of
TBodytoobject.Commits
bench: add net9.0 target to Refit.Benchmarks— Adds net9.0 alongside net8.0 as a benchmark targetperf: replace Activator.CreateInstance with direct constructor— The optimization itself + call sitenull→defaultfixTest results
All 501 tests in Refit.Tests and API approval tests pass on net9.0 with this change.