枚举列表比IList,ICollection和IEnumerable更快

Ras*_*ond 4 c# list

最近,我一直在研究一些编写返回集合的函数的约定。我想知道实际上使用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>速度大约是接口的两倍?
  • 如果我们关心性能,我们应该从函数中返回什么以及如何管理代码?

Eri*_*ert 8

为什么处理List<int>速度大约是接口的两倍?

好问题。尝试执行foreach某些操作时,C#首先检查集合的类型是否已经有一个称为的方法GetEnumerator,该方法返回具有MoveNext和的类型Current。如果是这样,它将直接调用那些。如果没有,那么它回退到使用IEnumerable<T>IEnumerableIEnumerator<T>IEnumerator获得枚举,以便它可以调用MoveNextCurrent

做出此设计选择有两个原因。首先,在C#1.0之前的泛型世界中,这意味着您可以调用一个Currentreturn intIEnumerator.Current当然object,框也会如此int,这既是速度又是内存的损失。其次,这意味着集合的作者可以进行实验,以确定哪种实现方式MoveNextCurrent具有最佳性能。

的实现List<T>者正是这样做的;如果仔细检查GetEnumeratorList<T>您会发现一些有趣的东西:它返回可变的值类型。是的,可变值类型被认为是一种容易滥用的不良习惯。但是,由于这种超载的使用的99.999%是由GetEnumerator代表您调用的foreach,绝大多数时间您甚至都没有注意到滥用的可变价值,因此不要滥用它。

(注意:上一段的内容不应该是“使用可变值类型,因为它们很快”。该内容应该理解用户的使用模式,然后设计满足他们需求的安全,有效的工具。通常是可变的值类型不是正确的工具。)

总之,长话短说,我们在迭代编译时已知的东西时,通过直接绑定到可变值类型上的方法来避免各种虚拟调用,接口类型检查等List<T>

如果我们关心性能,我们应该从函数中返回什么以及如何管理代码?

如果您关心速度性能,那么您应该专注于程序中最慢的事情。程序中最慢的事情是调用MoveNext集合吗?如果可以的话,恭喜,您的程序很快。MoveNext是接下来要优化的事情。但是在这种情况下,您实际上应该问“如何避免或完全延迟此循环?” 如果你在那条船上。

如果MoveNext不是程序中最慢的东西,那么谁会在乎某个特定实现的速度是否慢了几纳秒呢?返回逻辑上最接近调用者想要和需要的类型的类型,不必担心微小的损失。