为什么投射List< T>进入IList< T>导致性能下降? [英] Why does casting List<T> into IList<T> result in reduced performance?

查看:145
本文介绍了为什么投射List< T>进入IList< T>导致性能下降?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在做一些性能指标,我遇到了一些似乎对我来说很奇怪的事情。我给出了以下两个函数:

  private static void DoOne()
{
List< int> A =新列表< int>();
for(int i = 0; i <200; i ++)A.Add(i);
int s = 0; (int c = 0; c< A.Count; c ++)s + = A [c]为(int j = 0; j <100000; j ++)
{

) ;
}

}

private static void DoTwo()
{
List< int> A =新列表< int>();
for(int i = 0; i <200; i ++)A.Add(i);
IList< int> L = A;
int s = 0; (int c = 0; c {
;
}

}

即使在释放模式下编译,时间表的结果一致表明,DoTwo花费的时间比DoOne多100%:

  DoOne花了0.06171706秒。 
DoTwo花了8.841709秒。

鉴于List直接实现IList,我对结果非常惊讶。任何人都可以澄清这种行为?



血淋淋的细节



回答问题,这里是完整的代码和一个项目构建偏好的图像:


Dead Image Link




 使用系统; 
使用System.Collections.Generic;
使用System.Text;
使用System.Diagnostics;
使用System.Collections;

命名空间TimingTests
{
类程序
{
static void Main(string [] args)
{
秒表SW =新的秒表();
SW.Start();
DoOne();
SW.Stop();

Console.WriteLine(DoOne花费了{0}秒。,((float)SW.ElapsedTicks)/ Stopwatch.Frequency);
SW.Reset();
SW.Start();
DoTwo();
SW.Stop();

Console.WriteLine(DoTwo花了{0}秒。,((float)SW.ElapsedTicks)/ Stopwatch.Frequency);



private static void DoOne()
{
List< int> A =新列表< int>();
for(int i = 0; i <200; i ++)A.Add(i);
int s = 0; (int c = 0; c< A.Count; c ++)s + = A [c]为(int j = 0; j <100000; j ++)
{

) ;
}

}
private static void DoTwo()
{
List< int> A =新列表< int>();
for(int i = 0; i <200; i ++)A.Add(i);
IList< int> L = A;
int s = 0; (int c = 0; c {
;
}

}
}
}

感谢所有好的回答(特别是@kentaromiura)。我会解决这个问题,尽管我觉得我们仍然错过了这个难题的重要部分。为什么通过它实现的接口访问一个类要慢得多?我可以看到的唯一区别是通过接口访问函数意味着使用虚拟表,而通常的函数可以直接调用。要查看是否是这种情况,我对上述代码做了一些更改。首先我介绍了两个几乎完全相同的类:

  public class VC 
{
virtual public int f() {return 2; }
virtual public int Count {get {return 200; }}


$ b public class C
{
public int f(){return 2; }
public int Count {get {return 200;正如你所看到的,VC正在使用虚拟函数,而且, C没有。现在DoOne和DoTwo:

  private static void DoOne()
{C a = new C();
int s = 0; (int c = 0; c 为(int j = 0; j <100000; j ++)
{



$ b private static void DoTwo()
{
VC a = new VC();
int s = 0; (int c = 0; c 为(int j = 0; j <100000; j ++)
{

}

}

确实:

  DoOne花费了0.01287789秒。 
DoTwo花了8.982396秒。

这更加可怕 - 虚拟函数调用速度减慢800倍?所以对社区的一些问题:


  1. 你能重现吗? (考虑到
    的事实,之前所有的表现都比
    糟糕,但并不像我的那么差)

  2. 你能解释一下吗?

  3. (这可能是最重要的
    ) - 您能想到
    a的方式来避免吗?

Boaz

解决方案

首先我要感谢所有人的回答。在确定发生了什么的道路上,这真的很重要。特别感谢@kentaromiura,它发现了进入事物底部所需的关键。



使用List< T>通过IList< T>接口缺乏JIT编译器内联Item属性获取功能的能力。通过IList接口访问列表引起的虚拟表的使用阻止了这种情况的发生。



作为一个证明,我写了下面的代码:

  public class VC 
{
virtual public int f(){return 2; }
virtual public int Count {get {return 200; }}


$ b public class C
{
// [MethodImpl(MethodImplOptions.NoInlining)]
public int f() {return 2; }
public int Count
{
// [MethodImpl(MethodImplOptions.NoInlining)]
get {return 200; }
}

}

并修改了DoOne和DoTwo如下所示:

  private static void DoOne()
{
C c = new C );
int s = 0; (int i = 0; i< c.Count; i ++)s + = c.f();
为(int j = 0; j <100000; j ++)
{



$ b private static void DoTwo()
{
VC c = new VC();
int s = 0; (int i = 0; i< c.Count; i ++)s + = c.f();
为(int j = 0; j <100000; j ++)
{

}

}

与之前非常相似:

  DoOne花费了0.01273598秒。 
DoTwo花了8.524558秒。

现在,如果您在C类的MethodImpl之前删除注释(强制JIT不内联) - 时间变成:

  DoOne花了8.734635秒。 
DoTwo花了8.887354秒。

瞧 - 这些方法几乎在同一时间。你可以看到这种方法DoOne仍然稍微快一点,这是一个虚拟函数的额外开销。


I was doing some performance metrics and I ran into something that seems quite odd to me. I time the following two functions:

  private static void DoOne()
      {
         List<int> A = new List<int>();
         for (int i = 0; i < 200; i++) A.Add(i);
          int s=0;
         for (int j = 0; j < 100000; j++)
         {
            for (int c = 0; c < A.Count; c++) s += A[c];
         }

      }

   private static void DoTwo()
      {
         List<int> A = new List<int>();
         for (int i = 0; i < 200; i++) A.Add(i);
         IList<int> L = A;
         int s = 0;
         for (int j = 0; j < 100000; j++)
         {
            for (int c = 0; c < L.Count; c++) s += L[c];
         }

      }

Even when compiling in release mode, the timings results were consistently showing that DoTwo takes ~100 longer then DoOne:

 DoOne took 0.06171706 seconds.
 DoTwo took 8.841709 seconds.

Given the fact the List directly implements IList I was very surprised by the results. Can anyone clarify this behavior?

The gory details

Responding to questions, here is the full code and an image of the project build preferences:

Dead Image Link

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Collections;

namespace TimingTests
{
   class Program
   {
      static void Main(string[] args)
      {
         Stopwatch SW = new Stopwatch();
         SW.Start();
         DoOne();
         SW.Stop();

         Console.WriteLine(" DoOne took {0} seconds.", ((float)SW.ElapsedTicks) / Stopwatch.Frequency);
         SW.Reset();
         SW.Start();
         DoTwo();
         SW.Stop();

         Console.WriteLine(" DoTwo took {0} seconds.", ((float)SW.ElapsedTicks) / Stopwatch.Frequency);

      }

      private static void DoOne()
      {
         List<int> A = new List<int>();
         for (int i = 0; i < 200; i++) A.Add(i);
         int s=0;
         for (int j = 0; j < 100000; j++)
         {
            for (int c = 0; c < A.Count; c++) s += A[c];
         }

      }
      private static void DoTwo()
      {
         List<int> A = new List<int>();
         for (int i = 0; i < 200; i++) A.Add(i);
         IList<int> L = A;
         int s = 0;
         for (int j = 0; j < 100000; j++)
         {
            for (int c = 0; c < L.Count; c++) s += L[c];
         }

      }
   }
}

Thanks for all the good answers (especially @kentaromiura). I would have closed the question, though I feel we still miss an important part of the puzzle. Why would accessing a class via an interface it implements be so much slower? The only difference I can see is that accessing a function via an Interface implies using virtual tables while the normally the functions can be called directly. To see whether this is the case I made a couple of changes to the above code. First I introduced two almost identical classes:

  public class VC
  {
     virtual public int f() { return 2; }
     virtual public int Count { get { return 200; } }

  }

  public class C
  {
      public int f() { return 2; }
      public int Count { get { return 200; } }

  }

As you can see VC is using virtual functions and C doesn't. Now to DoOne and DoTwo:

    private static void DoOne()
      {  C a = new C();
         int s=0;
         for (int j = 0; j < 100000; j++)
         {
            for (int c = 0; c < a.Count; c++) s += a.f();
         }

      }
      private static void DoTwo()
      {
           VC a = new VC();
         int s = 0;
         for (int j = 0; j < 100000; j++)
         {
            for (int c = 0; c < a.Count; c++) s +=  a.f();
         }

      }

And indeed:

DoOne took 0.01287789 seconds.
DoTwo took 8.982396 seconds.

This is even more scary - virtual function calls 800 times slower?? so a couple of question to the community:

  1. Can you reproduce? (given the fact that all had worse performance before, but not as bad as mine)
  2. Can you explain?
  3. (this may be the most important) - can you think of a way to avoid?

Boaz

解决方案

First I want to thank all for their answers. It was really essential in the path figuring our what was going on. Special thanks goes to @kentaromiura which found the key needed to get to the bottom of things.

The source of the slow down of using List<T> via an IList<T> interface is the lack of the ability of the JIT complier to inline the Item property get function. The use of virtual tables caused by accessing the list through it's IList interface prevents that from happening.

As a proof , I have written the following code:

      public class VC
      {
         virtual public int f() { return 2; }
         virtual public int Count { get { return 200; } }

      }

      public class C
      {
         //[MethodImpl( MethodImplOptions.NoInlining)]
          public int f() { return 2; }
          public int Count 
          {
            // [MethodImpl(MethodImplOptions.NoInlining)] 
            get { return 200; } 
          }

      }

and modified the DoOne and DoTwo classes to the following:

      private static void DoOne()
      {
         C c = new C();
         int s = 0;
         for (int j = 0; j < 100000; j++)
         {
            for (int i = 0; i < c.Count; i++) s += c.f();
         }

      }
      private static void DoTwo()
      {
         VC c = new VC();
         int s = 0;
         for (int j = 0; j < 100000; j++)
         {
            for (int i = 0; i < c.Count; i++) s += c.f();
         }

      }

Sure enough the function times are now very similar to before:

 DoOne took 0.01273598 seconds.
 DoTwo took 8.524558 seconds.

Now, if you remove the comments before the MethodImpl in the C class (forcing the JIT not to inline) - the timing becomes:

DoOne took 8.734635 seconds.
DoTwo took 8.887354 seconds.

Voila - the methods take almost the same time. You can stil see that method DoOne is still slightly fast , which is consistent which the extra overhead of a virtual function.

这篇关于为什么投射List&lt; T&gt;进入IList&lt; T&gt;导致性能下降?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆