C# 收益回报性能



当我对方法执行 TOList() 时,使用收益返回语法为方法后面的基础集合保留了多少空间?与创建具有预定义容量的列表的标准方法相比,它有可能重新分配并因此降低性能?

两种方案:

public IEnumerable<T> GetList1()
{
foreach( var item in collection )
yield return item.Property;
}
public IEnumerable<T> GetList2()
{
List<T> outputList = new List<T>( collection.Count() );
foreach( var item in collection )
outputList.Add( item.Property );
return outputList;
}

yield return不会像List那样创建一个必须调整大小的数组;相反,它会创建一个带有状态机的IEnumerable

例如,让我们采用此方法:

public static IEnumerable<int> Foo()
{
Console.WriteLine("Returning 1");
yield return 1;
Console.WriteLine("Returning 2");
yield return 2;
Console.WriteLine("Returning 3");
yield return 3;
}

现在让我们调用它并将该可枚举分配给一个变量:

var elems = Foo();

Foo中的所有代码都尚未执行。控制台上不会打印任何内容。但是如果我们迭代它,就像这样:

foreach(var elem in elems)
{
Console.WriteLine( "Got " + elem );
}

foreach循环的第一次迭代中,Foo方法将执行,直到第一个yield return。然后,在第二次迭代中,该方法将从中断的地方"恢复"(紧接在yield return 1之后),并执行直到下一个yield return。所有后续元素都相同。
在循环结束时,控制台将如下所示:

Returning 1
Got 1
Returning 2
Got 2
Returning 3
Got 3

这意味着您可以编写如下方法:

public static IEnumerable<int> GetAnswers()
{
while( true )
{
yield return 42;
}
}

你可以调用GetAnswers方法,每次你请求一个元素时,它都会给你 42;序列永远不会结束。你不能用List来做到这一点,因为列表必须有一个有限的大小。

使用收益返回语法的方法后面的基础集合保留了多少空间?

没有基础集合。

有一个对象,但它不是一个集合。它将占用多少空间取决于它需要跟踪的内容。

它有可能重新分配

不。

因此,与我创建具有预定义容量的列表的标准方法相比,性能会降低?

与创建具有预定义容量的列表相比,它几乎肯定会占用更少的内存。

让我们尝试一个手动示例。假设我们有以下代码:

public static IEnumerable<int> CountToTen()
{
for(var i = 1; i != 11; ++i)
yield return i;
}

foreach,将遍历数字110包含。

现在,让我们按照yield不存在时必须这样做的方式执行此操作。我们会做这样的事情:

private class CountToTenEnumerator : IEnumerator<int>
{
private int _current;
public int Current
{
get
{
if(_current == 0)
throw new InvalidOperationException();
return _current;
}
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
if(_current == 10)
return false;
_current++;
return true;
}
public void Reset()
{
throw new NotSupportedException();
// We *could* just set _current back, but the object produced by
// yield won't do that, so we'll match that.
}
public void Dispose()
{
}
}
private class CountToTenEnumerable : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
return new CountToTenEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public static IEnumerable<int> CountToTen()
{
return new CountToTenEnumerable();
}

现在,由于各种原因,这与您可能从使用yield的版本获得的代码完全不同,但基本原理是相同的。如您所见,涉及两个对象的分配(与我们拥有集合然后对其进行foreach的数量相同)和单个 int 的存储。实际上,我们可以期望yield存储比这更多的字节,但不是很多。

编辑:yield实际上做了一个技巧,即在获取对象的同一线程上的第一个GetEnumerator()调用返回相同的对象,为这两种情况提供双重服务。由于这涵盖了超过 99% 的用例yield因此实际上执行一个分配而不是两个分配。

现在让我们看看:

public IEnumerable<T> GetList1()
{
foreach( var item in collection )
yield return item.Property;
}

虽然这会导致使用的内存不仅仅是return collection,但它不会产生更多;枚举器生成的唯一真正需要跟踪的是调用GetEnumerator()collection然后包装它生成的枚举器。

这将比你提到的浪费的第二种方法的内存少得多,而且开始的速度要快得多。

编辑:

您已经将问题更改为包含"当我对其执行 ToList() 时语法",这值得考虑。

现在,在这里我们需要添加第三种可能性:了解集合的大小。

在这里,使用new List(capacity)可能会阻止正在构建的列表的分配。这确实可以节省大量资金。

如果ToList调用的对象实现了ICollection<T>那么ToList最终将首先对内部T数组进行单次分配,然后调用ICollection<T>.CopyTo()

这意味着您的GetList2将导致比GetList1更快的ToList()

但是,您的GetList2已经浪费了时间和内存,无论如何ToList()都会对GetList1的结果进行处理!

它应该在这里做的只是return new List<T>(collection);并完成它。

如果我们需要在GetList1GetList2内部实际做一些事情(例如转换元素、过滤元素、跟踪平均值等),那么GetList1内存会更快、更轻。如果我们从不打电话给它ToList(),就会轻得多,如果我们打电话给ToList(),就会稍微轻一点,因为同样,更快、更轻的ToList()GetList2首先是完全相同的更慢和更重的量所抵消。

最新更新