C#.First()vs [0]

时间:2015-11-05 12:14:32

标签: c# performance linq collections

有兴趣,方法有什么不同。
所以,我创建了两个片段。

Snippet A 
List<int> a = new List<int>();
a.Add(4);
a.Add(6);
int b = a.First(); 

Snippet B 
List<int> a = new List<int>();
a.Add(4);
a.Add(6);
int b = a[0]; 

我们相信IL,所以

Snippet A IL
IL_0000:  nop         
IL_0001:  newobj      System.Collections.Generic.List<System.Int32>..ctor
IL_0006:  stloc.0     // a
IL_0007:  ldloc.0     // a
IL_0008:  ldc.i4.4    
IL_0009:  callvirt    System.Collections.Generic.List<System.Int32>.Add
IL_000E:  nop         
IL_000F:  ldloc.0     // a
IL_0010:  ldc.i4.6    
IL_0011:  callvirt    System.Collections.Generic.List<System.Int32>.Add
IL_0016:  nop         
IL_0017:  ldloc.0     // a
IL_0018:  call        System.Linq.Enumerable.First
IL_001D:  stloc.1     // b
IL_001E:  ret        

Snippet B IL
IL_0000:  nop         
IL_0001:  newobj      System.Collections.Generic.List<System.Int32>..ctor
IL_0006:  stloc.0     // a
IL_0007:  ldloc.0     // a
IL_0008:  ldc.i4.4    
IL_0009:  callvirt    System.Collections.Generic.List<System.Int32>.Add
IL_000E:  nop         
IL_000F:  ldloc.0     // a
IL_0010:  ldc.i4.6    
IL_0011:  callvirt    System.Collections.Generic.List<System.Int32>.Add
IL_0016:  nop         
IL_0017:  ldloc.0     // a
IL_0018:  ldc.i4.0    
IL_0019:  callvirt    System.Collections.Generic.List<System.Int32>.get_Item
IL_001E:  stloc.1     // b
IL_001F:  ret  

Snippet B产生了一个更多的IL命令,但最终哪个方法更快?

5 个答案:

答案 0 :(得分:6)

你可以自己检查一下:

    static void Main()
    {
        List<long> resultsFirst = new List<long>();
        List<long> resultsIndex = new List<long>();

        Stopwatch s = new Stopwatch();

        for (int z = 0; z < 100; z++)
        {
            List<int>[] lists = new List<int>[10000];

            int temp = 0;

            for (int i = 0; i < lists.Length; i++)
                lists[i] = new List<int>() { 4, 6 };                

            s.Restart();

            for (int i = 0; i < lists.Length; i++)
                temp = lists[i].First();

            s.Stop();

            resultsFirst.Add(s.ElapsedTicks);

            s.Restart();

            for (int i = 0; i < lists.Length; i++)
                temp = lists[i][0];

            s.Stop();

            resultsIndex.Add(s.ElapsedTicks);
        }

        Console.WriteLine("LINQ First()  :   " + resultsFirst.Average());
        Console.WriteLine(Environment.NewLine);
        Console.WriteLine("By index      :   " + resultsIndex.Average());

        Console.ReadKey();
    }

发布模式下的输出:

LINQ First():367

按索引:84

调试模式下的输出:

LINQ First():401

按索引:177

P.S。

方法First的源代码是:

public static TSource First<TSource>(this IEnumerable<TSource> source)
{
    IList<TSource> list = source as IList<TSource>;
    if (list != null)
    {
        if (list.Count > 0)
        {
            return list[0];
        }
    }
    else
    {
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                return enumerator.Current;
            }
        }
    }
}

转换操作source as IList<TSource>或创建Enumerator对象很可能是First()速度相当慢的原因。

答案 1 :(得分:4)

Enumerable.First方法定义为

public static TSource First<TSource>(this IEnumerable<TSource> source) 
{
    if (source == null) throw Error.ArgumentNull("source");
    IList<TSource> list = source as IList<TSource>;
    if (list != null) {
        if (list.Count > 0) return list[0];
    }
    else {
        using (IEnumerator<TSource> e = source.GetEnumerator()) {
            if (e.MoveNext()) return e.Current;
        }
    }
    throw Error.NoElements();
}

因此对于List<T>,它在空检查和强制转换后最终使用索引器。看起来并不多,但是当我测试性能时,First比索引器(for循环,10 000 000次迭代,释放构建:第一次 - 100 ms,索引器 - 10 ms)慢10倍。< / p>

答案 2 :(得分:2)

一般来说,具体类/接口方法应该优于泛型实现,因为,后者是泛型和数据结构应该考虑其细节。例如,链表不应该提供索引器,因为它无法有效实现。理想情况下,每个数据结构将定义一个自己的方法,它具有与相应的通用扩展方法相同的签名,因为它可以提供更好的实现,编译器将正确处理它。这可以被视为 specialization ,遗憾的是,在C ++模板中不能很好地支持它。 Enumerable.First的实现是“解决方法”而不是解决方案的一个很好的例子 - 它针对特定的BCL接口进行优化,但是无法处理可以提供相同信息的自定义数据结构(如链接列表)比使用通用实现更好。对于Enumerable.Last来说,情况更糟。

要恢复,如果您针对特定的类/接口进行编程,请尽可能使用他们的方法。如果您是针对标准通用接口进行编程,那么,您无其他选择(除了定义影响标准接口的扩展方法,但这通常会导致冲突)。

答案 3 :(得分:0)

如果您要询问渐近复杂度,两种方法都是O(1),请使用其中任何一种。

如果您要询问真正的速度,那么没有答案,因为它可能因版本而异,从一台机器到另一台机器。您生成的IL与任何其他版本的.NET不同。

通过选择其中一种方法来尝试优化代码显然是过早的优化。

答案 4 :(得分:0)

使用以下代码在LINQPad 5中测试:

var sw = Stopwatch.StartNew();
for(int i = 0; i < 1000000000; i++)
{
  List<int> a = new List<int>();
  a.Add(i);
  a.Add(i+2);
  int b = a.First();//[0] for B
}
sw.Stop();
Console.WriteLine(sw.ElapsedTicks); 

.First()给出了01:04.021和0:45.794的优化。 正如我所想,[0]为我提供了优化和更好的代码0:44.288,0:27.968。

实际上,对我而言[0].First()更具可读性,而且通常我不需要他提供的支票。 因此,在大多数情况下,我会选择[0]。 感谢。