为什么我的扩展方法重载不是首选?
Why is my Extension Method overload not preferred?
我为 Enum.HasFlag
做了一个防止装箱的通用重载:
public static unsafe bool HasFlag<T>(this T enumVal, T flag) where T : unmanaged, Enum {
return sizeof(T) switch {
1 => (*(byte*)&enumVal & *(byte*)&flag) == *(byte*)&flag,
2 => (*(ushort*)&enumVal & *(ushort*)&flag) == *(ushort*)&flag,
4 => (*(uint*)&enumVal & *(uint*)&flag) == *(uint*)&flag,
8 => (*(ulong*)&enumVal & *(ulong*)&flag) == *(ulong*)&flag,
_ => throw new ArgumentException("Unsupported base enum Type")
};
}
然而编译器仍然想使用默认的 Enum.HasFlag
而我必须明确定义通用类型以强制它使用扩展。
这里应该优先考虑扩展方法,因为参数具有正确的类型,并且与原始方法相比不需要隐式类型转换,为什么编译器仍然使用错误的方法?
有关重载解析过程的详细信息,请参阅§12.6.4 of the specification。在 §12.7.6.1 中流程的一般描述的底部,您可以看到:
Otherwise, an attempt is made to process E.I
as an extension method invocation (§12.7.8.3). If this fails, E.I
is an invalid member reference, and a binding-time error occurs.
如果我们看一下 §12.7.8.3:
if the normal processing of the invocation finds no applicable methods, an attempt is made to process the construct as an extension method invocation
很明显,仅当重载解析过程未能找到适用的实例方法时,才会尝试绑定扩展方法。
这是一个深思熟虑的决定。如果不是这种情况,在文件顶部添加单个 using
语句可能会改变方法在文件中进一步绑定的方式——远距离的诡异动作,规范通常试图避免这种情况。
但是,自 .NET Core 2.1 以来,Enum.HasFlag
一直是 JIT 内在函数(它是引入 JIT 内在函数机制的典型代表)。这意味着虽然 IL 可能会说要装箱并调用 Enum.HasFlag
方法,但实际上 JIT 知道它可以用单个按位测试替换它。
例如代码:
public void Native(StringSplitOptions o) {
if (o.HasFlag(StringSplitOptions.RemoveEmptyEntries))
{
Console.WriteLine("Noo");
}
}
在版本中与此程序集同步:
C.Native(System.StringSplitOptions)
L0000: test dl, 1
L0003: je short L0017
L0005: mov rcx, 0x1ac4adebda0
L000f: mov rcx, [rcx]
L0012: jmp 0x00007ffb2f6ff7f8
L0017: ret
那里没有任何方法调用的迹象(除了最后的 Console.WriteLine
)!
使用您的扩展方法的相同代码明显更差:
public void Worse(StringSplitOptions o) {
if (o.HasFlag<StringSplitOptions>(StringSplitOptions.RemoveEmptyEntries))
{
Console.WriteLine("Noo");
}
}
给出:
C.Worse(System.StringSplitOptions)
L0000: sub rsp, 0x28
L0004: mov [rsp+0x24], edx
L0008: mov dword ptr [rsp+0x20], 1
L0010: mov ecx, [rsp+0x24]
L0014: and ecx, [rsp+0x20]
L0018: cmp ecx, [rsp+0x20]
L001c: sete cl
L001f: movzx ecx, cl
L0022: test ecx, ecx
L0024: je short L0038
L0026: mov rcx, 0x1ac4adebda0
L0030: mov rcx, [rcx]
L0033: call 0x00007ffb2f6ff7f8
L0038: nop
L0039: add rsp, 0x28
L003d: ret
我们可以看到这已经内联了你的HasFlag
方法的内容,即:
Extensions.HasFlag[[System.StringSplitOptions, System.Private.CoreLib]](System.StringSplitOptions, System.StringSplitOptions)
L0000: mov [rsp+8], ecx
L0004: mov [rsp+0x10], edx
L0008: mov eax, [rsp+8]
L000c: and eax, [rsp+0x10]
L0010: cmp eax, [rsp+0x10]
L0014: sete al
L0017: movzx eax, al
L001a: ret
因为你的问题被标记为 [.net-6.0]
,最好的建议是放弃你的扩展方法并使用内置的 Enum.HasFlag
,因为它比你写的要快得多。
我为 Enum.HasFlag
做了一个防止装箱的通用重载:
public static unsafe bool HasFlag<T>(this T enumVal, T flag) where T : unmanaged, Enum {
return sizeof(T) switch {
1 => (*(byte*)&enumVal & *(byte*)&flag) == *(byte*)&flag,
2 => (*(ushort*)&enumVal & *(ushort*)&flag) == *(ushort*)&flag,
4 => (*(uint*)&enumVal & *(uint*)&flag) == *(uint*)&flag,
8 => (*(ulong*)&enumVal & *(ulong*)&flag) == *(ulong*)&flag,
_ => throw new ArgumentException("Unsupported base enum Type")
};
}
然而编译器仍然想使用默认的 Enum.HasFlag
而我必须明确定义通用类型以强制它使用扩展。
这里应该优先考虑扩展方法,因为参数具有正确的类型,并且与原始方法相比不需要隐式类型转换,为什么编译器仍然使用错误的方法?
有关重载解析过程的详细信息,请参阅§12.6.4 of the specification。在 §12.7.6.1 中流程的一般描述的底部,您可以看到:
Otherwise, an attempt is made to process
E.I
as an extension method invocation (§12.7.8.3). If this fails,E.I
is an invalid member reference, and a binding-time error occurs.
如果我们看一下 §12.7.8.3:
if the normal processing of the invocation finds no applicable methods, an attempt is made to process the construct as an extension method invocation
很明显,仅当重载解析过程未能找到适用的实例方法时,才会尝试绑定扩展方法。
这是一个深思熟虑的决定。如果不是这种情况,在文件顶部添加单个 using
语句可能会改变方法在文件中进一步绑定的方式——远距离的诡异动作,规范通常试图避免这种情况。
但是,自 .NET Core 2.1 以来,Enum.HasFlag
一直是 JIT 内在函数(它是引入 JIT 内在函数机制的典型代表)。这意味着虽然 IL 可能会说要装箱并调用 Enum.HasFlag
方法,但实际上 JIT 知道它可以用单个按位测试替换它。
例如代码:
public void Native(StringSplitOptions o) {
if (o.HasFlag(StringSplitOptions.RemoveEmptyEntries))
{
Console.WriteLine("Noo");
}
}
在版本中与此程序集同步:
C.Native(System.StringSplitOptions)
L0000: test dl, 1
L0003: je short L0017
L0005: mov rcx, 0x1ac4adebda0
L000f: mov rcx, [rcx]
L0012: jmp 0x00007ffb2f6ff7f8
L0017: ret
那里没有任何方法调用的迹象(除了最后的 Console.WriteLine
)!
使用您的扩展方法的相同代码明显更差:
public void Worse(StringSplitOptions o) {
if (o.HasFlag<StringSplitOptions>(StringSplitOptions.RemoveEmptyEntries))
{
Console.WriteLine("Noo");
}
}
给出:
C.Worse(System.StringSplitOptions)
L0000: sub rsp, 0x28
L0004: mov [rsp+0x24], edx
L0008: mov dword ptr [rsp+0x20], 1
L0010: mov ecx, [rsp+0x24]
L0014: and ecx, [rsp+0x20]
L0018: cmp ecx, [rsp+0x20]
L001c: sete cl
L001f: movzx ecx, cl
L0022: test ecx, ecx
L0024: je short L0038
L0026: mov rcx, 0x1ac4adebda0
L0030: mov rcx, [rcx]
L0033: call 0x00007ffb2f6ff7f8
L0038: nop
L0039: add rsp, 0x28
L003d: ret
我们可以看到这已经内联了你的HasFlag
方法的内容,即:
Extensions.HasFlag[[System.StringSplitOptions, System.Private.CoreLib]](System.StringSplitOptions, System.StringSplitOptions)
L0000: mov [rsp+8], ecx
L0004: mov [rsp+0x10], edx
L0008: mov eax, [rsp+8]
L000c: and eax, [rsp+0x10]
L0010: cmp eax, [rsp+0x10]
L0014: sete al
L0017: movzx eax, al
L001a: ret
因为你的问题被标记为 [.net-6.0]
,最好的建议是放弃你的扩展方法并使用内置的 Enum.HasFlag
,因为它比你写的要快得多。