Skip to content

Commit 47c5a83

Browse files
authored
perf: comprehensive multi-TFM performance optimization (Phase 1-3) (#400)
* perf: Phase 1 — eliminate hot-path allocations (all TFMs) - SG: cache ShouldIntercept result per-method in byte field (skip repeated ConcurrentDictionary lookups + IsNonAspect reflection) - DP: TryGetValue before GetOrAdd + static lambdas in reflectorTable, CacheAspectValidationHandler, and async expression caches - IoC: TryGetValue fast path for Singleton/Scoped resolution, replace LinkedList<ServiceDefinition> with List<T> for cache locality * perf: Phase 2 — .NET 8+ conditional optimizations - P1: ObjectPool<SourceGeneratedAspectContext> (reuse context instances, eliminate ~120B/call heap allocation on net8.0+) - P3: FrozenDictionary snapshot for CacheAspectValidationHandler after 100-call warmup (30-50% faster lookups on net8.0+) - P4: UnsafeAccessor for interface proxy init-only setters (net8.0+, replaces MethodReflector with ~10ns direct access) All optimizations behind #if NET8_0_OR_GREATER; net6.0 uses fallbacks. * perf: Phase 3 — PoolingAsyncVTMB + generic method caching - P5: SG emits [AsyncMethodBuilder(PoolingAsyncValueTaskMethodBuilder<>)] on ValueTask/ValueTask<T> async helper methods (pools state machines) - Generic method MakeGenericMethod results cached in ConcurrentDictionary with TypeArrayComparer (eliminates 3 MethodInfo + 1 Type[] alloc/call) - Add TypeArrayComparer for structural Type[] equality - Add performance optimization design document * test: add 35 E2E tests to boost coverage above 80% Exercise TypeArrayComparer (generic method cache), CacheAspectValidation Handler (freeze path), ServiceTable/ServiceResolver (keyed services, enumerable, multi-constructor), and validation warmup loop (110+ calls). E2E coverage: 79.28% → 80.32%.
1 parent 69eb544 commit 47c5a83

14 files changed

Lines changed: 1811 additions & 71 deletions

File tree

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# 整体性能优化技术方案
2+
3+
> 版本:2026-07-21
4+
> 状态:待评审
5+
6+
---
7+
8+
## 一、目标
9+
10+
通过利用 .NET 6-10 各版本的新特性,在不改变公共 API 的前提下,系统性消除 DynamicProxy 和 Source Generator 引擎热路径上的分配与开销。采用条件编译实现多 TFM 分层——低版本保持基础实现,高版本自动获得最优性能。
11+
12+
---
13+
14+
## 二、当前热路径分配分析
15+
16+
### 每次拦截调用的分配(Source Generator 引擎)
17+
18+
| 分配点 | 大小 | 可消除? | 所需特性 |
19+
|--------|------|---------|---------|
20+
| `object[N]` 参数数组 | 24 + 8N bytes || 泛型特化 / Span |
21+
| 值类型参数 boxing | 每个 16+ bytes || 泛型管道 |
22+
| `SourceGeneratedAspectContext` 实例 | ~120 bytes || 对象池 |
23+
| `ShouldIntercept` 调用(ConcurrentDict 查找) | 0 alloc 但有 CPU 开销 || 编译时常量 |
24+
| `AspectBuilderFactory.GetKey()` Tuple | 32 bytes | 已消除 | pipeline 缓存 |
25+
| 异步状态机 | ~100-200 bytes | 部分 | PoolingAsyncVTMB |
26+
27+
### DynamicProxy 引擎额外开销
28+
29+
| 分配点 | 来源 | 可优化? |
30+
|--------|------|---------|
31+
| `reflectorTable.GetOrAdd` lambda | 非 static lambda ||
32+
| `AspectBuilderFactory` Tuple key + lambda | 每次调用 ||
33+
| `CacheAspectValidationHandler.GetOrAdd` 闭包 | 每次调用 ||
34+
| `ServiceResolver.GetOrAdd` 闭包 | Singleton/Scoped 解析 ||
35+
36+
---
37+
38+
## 三、优化方案(按优先级排序)
39+
40+
### P0:SG 引擎 — 消除 ShouldIntercept 运行时检查
41+
42+
**问题**:SG 在编译时已经知道哪些方法需要拦截,但生成的代理仍在每次调用时执行 `ShouldIntercept(serviceMethod, implMethod)` — 涉及 2 次 ConcurrentDictionary 查找和 `IsNonAspect` 反射检查。
43+
44+
**方案**:SG 为已确定需要拦截的方法直接生成拦截路径,不生成 `if (!ShouldIntercept(...))` 分支。对非拦截方法根本不 override。
45+
46+
```csharp
47+
// 优化前:每次调用都检查
48+
public override int Add(int a, int b)
49+
{
50+
if (!ShouldIntercept(__Meta.Service_Add, __Meta.Impl_Add))
51+
return base.Add(a, b); // fast path
52+
// ... interception ...
53+
}
54+
55+
// 优化后:编译时已决定拦截,无需运行时检查
56+
public override int Add(int a, int b)
57+
{
58+
// 直接拦截,SG 已知此方法需要拦截
59+
var __args = ...;
60+
// ... interception ...
61+
}
62+
```
63+
64+
**影响**:消除每次调用的 ConcurrentDictionary 查找 + IsNonAspect 反射。
65+
66+
**TFM 要求**:无(纯 SG 改动)。
67+
68+
---
69+
70+
### P1:对象池化 SourceGeneratedAspectContext
71+
72+
**问题**:每次拦截调用创建新的 `SourceGeneratedAspectContext` 实例(~120 bytes 堆分配 + GC 压力)。
73+
74+
**方案**:使用 `ObjectPool<SourceGeneratedAspectContext>` 复用实例。
75+
76+
```csharp
77+
// AspectContextFactory.cs
78+
#if NET8_0_OR_GREATER
79+
private readonly ObjectPool<SourceGeneratedAspectContext> _contextPool =
80+
ObjectPool.Create(new ContextPoolPolicy());
81+
#endif
82+
83+
public AspectContext CreateContext(AspectActivatorContext ctx, IAspectInvokeDelegate del)
84+
{
85+
#if NET8_0_OR_GREATER
86+
var context = _contextPool.Get();
87+
context.Reset(ctx, del); // 重置字段而非新建
88+
return context;
89+
#else
90+
return new SourceGeneratedAspectContext(...);
91+
#endif
92+
}
93+
94+
public void ReleaseContext(AspectContext ctx)
95+
{
96+
#if NET8_0_OR_GREATER
97+
if (ctx is SourceGeneratedAspectContext sgCtx)
98+
{
99+
sgCtx.Clear();
100+
_contextPool.Return(sgCtx);
101+
return;
102+
}
103+
#endif
104+
(ctx as IDisposable)?.Dispose();
105+
}
106+
```
107+
108+
**影响**:消除 ~120 bytes/call 堆分配,显著降低 Gen0 GC。
109+
110+
**TFM 要求**:.NET 8+(`Microsoft.Extensions.ObjectPool`),.NET 6 回退到 new。
111+
112+
---
113+
114+
### P2:消除 DynamicProxy 路径的冗余 lambda 分配
115+
116+
**问题**:4 个热路径 ConcurrentDictionary 调用使用非 static lambda,每次调用分配闭包。
117+
118+
**方案**
119+
120+
```csharp
121+
// 优化前(RuntimeAspectContext.Complete)
122+
var reflector = reflectorTable.GetOrAdd(
123+
_implementationMethod,
124+
method => method.GetReflector(method.IsCallvirt() ? CallOptions.Callvirt : CallOptions.Call));
125+
126+
// 优化后:static lambda + TryGetValue 优先
127+
if (!reflectorTable.TryGetValue(_implementationMethod, out var reflector))
128+
{
129+
reflector = reflectorTable.GetOrAdd(
130+
_implementationMethod,
131+
static method => method.GetReflector(method.IsCallvirt() ? CallOptions.Callvirt : CallOptions.Call));
132+
}
133+
```
134+
135+
同样的模式应用于:
136+
- `AspectBuilderFactory.GetBuilder` 的 lambda
137+
- `CacheAspectValidationHandler.Invoke` 的 lambda
138+
- `ServiceResolver.ResolveDefinition` 的 lambda
139+
140+
**影响**:消除 4 个 lambda 闭包分配/call。
141+
142+
**TFM 要求**:C# 9+(static lambda),所有 TFM 均可用(LangVersion 已是 13.0)。
143+
144+
---
145+
146+
### P3:FrozenDictionary 替换稳定查找表
147+
148+
**问题**:多个 ConcurrentDictionary 在启动后内容不再变化,但每次查找仍有锁竞争开销。
149+
150+
**方案**:启动完成后 freeze:
151+
152+
```csharp
153+
#if NET8_0_OR_GREATER
154+
using System.Collections.Frozen;
155+
#endif
156+
157+
internal class AspectCaching : IAspectCaching
158+
{
159+
#if NET8_0_OR_GREATER
160+
private FrozenDictionary<object, object>? _frozen;
161+
private ConcurrentDictionary<object, object> _dict = new();
162+
163+
public void Freeze()
164+
{
165+
_frozen = _dict.ToFrozenDictionary();
166+
}
167+
168+
public object GetOrAdd(object key, Func<object, object> factory)
169+
{
170+
if (_frozen != null && _frozen.TryGetValue(key, out var val))
171+
return val;
172+
return _dict.GetOrAdd(key, factory);
173+
}
174+
#else
175+
private readonly ConcurrentDictionary<object, object> _dict = new();
176+
public object GetOrAdd(object key, Func<object, object> factory) => _dict.GetOrAdd(key, factory);
177+
#endif
178+
}
179+
```
180+
181+
适用于:
182+
- `AspectCaching`(拦截器管道缓存)
183+
- `reflectorTable`(MethodReflector 缓存)
184+
- `CacheAspectValidationHandler`(验证缓存)
185+
- `ServiceTable`(IoC 服务定义表)
186+
187+
**影响**:查找性能提升 30-50%(FrozenDictionary 使用完美哈希)。
188+
189+
**TFM 要求**:.NET 8+,.NET 6 回退到 ConcurrentDictionary。
190+
191+
---
192+
193+
### P4:UnsafeAccessor 替代 MethodReflector(部分场景)
194+
195+
**问题**`MethodReflector` 使用 `DynamicMethod` + IL emit 构建调用委托,初始化重(微秒级),且不兼容 NativeAOT。
196+
197+
**方案**:对 SG 已知的目标方法,生成 `[UnsafeAccessor]` 静态方法作为 NativeAOT 安全的替代:
198+
199+
```csharp
200+
// SG 生成(.NET 8+)
201+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Name")]
202+
private static extern void SetName_Accessor(InitOnlyService target, string value);
203+
204+
// 在 delegate 中使用
205+
public object Invoke(object instance, object[] parameters)
206+
{
207+
SetName_Accessor((InitOnlyService)instance, (string)parameters[0]);
208+
return null;
209+
}
210+
```
211+
212+
适用场景:
213+
- init-only property setter(当前用 MethodReflector fallback)
214+
- 私有/internal 成员访问
215+
- 非虚方法调用(替代 CallOptions.Call 的 DynamicMethod)
216+
217+
**影响**:init-only setter 路径从 ~1000ns 降到 ~10ns。NativeAOT 完全安全。
218+
219+
**TFM 要求**:.NET 8+(`[UnsafeAccessor]`),低版本回退到 MethodReflector / MethodInfo.Invoke。
220+
221+
---
222+
223+
### P5:PoolingAsyncValueTaskMethodBuilder
224+
225+
**问题**:每个 async 拦截调用生成一个编译器异步状态机对象(100-200 bytes)。
226+
227+
**方案**:在 SG 生成的异步方法上标注 `[AsyncMethodBuilder]`
228+
229+
```csharp
230+
// SG 生成
231+
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
232+
public async ValueTask<string> GetDataAsync(int id)
233+
{
234+
// ... interception ...
235+
}
236+
```
237+
238+
**限制**:仅适用于返回 `ValueTask<T>` / `ValueTask` 的方法。`Task<T>` 方法不能用此优化。
239+
240+
**影响**:async 路径 GC 压力下降 50-80%。
241+
242+
**TFM 要求**:.NET 6+(`PoolingAsyncValueTaskMethodBuilder` 在 .NET 6 引入)。
243+
244+
---
245+
246+
### P6:IoC 容器优化
247+
248+
**问题**`ServiceResolver``GetOrAdd` 每次分配闭包;`LinkedList` 缓存不友好;枚举服务每次分配数组。
249+
250+
**方案**
251+
252+
```csharp
253+
// 1. TryGetValue before GetOrAdd(所有 TFM)
254+
if (!_resolvedSingletonServices.TryGetValue(definition, out var service))
255+
{
256+
service = _resolvedSingletonServices.GetOrAdd(definition,
257+
static (def, resolver) => resolver._serviceCallSiteResolver.Resolve(def)(resolver),
258+
this);
259+
}
260+
261+
// 2. Replace LinkedList with array(所有 TFM)
262+
private readonly Dictionary<Type, ServiceDefinition[]> _services;
263+
264+
// 3. Pre-compute enumerables(所有 TFM)
265+
private readonly Dictionary<Type, object[]> _enumerableCache;
266+
```
267+
268+
**影响**:IoC 解析路径 -30% 延迟,-60% 分配。
269+
270+
**TFM 要求**:基础优化所有 TFM 可用;`FrozenDictionary` 需 .NET 8+。
271+
272+
---
273+
274+
## 四、TFM 分层矩阵
275+
276+
| 优化 | net6.0 | net8.0 | net9.0 | net10.0 |
277+
|------|:---:|:---:|:---:|:---:|
278+
| P0: SG 消除 ShouldIntercept |||||
279+
| P1: Context 对象池 | ❌ (new) | ✅ ObjectPool |||
280+
| P2: static lambda |||||
281+
| P3: FrozenDictionary | ❌ (ConcurrentDict) ||||
282+
| P4: UnsafeAccessor | ❌ (Reflector) ||||
283+
| P5: PoolingAsyncVTMB |||||
284+
| P6: IoC 优化 | ✅ (部分) | ✅ (全部) |||
285+
286+
---
287+
288+
## 五、预期收益
289+
290+
| 引擎/场景 | 当前 | 优化后(预估) | 提升 |
291+
|-----------|------|--------------|------|
292+
| SG sync invoke (pre-resolved) | 205 ns / 448 B | ~120 ns / ~80 B | 40% faster, 80% less alloc |
293+
| SG async Task<T> | 1843 ns / 879 B | ~1200 ns / ~400 B | 35% faster, 55% less alloc |
294+
| DP sync invoke | 221 ns / 360 B | ~180 ns / ~200 B | 20% faster, 45% less alloc |
295+
| IoC Singleton resolve | 28 ns / 32 B | ~15 ns / 0 B | 45% faster, zero-alloc |
296+
297+
---
298+
299+
## 六、实施分阶段
300+
301+
### Phase 1(低风险,全 TFM)
302+
- P0: SG 消除 ShouldIntercept
303+
- P2: static lambda 全量替换
304+
- P6: IoC TryGetValue + LinkedList→Array
305+
306+
### Phase 2(.NET 8+ 条件编译)
307+
- P1: Context 对象池
308+
- P3: FrozenDictionary
309+
- P4: UnsafeAccessor(SG init-only setter)
310+
311+
### Phase 3(SG 生成代码优化)
312+
- P5: PoolingAsyncValueTaskMethodBuilder
313+
- 泛型方法 MakeGenericMethod 缓存
314+
315+
---
316+
317+
## 七、风险评估
318+
319+
| 风险 | 等级 | 缓解 |
320+
|------|------|------|
321+
| 对象池化后的 context 状态泄漏 || Clear() 方法强制重置所有字段;UT 验证 |
322+
| FrozenDictionary freeze 时机不当 || 第一次拦截调用后 freeze;或手动 API |
323+
| UnsafeAccessor 访问权限限制 || 仅用于 SG 已知的具体类型;fallback 到 Reflector |
324+
| PoolingAsyncVTMB 改变异常语义 || 仅用于 ValueTask 返回方法;Task 方法不变 |
325+
| ShouldIntercept 去掉后动态配置失效 || 保留运行时 validator 作为可选 fallback |
326+
327+
---
328+
329+
*本方案待评审后分阶段实施。*

src/AspectCore.Core/AspectCore.Core.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
2020
</ItemGroup>
2121

22+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net9.0' Or '$(TargetFramework)' == 'net10.0'">
23+
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.0" />
24+
</ItemGroup>
25+
2226
<ItemGroup>
2327
<ProjectReference Include="..\AspectCore.Abstractions\AspectCore.Abstractions.csproj" />
2428
<ProjectReference Include="..\AspectCore.Extensions.Reflection\AspectCore.Extensions.Reflection.csproj" />

src/AspectCore.Core/DependencyInjection/ServiceCallSiteResolver.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public ServiceCallSiteResolver(ServiceTable serviceTable)
2222

2323
internal Func<ServiceResolver, object> Resolve(ServiceDefinition service)
2424
{
25+
if (_resolvedCallSites.TryGetValue(service, out var callSite))
26+
{
27+
return callSite;
28+
}
2529
return _resolvedCallSites.GetOrAdd(service, ResolveCallback);
2630
}
2731

src/AspectCore.Core/DependencyInjection/ServiceResolver.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,17 @@ internal object ResolveDefinition(ServiceDefinition definition)
6464
switch (definition.Lifetime)
6565
{
6666
case Lifetime.Singleton:
67-
return _resolvedSingletonServices.GetOrAdd(definition, d => _serviceCallSiteResolver.Resolve(d)(_root ?? this));
67+
if (_resolvedSingletonServices.TryGetValue(definition, out var singleton))
68+
{
69+
return singleton;
70+
}
71+
return _resolvedSingletonServices.GetOrAdd(definition, static (d, state) => state.callSiteResolver.Resolve(d)(state.resolver), (callSiteResolver: _serviceCallSiteResolver, resolver: _root ?? this));
6872
case Lifetime.Scoped:
69-
return _resolvedScopedServices.GetOrAdd(definition, d => _serviceCallSiteResolver.Resolve(d)(this));
73+
if (_resolvedScopedServices.TryGetValue(definition, out var scoped))
74+
{
75+
return scoped;
76+
}
77+
return _resolvedScopedServices.GetOrAdd(definition, static (d, state) => state.callSiteResolver.Resolve(d)(state.resolver), (callSiteResolver: _serviceCallSiteResolver, resolver: this));
7078
default:
7179
return _serviceCallSiteResolver.Resolve(definition)(this);
7280
}

0 commit comments

Comments
 (0)