微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

C#通用方法的怪异内联行为-可能的错误

如何解决C#通用方法的怪异内联行为-可能的错误

出于某些奇怪的原因,除非该其他方法包含循环,否则该通用方法将不会在另一个方法中内联。有什么可以解释这种奇怪的行为?对于非泛型方法,无论有无循环,内联都会同时发生。

代码

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,Int32)
    L000a: jmp rax

// Direct Inline
C`1[[system.int32,System.Private.CoreLib]].M2(Int32,Int32) // Direct Inline
    L0000: cmp ecx,al
    L0008: ret

要点:

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

此示例引起的问题是:

  • 此行为是故意的还是错误
  • 是否有一种方法可以保证Less()方法的内联,而无需在调用方法中使用怪异的循环?
  • System.Numerics.Vector<T>类中是否还会发生这种行为,因为它使用了经过优化的相同的通用类型开关?

解决方法

鉴于此问题已在 .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. 您不能保证内联。尤其不适用于 <T> 类型。规则/启发式方法可能非常复杂。
  3. 答案的线索可以在 here 中看到。 Microsoft 非常了解 JIT,因此,如果像 Vector<T> 这样的高性能类会遇到内联问题,那就有点令人惊讶了。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。