在.Net Framework使用Enum中HasFlag注意事项
起因
最近在项目中看到有大量使用枚举(Enum类型)的HasFlag判断是否存在,在编写<<高性能的.Net代码>>一书中,有提到大量使用HasFlag检测会让CPU偏高,当然HasFlag性能也不太好,这些都是针对HasFlag在.NetFramework中,在.Net Core 3.0以后的版本是不存在该问题的.在.Net Core中对HasFlag进行了优化的.使用BenchmarkDotNet测试HasFlag性能怎么样
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class EnumHasFlagTest
{
//初始化
public CheckInfo checkInfo = new CheckInfo
{
Id = 1,
Name = "tom",
CheckState = CheckState.Checking
};
[Benchmark(Baseline = true)]
public void HasFlag()
{
for (int i = 0; i < 10000; i++)
{
if (checkInfo.CheckState.HasFlag(CheckState.Checking))
{
}
}
}
[Benchmark]
public void EnumEquals()
{
for (int i = 0; i < 10000; i++)
{
if ((checkInfo.CheckState & CheckState.Checking) != 0) //使用这种方式优化
{
}
}
}
private class Config : ManualConfig
{
public Config()
{
SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); //
}
}
}
/// <summary>
/// 检查信息
/// </summary>
public class CheckInfo
{
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 检查状态
/// </summary>
public CheckState CheckState { get; set; }
}
[Flags]
public enum CheckState
{
None = 0,
/// <summary>
/// 待检
/// </summary>
Waiting = 1,
/// <summary>
/// 检查中
/// </summary>
Checking = 2,
/// <summary>
/// 检毕
/// </summary>
Over = 8,
}
为什么要测试10000次的耗时,是因为单次测试,看不到性能差异.
通过上图看到在.NetFramework 4.8中使用HasFlag性能是最不好的,在堆上内存分配是最多的,生成的汇编代码也是最多的.
先看看.Net Framework4.8 HasFlag生成的汇编代码:
; dotnet_perf.EnumHasFlagTest.HasFlag()
push rdi
push rsi
sub rsp,28
xor esi,esi
M00_L00:
mov rcx,offset MT_dotnet_perf.CheckState
call CORINFO_HELP_NEWFAST
mov rdi,rax
mov dword ptr [rdi+8],14
mov rcx,offset MT_dotnet_perf.CheckState
call CORINFO_HELP_NEWFAST
mov rdx,rax
mov dword ptr [rdx+8],0A
mov rcx,rdi
call System.Enum.HasFlag(System.Enum)
inc esi
cmp esi,2710
jl short M00_L00
add rsp,28
pop rsi
pop rdi
ret
; Total bytes of code 83
; System.Enum.HasFlag(System.Enum)
push rdi
push rsi
push rbx
sub rsp,20
mov rdi,rcx
mov rsi,rdx
test rsi,rsi
je near ptr M01_L00
mov rcx,rdi
call qword ptr [6FD1]
mov rbx,rax
mov rcx,rsi
call qword ptr [6FC5]
mov rdx,rax
mov rcx,rbx
mov rax,[rbx]
mov rax,[rax+0E0]
call qword ptr [rax+8]
test al,al
je near ptr M01_L01
mov rcx,rdi
mov rdx,rsi
mov rax,[2B15]
add rsp,20
pop rbx
pop rsi
pop rdi
jmp rax
M01_L00:
lea rcx,[147D]
call qword ptr [6137]
mov rsi,rax
mov ecx,645E
call 00007FF9A480A776
mov rdx,rax
mov rcx,rsi
call qword ptr [9516]
mov rcx,rsi
call 00007FF9A480A76A
M01_L01:
lea rcx,[0FEA9]
mov edx,2
call qword ptr [60D4]
mov rbx,rax
mov rcx,rsi
call qword ptr [26B8]
mov r8,rax
mov rcx,rbx
xor edx,edx
call qword ptr [60DA]
mov rcx,rdi
call qword ptr [26A1]
mov r8,rax
mov rcx,rbx
mov edx,1
call qword ptr [60C0]
lea rcx,[1F89]
call qword ptr [60C3]
mov rsi,rax
mov ecx,6468
call 00007FF9A480A776
mov rcx,rax
mov rdx,rbx
nop
call System.Environment.GetResourceString(System.String, System.Object[])
mov rdx,rax
mov rcx,rsi
nop
call System.ArgumentException..ctor(System.String)
mov rcx,rsi
call 00007FF9A480A76A
int 3
; Total bytes of code 268
可以看到HasFlag生成的汇编代码比较多,内部比较多的调用.
在看优化生成的汇编代码:
; dotnet_perf.EnumHasFlagTest.EnumEquals()
xor eax,eax
M00_L00:
inc eax
cmp eax,2710
jl short M00_L00
ret
; Total bytes of code 12
在看看.Net 3.1生成的汇编代码, .Net 5.0和.Net 6.0生成的汇编代码是一样的.
HasFlag源码
.Net Framework 4.8源码地址: https://referencesource.microsoft.com/#mscorlib/system/enum.cs,9cd73f33d2df3074两个版本的源码内部都是调用 InternalHasFlag,这个方式是一个外部方法(不是c#实现),好在.Net Core和Mono代码都是开源的.
我们去看看InternalHasFlag在Mono和.Net Core是如何对HasFlag优化的.
先看Mono代码实现,在src/mono/mono/metadata/icall.c文件中.
MonoBoolean
ves_icall_System_Enum_InternalHasFlag (MonoObjectHandle a, MonoObjectHandle b, MonoError *error)
{
int size = mono_class_value_size (mono_handle_class (a), NULL);
guint64 a_val = 0, b_val = 0; //guint64对应c语言的unsigned long 对应c#的ulong类型
//分别对a和b进行拆箱,将内容拷入a_val和b_val中
memcpy (&a_val, mono_handle_unbox_unsafe (a), size);
memcpy (&b_val, mono_handle_unbox_unsafe (b), size);
//a_val和v_val进行位与运算,只有两个值相同才会返回不为0的值,在b_val进行相等比较
return (a_val & b_val) == b_val;
}
.Net Core实现代码,在src/coreclr/vm/reflectioninvocation.cpp
// perform (this & flags) == flags
FCIMPL2(FC_BOOL_RET, ReflectionEnum::InternalHasFlag, Object *pRefThis, Object* pRefFlags)
{
FCALL_CONTRACT;
VALIDATEOBJECT(pRefThis);
BOOL cmp = false;
_ASSERTE(pRefFlags != NULL); // Enum.cs would have thrown ArgumentNullException before calling into InternalHasFlag
VALIDATEOBJECT(pRefFlags);
//进行拆箱
void * pThis = pRefThis->UnBox();
void * pFlags = pRefFlags->UnBox();
MethodTable* pMTThis = pRefThis->GetMethodTable();
_ASSERTE(!pMTThis->IsArray()); // bunch of assumptions about arrays wrong.
_ASSERTE(pMTThis->GetNumInstanceFieldBytes() == pRefFlags->GetMethodTable()->GetNumInstanceFieldBytes()); // Enum.cs verifies that the types are Equivalent
//通过指针指向,进行两个值直接进行对比
switch (pMTThis->GetNumInstanceFieldBytes()) {
case 1:
cmp = ((*(UINT8*)pThis & *(UINT8*)pFlags) == *(UINT8*)pFlags);
break;
case 2:
cmp = ((*(UINT16*)pThis & *(UINT16*)pFlags) == *(UINT16*)pFlags);
break;
case 4:
cmp = ((*(UINT32*)pThis & *(UINT32*)pFlags) == *(UINT32*)pFlags);
break;
case 8:
cmp = ((*(UINT64*)pThis & *(UINT64*)pFlags) == *(UINT64*)pFlags);
break;
default:
// should not reach here.
UNREACHABLE_MSG("Incorrect Enum Type size!");
break;
}
FC_RETURN_BOOL(cmp);
}
FCIMPLEND
Mono的源码看起来更容易理解,两者都是通过拆箱之后,进行位与算,在判断是否相等
秋风
2021-06-05