C# 泛型方法的奇怪内联行为 - 可能的错误
C# Weird inline behavior for generic methods - possible bug
出于某些奇怪的原因,此泛型方法不会内联到另一个方法中,除非另一个方法包含循环。什么可以解释这种奇怪的行为?对于非泛型方法,内联在两种情况下都会发生,有循环和没有循环。
代码:
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
要点:
- 奇怪的是,它不会在
C.M1()
中内联方法调用,即使它使生成的 JIT 大小更小 - 我也用不同的方法进行了测试。
- 只有当方法是泛型时才会发生这种奇怪的行为,它总是内联一个直接的非泛型实现。
- 这与泛型方法中的类型转换有关。如果泛型方法不包含这些类型开关,那么它在两种情况下(
M1
和 M2
)都会被内联,即使没有 AggressiveInlining
属性,只要方法很短。
- 循环启动了一些试探法,导致内联发生。
从这个例子中产生的问题是:
- 这种行为是故意的还是错误?
- 有没有办法保证
Less()
方法的内联,而不在调用方方法中使用奇怪的循环?
- 此行为是否也发生在
System.Numerics.Vector<T>
class 中,因为它使用相同的通用类型开关,但已被优化掉?
鉴于此问题已在 .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 - 未内联
所以,回答你的问题:
- 是的,这可能是一个错误。
- 您不能保证内联。尤其不适用于
<T>
类型。 rules/heuristics 可能相当复杂。
- 答案可见线索here。 Microsoft 非常了解 JIT,因此如果像
Vector<T>
这样的高性能 class 会遇到内联问题,那将有些令人惊讶。
出于某些奇怪的原因,此泛型方法不会内联到另一个方法中,除非另一个方法包含循环。什么可以解释这种奇怪的行为?对于非泛型方法,内联在两种情况下都会发生,有循环和没有循环。
代码:
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
要点:
- 奇怪的是,它不会在
C.M1()
中内联方法调用,即使它使生成的 JIT 大小更小 - 我也用不同的方法进行了测试。 - 只有当方法是泛型时才会发生这种奇怪的行为,它总是内联一个直接的非泛型实现。
- 这与泛型方法中的类型转换有关。如果泛型方法不包含这些类型开关,那么它在两种情况下(
M1
和M2
)都会被内联,即使没有AggressiveInlining
属性,只要方法很短。 - 循环启动了一些试探法,导致内联发生。
从这个例子中产生的问题是:
- 这种行为是故意的还是错误?
- 有没有办法保证
Less()
方法的内联,而不在调用方方法中使用奇怪的循环? - 此行为是否也发生在
System.Numerics.Vector<T>
class 中,因为它使用相同的通用类型开关,但已被优化掉?
鉴于此问题已在 .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 - 未内联
所以,回答你的问题:
- 是的,这可能是一个错误。
- 您不能保证内联。尤其不适用于
<T>
类型。 rules/heuristics 可能相当复杂。 - 答案可见线索here。 Microsoft 非常了解 JIT,因此如果像
Vector<T>
这样的高性能 class 会遇到内联问题,那将有些令人惊讶。