在C#中如何高效返回空集合
前言
在项目中,在列表返回数据,有时会遇到返回长度为0的集合,这里说说两个集合Array和List.如何高效的返回空集合呢?- 长度为0数组(Array),使用new int[0]{},还是Array.Empty<int>() ?
- 长度为0的List,是使用new List<int>(0),还是Enumerable.Empty<int>() ?
至于用哪个方式,等下用代码进行基准测试后.在具体分析一下.
测试代码
[MemoryDiagnoser]
[DisassemblyDiagnoser(printSource: true)]
public class EmptyCollectionTest
{
[Params(10000)]
public int Count { get; set; }
[Benchmark]
public int ArrayEmpty1()
{
int sum = 0;
for (int i = 0; i < Count; i++)
{
var arr = new int[0]; //这是重点
if (arr.Length == 0)
{
sum += 1;
}
}
return sum;
}
[Benchmark]
public int ArrayEmpty2()
{
int sum = 0;
for (int i = 0; i < Count; i++)
{
var arr = Array.Empty<int>(); //这是重点
if (arr.Length == 0)
{
sum += 1;
}
}
return sum;
}
[Benchmark]
public int ListEmpty1()
{
int sum = 0;
for (int i = 0; i < Count; i++)
{
var list = new List<int>(0); //这是重点
if (list.Count == 0)
{
sum += 1;
}
}
return sum;
}
[Benchmark]
public int ListEmpty2()
{
int sum = 0;
for (int i = 0; i < Count; i++)
{
var list = Enumerable.Empty<int>(); //这是重点
if (list.Any())
{
sum += 1;
}
}
return sum;
}
}
代码比较简单,看看测试结果:
根据测试结果,先说空数组.new int[0]和Array.Empty<int>(),从耗时上看相差将近14倍,从GC上看new int[0]在0代回收了76.4多次,从汇编代码大小来看比Array.Empty<int>()多2倍,当然在.Net 8中对new int[0]也进行了优化. new[0]和Array.Empty<int>()生成一样的汇编代码.不过还是建议使用Array.Empty<int>().
在说空List,new List<int>(0)和Enumerable.Empty<int>(), 在.Net 7中从耗时来看使用new List<int>(0),要好一些,不过综合来看使用Enumerable.Empty<int>()更好一些,除非在后续.Net版本JIT对new List<int>(0)进行了特殊优化.
源码阅读
Array.Empty源码:private static class EmptyArray<T>
{
#pragma warning disable CA1825 // this is the implementation of Array.Empty<T>()
internal static readonly T[] Value = new T[0];
#pragma warning restore CA1825
}
public static T[] Empty<T>()
{
return EmptyArray<T>.Value;
}
Enumerable.Empty源码:
public static readonly IPartition<TElement> Instance = new EmptyPartition<TElement>();
public static IEnumerable<TResult> Empty<TResult>()
{
return EmptyPartition<TResult>.Instance;
}
这两个Empty方法在BCL库中,都是单例的.
通过汇编代码分析
;CSharpBenchmarks.ArrayTest.EmptyCollectionTest.ArrayEmpty1()
; int sum = 0;
; ^^^^^^^^^^^^
; for (int i = 0; i < Count; i++)
; ^^^^^^^^^
; var arr = new int[0];
; ^^^^^^^^^^^^^^^^^^^^^
; sum += 1;
; ^^^^^^^^^
; return sum;
; ^^^^^^^^^^^
push rdi
push rsi
push rbp
push rbx
sub rsp,28
xor esi,esi
xor edi,edi
mov ebx,[rcx+8]
test ebx,ebx
jle short M00_L02
mov rbp,offset MT_System.Int32[]
M00_L00:
mov rcx,rbp
xor edx,edx
call CORINFO_HELP_NEWARR_1_VC ;;;;进行内存分配(数组)
cmp dword ptr [rax+8],0
jne short M00_L01
inc esi
M00_L01:
inc edi
cmp edi,ebx
jl short M00_L00
M00_L02:
mov eax,esi
add rsp,28
pop rbx
pop rbp
pop rsi
pop rdi
ret
; Total bytes of code 64
new int[0]在.Net 7中为什么比Array.Empty<int>()慢,是因为每次调用的时候在内存上分配一个长度为0的数组,这也就是0代GC被触发了70多次的原因.
; new int[0]在.Net 8.0和Array.Empty<int>(0)生成一样的汇编代码
; CSharpBenchmarks.ArrayTest.EmptyCollectionTest.ArrayEmpty2()
; int sum = 0;
; ^^^^^^^^^^^^
; for (int i = 0; i < Count; i++)
; ^^^^^^^^^
; var arr = Array.Empty<int>();
; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
; sum += 1;
; ^^^^^^^^^
; return sum;
; ^^^^^^^^^^^
xor eax,eax
xor edx,edx
mov ecx,[rcx+8]
test ecx,ecx
jle short M00_L01
M00_L00:
inc eax
inc edx
cmp edx,ecx
jl short M00_L00
M00_L01:
ret
; Total bytes of code 20
接下来说一下new List<int>(0),但因为生成的汇编代码太长,这里就不贴出具体代码了.
;; .Net 7和.Net 8在都有内存分配的操作
mov rbp,offset MT_System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib]]
mov rcx,1B749806DF8
mov r14,[rcx]
M00_L00:
mov rcx,rbp
call CORINFO_HELP_NEWSFAST ;;内存分配
mov r15,rax
lea rcx,[r15+8]
mov rdx,r14
call CORINFO_HELP_ASSIGN_REF
再说一下Enumerable.Empty:
[Benchmark]
public int ListEmpty2()
{
int sum = 0;
for (int i = 0; i < Count; i++)
{
var list = Enumerable.Empty<int>(); //这是重点
if (list.Any()) //.Net 7对Any并没有优化,这可能造成Any生成的汇编代码(328 byte)
{
sum += 1;
}
}
return sum;
}
看看new List<int>(0)在.Net 7生成的汇编代码:
; CSharpBenchmarks.ArrayTest.EmptyCollectionTest.ListEmpty1()
; int sum = 0;
; ^^^^^^^^^^^^
; for (int i = 0; i < Count; i++)
; ^^^^^^^^^
; var list = new List<int>(0);
; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
; if (list.Count == 0)
; ^^^^^^^^^^^^^^^^^^^^
; sum += 1;
; ^^^^^^^^^
; return sum;
; ^^^^^^^^^^^
push r15
push r14
push rdi
push rsi
push rbp
push rbx
sub rsp,28
xor esi,esi
xor edi,edi
mov ebx,[rcx+8]
test ebx,ebx
jle short M00_L02
mov rbp,offset MT_System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib]]
mov rcx,1B749806DF8
mov r14,[rcx]
M00_L00:
mov rcx,rbp
call CORINFO_HELP_NEWSFAST ;;有内存分配
mov r15,rax
lea rcx,[r15+8]
mov rdx,r14
call CORINFO_HELP_ASSIGN_REF
cmp dword ptr [r15+10],0
jne short M00_L01
inc esi
M00_L01:
inc edi
cmp edi,ebx
jl short M00_L00
M00_L02:
mov eax,esi
add rsp,28
pop rbx
pop rbp
pop rsi
pop rdi
pop r14
pop r15
ret
; Total bytes of code 99
在.Net 8下生成的汇编代码:
; CSharpBenchmarks.ArrayTest.EmptyCollectionTest.ListEmpty2()
; int sum = 0;
; ^^^^^^^^^^^^
; for (int i = 0; i < Count; i++)
; ^^^^^^^^^
; var list = Enumerable.Empty<int>();
; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
; sum += 1;
; ^^^^^^^^^
; return sum;
; ^^^^^^^^^^^
push rdi
push rsi
push rbx
sub rsp,20
mov rsi,rcx
xor edi,edi
xor ebx,ebx
cmp dword ptr [rsi+8],0
jle short M00_L03
M00_L00:
mov rax,158D7801EB8
mov rcx,[rax]
jmp short M00_L04
M00_L01:
test eax,eax
jne short M00_L02
call qword ptr [7FF82E99D008] ;;通过源码发现这里是个单例
test eax,eax
je short M00_L02
inc edi
M00_L02:
inc ebx
cmp ebx,[rsi+8]
jl short M00_L00
M00_L03:
mov eax,edi
add rsp,20
pop rbx
pop rsi
pop rdi
ret
M00_L04:
mov eax,1
jmp short M00_L01
; Total bytes of code 75
秋风
2023-08-01