【问题标题】:Why does enumerating an empty array does not allocate on the heap?为什么枚举一个空数组不会在堆上分配?
【发布时间】:2020-10-26 10:47:01
【问题描述】:

考虑以下基准:

[MemoryDiagnoser]
public class EnumerableBenchmark
{
    private IEnumerable<string> _emptyArray = new string[0];
    private IEnumerable<string> _notEmptyArray = new string[1];

    [Benchmark]
    public IEnumerator<string> ArrayEmpty()
    {
        return _emptyArray.GetEnumerator();
    }

    [Benchmark]
    public IEnumerator<string> ArrayNotEmpty()
    {
        return _notEmptyArray.GetEnumerator();
    }
}

BenchmarkDotNet 在 .net framework 4.8 和 .net core 3.1 上报告以下结果:

// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.329 (2004/?/20H1)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.301
  [Host]     : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
  DefaultJob : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT


|        Method |     Mean |     Error |    StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------- |---------:|----------:|----------:|-------:|------:|------:|----------:|
|    ArrayEmpty | 3.692 ns | 0.1044 ns | 0.0872 ns |      - |     - |     - |         - |
| ArrayNotEmpty | 7.235 ns | 0.2177 ns | 0.3051 ns | 0.0051 |     - |     - |      32 B |

从结果来看,GetEnumerator 似乎在数组不为空时会导致堆分配,但在数组为空时不会。我用许多不同的方式重写了基准,但总是得到相同的结果,所以我认为 BenchmarkDotNet 没有错。

我的逻辑结论是空数组有一个缓存的枚举器。但是,这段代码似乎与该理论相矛盾:

var emptyArray = new string[0];

var enum1 = emptyArray.GetEnumerator();
var enum2 = emptyArray.GetEnumerator();

Console.WriteLine("Equals: " + object.ReferenceEquals(enum1, enum2));
Console.WriteLine(enum1.GetType().Name + " - " + enum1.GetType().IsValueType);

其中显示:

Equals: False
SZArrayEnumerator - False

我真的在这个问题上摸不着头脑。有人知道发生了什么吗?

【问题讨论】:

  • 也许 JIT 设法内联了它?
  • 可能是 JIT 编译器将其优化为 Array.Empty&lt;string&gt;()
  • 事情是这样的:当您在第二个测试中调用emptyArray.GetEnumerator() 时,您调用的是非泛型实现。请改用((IEnumerable&lt;string&gt;) emptyArray).GetEnumerator()。 (通用实现确实为空数组的枚举器使用缓存。)
  • @JeroenMostert 你是对的。在这种情况下,我们改为使用这个代码路径:referencesource.microsoft.com/#mscorlib/system/array.cs,2745,它返回一个缓存的枚举器。打得好
  • @JeroenMostert 啊,是的,ReferenceEquals 通过了——你应该把它作为答案发布

标签: c# arrays enumerator


【解决方案1】:

你的假设是正确的。 在提供的基准测试中,使用了枚举器的缓存版本。这是反编译的代码:

internal IEnumerator<T> GetEnumerator<T>()
{
  T[] array = Unsafe.As<T[]>((object) this);
  return array.Length != 0
      ? (IEnumerator<T>) new SZGenericArrayEnumerator<T>(array)
      : (IEnumerator<T>) SZGenericArrayEnumerator<T>.Empty;
}

但是,当您尝试检查您的假设时,您更改了代码。在基准测试中,_emptyArrayIEnumerable&lt;string&gt;,但在代码 sn-p 中,它是 string[]。这是string[].GetEnumerator的反编译代码:

public IEnumerator GetEnumerator()
{
  int lowerBound = this.GetLowerBound(0);
  return this.Rank == 1 && lowerBound == 0
      ? (IEnumerator) new SZArrayEnumerator(this)
      : (IEnumerator) new ArrayEnumerator(this, lowerBound, this.Length);
}

让我们尝试更改 sn-p 并将数组转换为IEnumerable&lt;string&gt;

IEnumerable<string> emptyArray = new string[0];

var enum1 = emptyArray.GetEnumerator();
var enum2 = emptyArray.GetEnumerator();

Console.WriteLine("Equals: " + object.ReferenceEquals(enum1, enum2));
Console.WriteLine(enum1.GetType().Name + " - " + enum1.GetType().IsValueType);

以下是正确验证缓存枚举器假设的更新输出:

Equals: True
SZGenericArrayEnumerator`1 - False

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-02
    • 1970-01-01
    相关资源
    最近更新 更多