最近,我一直在研究一些编写返回集合的函数的约定。我想知道实际上使用a的函数是否List<int>应该返回a List<int>或宁可IList<int>是ICollection<int>or IEnumerable<int>。我创建了一些性能测试,结果令我非常惊讶。
static List<int> list = MakeList();
static IList<int> iList = MakeList();
static ICollection<int> iCollection = MakeList();
static IEnumerable<int> iEnumerable = MakeList();
public static TimeSpan Measure(Action f)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
f();
stopWatch.Stop();
return stopWatch.Elapsed;
}
public static List<int> MakeList()
{
var list = new List<int>();
for (int i = 0; i < 100; ++i)
{
list.Add(i);
}
return list;
}
public static void Main()
{
var time1 = Measure(() => { // Measure time of enumerating List<int>
for (int i = 1000000; i > 0; i-- ) {
foreach (var item in list)
{
var x = item;
}
}
});
Console.WriteLine($"List<int> time: {time1}");
var time2 = Measure(() => { // IList<int>
for (int i = 1000000; i > 0; i-- ) {
foreach (var item in iList)
{
var x = item;
}
}
});
Console.WriteLine($"IList<int> time: {time2}");
var time3 = Measure(() => { // ICollection<int>
for (int i = 1000000; i > 0; i-- ) {
foreach (var item in iCollection)
{
var x = item;
}
}
});
Console.WriteLine($"ICollection<int> time: {time3}");
var time4 = Measure(() => { // IEnumerable<int>
for (int i = 1000000; i > 0; i-- ) {
foreach (var item in iEnumerable)
{
var x = item;
}
}
});
Console.WriteLine($"IEnumerable<int> time: {time4}");
}
Run Code Online (Sandbox Code Playgroud)
输出:
List<int> time: 00:00:00.7976577
IList<int> time: 00:00:01.5599382
ICollection<int> time: 00:00:01.7323919
IEnumerable<int> time: 00:00:01.6075277
Run Code Online (Sandbox Code Playgroud)
我尝试了不同顺序的测量或使MakeList()上述接口之一返回,但是所有这些仅确认返回a List<int>并将其作为a处理的List<int>速度大约是接口的两倍。
但是,包括此答案在内的各种资料都声称您永远不应该返回List<>并且始终使用接口。
所以我的问题是:
List<int>速度大约是接口的两倍?为什么处理
List<int>速度大约是接口的两倍?
好问题。尝试执行foreach某些操作时,C#首先检查集合的类型是否已经有一个称为的方法GetEnumerator,该方法返回具有MoveNext和的类型Current。如果是这样,它将直接调用那些。如果没有,那么它回退到使用IEnumerable<T>或IEnumerable和IEnumerator<T>或IEnumerator获得枚举,以便它可以调用MoveNext和Current。
做出此设计选择有两个原因。首先,在C#1.0之前的泛型世界中,这意味着您可以调用一个Currentreturn int;IEnumerator.Current当然object,框也会如此int,这既是速度又是内存的损失。其次,这意味着集合的作者可以进行实验,以确定哪种实现方式MoveNext并Current具有最佳性能。
的实现List<T>者正是这样做的;如果仔细检查GetEnumerator,List<T>您会发现一些有趣的东西:它返回可变的值类型。是的,可变值类型被认为是一种容易滥用的不良习惯。但是,由于这种超载的使用的99.999%是由GetEnumerator代表您调用的foreach,绝大多数时间您甚至都没有注意到滥用的可变价值,因此不要滥用它。
(注意:上一段的内容不应该是“使用可变值类型,因为它们很快”。该内容应该理解用户的使用模式,然后设计满足他们需求的安全,有效的工具。通常是可变的值类型不是正确的工具。)
总之,长话短说,我们在迭代编译时已知的东西时,通过直接绑定到可变值类型上的方法来避免各种虚拟调用,接口类型检查等List<T>。
如果我们关心性能,我们应该从函数中返回什么以及如何管理代码?
如果您关心速度性能,那么您应该专注于程序中最慢的事情。程序中最慢的事情是调用MoveNext集合吗?如果可以的话,恭喜,您的程序很快。MoveNext是接下来要优化的事情。但是在这种情况下,您实际上应该问“如何避免或完全延迟此循环?” 如果你在那条船上。
如果MoveNext不是程序中最慢的东西,那么谁会在乎某个特定实现的速度是否慢了几纳秒呢?返回逻辑上最接近调用者想要和需要的类型的类型,不必担心微小的损失。