You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}

基准测试结果:

MethodMeanErrorStdDevAllocated
ComparingGenericArgument1254.2 ms3.72 ms3.29 ms1804 B
ComparingGenericArgument2458.0 ms5.04 ms4.71 ms480 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 10:05:24