【问题标题】:C# Weird inline behavior for generic methods - possible bug泛型方法的 C# 奇怪的内联行为 - 可能的错误
【发布时间】:2021-01-25 01:40:43
【问题描述】:

出于某种奇怪的原因,这个泛型方法不会内联到另一个方法中,除非另一个方法包含循环。什么可以解释这种奇怪的行为?对于非泛型方法,内联在两种情况下都会发生,包括循环和不循环。

代码:

using System;
using System.Runtime.CompilerServices;
using SharpLab.Runtime;

[JitGeneric(typeof(int))]
public static class GenericOps<T> where T : unmanaged
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Less(T left, T right)
    {
        if (typeof(T) == typeof(byte)) return (byte)(object)left < (byte)(object)right;
        if (typeof(T) == typeof(sbyte)) return (sbyte)(object)left < (sbyte)(object)right;
        if (typeof(T) == typeof(ushort)) return (ushort)(object)left < (ushort)(object)right;
        if (typeof(T) == typeof(short)) return (short)(object)left < (short)(object)right;
        if (typeof(T) == typeof(uint)) return (uint)(object)left < (uint)(object)right;
        if (typeof(T) == typeof(int)) return (int)(object)left < (int)(object)right;
        if (typeof(T) == typeof(ulong)) return (ulong)(object)left < (ulong)(object)right;
        if (typeof(T) == typeof(long)) return (long)(object)left < (long)(object)right;
        if (typeof(T) == typeof(float)) return (float)(object)left < (float)(object)right;
        if (typeof(T) == typeof(double)) return (double)(object)left < (double)(object)right;
        return default;
    }
}

[JitGeneric(typeof(int))]
public static class C<T> where T : unmanaged
{      
    public static bool M1(T a, T b)
    {
        return GenericOps<T>.Less(a, b);      
    }
        
    public static bool M2(T a, T b)
    {
        for(int i = 0; i<0; i++) {}
            
        return GenericOps<T>.Less(a, b);    
    }        
}

JIT:(使用 SharpLab 反编译)

// All the type checks are omitted since the type is known during compile time
// This generated JIT equals to a direct int < int JIT.
GenericOps`1[[System.Int32, System.Private.CoreLib]].Less(Int32, Int32)
    L0000: cmp ecx, edx
    L0002: setl al
    L0005: movzx eax, al
    L0008: ret

// No Inlining
C`1[[System.Int32, System.Private.CoreLib]].M1(Int32, Int32)
    L0000: mov rax, GenericOps`1[[System.Int32, System.Private.CoreLib]].Less(Int32, Int32)
    L000a: jmp rax

// Direct Inline
C`1[[System.Int32, System.Private.CoreLib]].M2(Int32, Int32) // Direct Inline
    L0000: cmp ecx, edx
    L0002: setl al
    L0005: movzx eax, al
    L0008: ret

要点:

  • 奇怪的是,即使它使生成的 JIT 大小更小,它也不会内联 C.M1() 中的方法调用 - 我也使用不同的方法进行了测试。
  • 这种奇怪的行为只有在方法是泛型时才会发生,它总是内联直接的非泛型实现。
  • 这与泛型方法中的类型切换有关。如果泛型方法不包含这些类型开关,那么它会在两种情况下(M1M2)都被内联,即使没有 AggressiveInlining 属性,只要方法很短。
  • 循环启动了一些启发式方法,导致内联发生。

这个例子提出的问题是:

  • 这种行为是故意的还是错误?
  • 有没有办法保证Less() 方法的内联,而不在调用者方法中使用奇怪的循环?
  • 这种行为是否也发生在 System.Numerics.Vector&lt;T&gt; 类中,因为它使用相同的泛型类型开关,而这些开关已被优化掉?

【问题讨论】:

标签: c# performance jit inlining system.numerics


【解决方案1】:

鉴于这已在 .NET 5 中修复,我将其称为错误。已在SharpLab 中使用以下.NET 版本进行验证:

  • x64 (.NET 5) - 内联
  • x86 上的核心 CLR v5.0.321.7212 - 内联
  • x86/amd64 上的桌面 CLR v4.8.4261.00 - 未内联
  • x86 上的核心 CLR v4.700.20.20201 - 未内联
  • x86 上的核心 CLR v4.700.19.46205 - 未内联

所以,回答你的问题:

  1. 是的,这很可能是一个错误。
  2. 您不能保证内联。尤其不适用于&lt;T&gt; 类型。规则/启发式可能相当复杂。
  3. 答案的线索可见here。 Microsoft 非常了解 JIT,因此如果像 Vector&lt;T&gt; 这样的高性能类会遇到内联问题,那就有点令人惊讶了。

【讨论】:

    猜你喜欢
    • 2021-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-07
    • 1970-01-01
    • 2017-08-08
    相关资源
    最近更新 更多