在C#中如何高效返回空集合

前言

在项目中,在列表返回数据,有时会遇到返回长度为0的集合,这里说说两个集合Array和List.如何高效的返回空集合呢? 
  1. 长度为0数组(Array),使用new int[0]{},还是Array.Empty<int>() ?
  2. 长度为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