C#中嵌套泛型类型调用性能更差的原因是什么?
问题重现
测试代码如下:
[Benchmark] public int ComparingGenericArgument1() { var comparer = new Wrap<int>(Comparer<int>.Default); return CompareSum<Wrap<int>>.Calc(10000, comparer); } [Benchmark] public int ComparingGenericArgument2() { var comparer = new Wrap<int, Comparer<int>>(Comparer<int>.Default); return CompareSum<Wrap<int, Comparer<int>>>.Calc(10000, comparer); } static class CompareSum<TComparer> where TComparer : struct, IComparer<int> { public static int Calc(int count, TComparer comparer) { int sum = 0; for (int i = 0; i < count; ++i) for (int j = 0; j < count; ++j) sum += comparer.Compare(i, j); return sum; } } struct Wrap<T> : IComparer<T> { IComparer<T> comparer; public Wrap(IComparer<T> comparer) { this.comparer = comparer; } public int Compare(T x, T y) { return comparer.Compare(x, y); } } struct Wrap<T, TComparer> : IComparer<T> where TComparer : IComparer<T> { TComparer comparer; public Wrap(TComparer comparer) { this.comparer = comparer; } public int Compare(T x, T y) { return comparer.Compare(x, y); } }
基准测试结果:
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| ComparingGenericArgument1 | 254.2 ms | 3.72 ms | 3.29 ms | 1804 B |
| ComparingGenericArgument2 | 458.0 ms | 5.04 ms | 4.71 ms | 480 B |
简化后的汇编对比:
; ComparingGenericArgument1 对应的 Calc 方法汇编 C+CompareSum`1[[C+Wrap`1[[System.Int32, System.Private.CoreLib]], _]].Calc(Int32, Wrap`1<Int32>) L0000: push ebp L0001: mov ebp, esp L0003: push ecx L0004: mov ecx, [ebp+8] L0007: xor edx, edx L0009: call dword ptr [0x10d87008] L000f: pop ebp L0010: ret 4 ; ComparingGenericArgument2 对应的 Calc 方法汇编 C+CompareSum`1[[C+Wrap`2[[System.Int32, System.Private.CoreLib],[System.__Canon, System.Private.CoreLib]], _]].Calc(Int32, Wrap`2<Int32,System.__Canon>) L0000: push ebp L0001: mov ebp, esp L0003: push esi L0004: push eax L0005: mov [ebp-8], edx L0008: mov esi, ecx L000a: mov ecx, [edx+0x20] L000d: mov ecx, [ecx] L000f: mov eax, [ecx+8] L0012: test eax, eax L0014: je short L0018 L0016: jmp short L0024 L0018: mov ecx, edx L001a: mov edx, 0x10d8d530 L001f: call 0x0f429aa0 L0024: push esi L0025: lea ecx, [ebp+8] L0028: xor edx, edx L002a: call eax L002c: pop ecx L002d: pop esi L002e: pop ebp L002f: ret 4
核心原因分析
1. JIT去虚拟化与内联能力的差异
ComparingGenericArgument1路径:Wrap<int>的Compare方法调用的是IComparer<int>接口方法,但JIT编译CompareSum<Wrap<int>>.Calc时,能追踪到comparer字段的实际类型是密封类Comparer<int>,因此执行去虚拟化优化,直接将comparer.Compare替换为Comparer<int>.Compare的直接调用,甚至进一步内联整个方法。汇编中直接调用固定地址的方法就是明证,循环内的调用几乎无额外开销。ComparingGenericArgument2路径:Wrap<int, Comparer<int>>的Compare方法调用泛型参数TComparer的Compare方法,但CompareSum<TComparer>的泛型约束仅限定TComparer为struct, IComparer<int>。JIT无法穿透嵌套泛型层级,确定Wrap<int, Comparer<int>>内部comparer字段的具体类型,只能将其视为IComparer<int>接口处理。这导致每次Compare调用都需要通过接口虚表查找方法地址,无法内联,虚调用的开销在1亿次循环(10000×10000)中被大幅放大,最终耗时翻倍。
2. 泛型参数的具体化限制
汇编中出现的System.__Canon是.NET中泛型接口的“规范化类型”,意味着JIT无法为该泛型参数生成具体类型的优化代码,只能使用通用的接口调用逻辑。而第一个路径中Wrap<int>的类型明确,JIT可以生成针对具体类型的优化代码,避免了虚调用的额外步骤。
3. 直接调用与泛型嵌套的差异
直接调用Wrap<T>或Wrap<T, TComparer>时,JIT能获取完整类型信息,可做去虚拟化优化;但通过CompareSum<TComparer>这个泛型包装类时,嵌套泛型的类型信息被泛型约束屏蔽,JIT无法进行跨层级类型推断,导致优化失效。
内容的提问来源于stack exchange,提问作者cathei

