【问题标题】:is it better performance wise to use the concrete type rather than the interface使用具体类型而不是接口是否更好的性能?
【发布时间】:2011-05-14 12:05:41
【问题描述】:

我遇到了一些规则(建议)来使用具体的ListDictionary 而不是IListIDictionary,因为示例测试显示通过接口访问要慢很多。例如,将 10000 个值添加到列表中,然后在列表中执行 Count 10 亿次,这表明通过接口执行此操作比通过具体类执行此操作要慢 28 倍。即,通过具体类需要 80 毫秒,通过接口需要 2800 毫秒,这表明通过接口的速度有多慢。鉴于此,使用具体类是否合理。界面这么慢是有原因的吗? (可能更针对那些更了解 .net 内部结构的人)。

【问题讨论】:

  • 很少会看到 100 倍的性能差异。我怀疑您的性能测量的正确性。你能告诉我们你的代码吗?
  • 为什么要在代码中间强制进行 GC 收集?没有任何东西可以收集,也不能保证在那个时候运行它可能会破坏你的基准。
  • @user455095:GC.Collect() 可能会触发一些后台活动。
  • @Henk Holterman: GC.Collect() 触发后台活动:终结器线程。
  • 这里的问题不是间接接口方法调用这么慢,而是Count属性非常非常快。您可以通过执行 10 亿次来使任何快的东西变慢。

标签: c# .net performance


【解决方案1】:

我觉得如果你看反汇编就很明显了:

IList 版本编译为:

            for (int i = 0; i < 1000000000; i++) 
0000003d  xor         edi,edi 
            { 
                count = lst.Count; 
0000003f  mov         ecx,esi 
00000041  call        dword ptr ds:[00280024h] 
00000047  mov         ebx,eax 
            for (int i = 0; i < 1000000000; i++) 
00000049  inc         edi 
0000004a  cmp         edi,3B9ACA00h 
00000050  jl          0000003F 
            }

IList.Count的访问被编译成call指令。

另一方面,List 版本是内联的:

            for (int i = 0; i < 1000000000; i++) 
0000003a  xor         edx,edx 
0000003c  mov         eax,dword ptr [esi+0Ch] 
0000003f  mov         esi,eax 
00000041  inc         edx 
00000042  cmp         edx,3B9ACA00h 
00000048  jl          0000003F 
            }

这里没有call 指令。循环中只有一条 mov、inc、cmp 和 jl 指令。当然这样更快。

但请记住:通常,您是在列表内容的事情,而不仅仅是对其进行迭代。这通常会比单个函数调用花费更长的时间,因此调用接口方法很少会导致任何性能问题。

【讨论】:

  • 谢谢,这有助于解释性能差异无论如何,你在循环中确实需要更多时间,但我想这更像是尝试优化循环中的每一行代码以使其如果性能非常重要,则尽可能快。
  • @Steven:实际上,它是汇编,而不是 IL。不过还是谢谢 ;-)
  • 我当然是这个意思。 ;-) IL 并没有真正解释任何事情。我们需要看看组装。
【解决方案2】:

使用接口的主要原因是灵活性、关注点分离等。

所以我仍然建议在大多数情况下使用接口(不是全部,只在适当的时候使用),并且只在出现真正的性能问题时才考虑切换到具体的类。

并且,在没有GC.Collect() 的情况下运行基准测试并在发布模式下我得到 96 和 560 毫秒。差很多。

【讨论】:

    【解决方案3】:

    您的性能测试显然是错误的。你正在测试很多不同的东西。首先GC.Collect 确实触发了终结器线程,这会影响在它之后运行的所有内容的性能。接下来,您不仅要测试调用接口方法所需的时间,而且还要花费大量时间将数据复制到新数组(因为您正在创建大型数组)并收集它们!- 所以接口调用之间的区别并且实例调用将完全丢失。

    这是我测试接口调用的原始性能开销的测试。在 Visual Studio 之外以发布模式运行时:

    public interface IMyInterface
    {
        void InterfaceMethod();
    }
    
    public class MyClass : IMyInterface
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        public void InterfaceMethod()
        {
        }
    
        [MethodImpl(MethodImplOptions.NoInlining)]
        public void InstanceMethod()
        {
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // JITting everyting:
            MyClass c = new MyClass();
            c.InstanceMethod();
            c.InterfaceMethod();
            TestInterface(c, 1);
            TestConcrete(c, 1);
            Stopwatch watch = Stopwatch.StartNew();
            watch.Start();
            var x = watch.ElapsedMilliseconds;
    
            // Starting tests:
            watch = Stopwatch.StartNew();
    
            TestInterface(c, Int32.MaxValue - 2);
    
            var ms = watch.ElapsedMilliseconds;
    
            Console.WriteLine("Interface: " + ms);
    
            watch = Stopwatch.StartNew();
    
            TestConcrete(c, Int32.MaxValue - 2);
    
            ms = watch.ElapsedMilliseconds;
    
            Console.WriteLine("Concrete: " + ms);
        }
    
        static void TestInterface(IMyInterface iface, int iterations)
        {
            for (int i = 0; i < iterations; i++)
            {
                iface.InterfaceMethod();
            }
        }
    
        static void TestConcrete(MyClass c, int iterations)
        {
            for (int i = 0; i < iterations; i++)
            {
                c.InstanceMethod();
            }
        }
    }
    

    输出:

    Interface: 4861
    Concrete: 4236
    

    【讨论】:

    • 这只是用户进行的更正:1198075 被拒绝 - 在// Starting tests 行之后代码调用TestInterface 两次并且不调用TestConcrete
    • 为什么编辑被拒绝了?此答案包含错误并且具有误导性。如果将 TestInterface 与 TestConcrete(而不是自身)进行比较,则结果为: Interface: 5791 Concrete: 4470
    • @JohanNilsson 和 qujck:测试中确实存在错误。我更正了测试和输出。
    • @JohanNilsson IMO 对 answers 的大部分编辑似乎都被拒绝了 - 我无法解释原因。我注意到这个编辑是正确的,所以我添加了一条评论让史蒂文自己做出改变......
    【解决方案4】:

    这真的是你的应用程序中的一个问题吗,接口可以让你的代码更好,更容易重用/维护

    如果你真的需要提高性能,首先尝试改进算法,例如,你真的需要计算 10 亿次元素吗?您不能将计数存储在某处并有某种标志表明元素已更改并且您需要重新计数吗?

    话虽如此,Performance impact of changing to generic interfaces 的问题涉及接口的性能

    【讨论】:

    • 我们可以,但我想这是一个例子,任何代码都可能最终被执行十亿次(好吧,这听起来有点疯狂,但它是可能的)并且你可以防止这种情况发生说要使用具体的类。我猜在这种情况下,它只适用于列表和字典,而不是每个可能的接口。
    • 我的观点是,通常算法改进会对性能产生巨大影响(删除代码中的 GC.Collect 就是一个很好的例子),使得微优化变得不必要,但是是的,使用具体类似乎比使用接口更快
    【解决方案5】:

    在未优化的 DEBUG 模式下没有明显的性能差异,在使用 .Net 3.5、Visual Stusio 2008 的 RELEASE 模式下提高了大约 35-40%。

    Debug:
     List test,  ms: 1234.375
     IList test, ms: 1218.75
    Release:
     List test,  ms: 609.375
     IList test, ms: 968.75
    

    测试代码:

    List<int> list = new List<int>();
    var start = DateTime.Now;
    for (int i = 0; i < 50000000; i++) list.Add(i);
    for (int i = 0; i < 50000000; i++) list[i] = 0;
    var span = DateTime.Now - start;
    Console.WriteLine("List test,  ms: {0}", span.TotalMilliseconds);
    
    IList<int> ilist = new List<int>();
    start = DateTime.Now;
    for (int i = 0; i < 50000000; i++) ilist.Add(i);
    for (int i = 0; i < 50000000; i++) ilist[i] = 0;
    span = DateTime.Now - start;
    Console.WriteLine("IList test, ms: {0}", span.TotalMilliseconds);
    

    【讨论】:

      【解决方案6】:

      这是我用来查看差异的测试代码:

      using System;
      using System.Collections.Generic;
      using System.Diagnostics;
      
      public class test1
      {
            static void Main(string[] args)
            {
               Stopwatch sw = new Stopwatch();
      
               const int NUM_ITEMS = 10000;
               const int NUM_LOOPS2 = 1000000000;
               List<int> lst = new List<int>(NUM_ITEMS);
               IList<int> ilst = lst;
               for (int i = 0; i < NUM_ITEMS; i++)
               {
                  lst.Add(i);
               }
               int count = 0;
               sw.Reset();
               //GC.Collect();
               sw.Start();
               for (int i = 0; i < NUM_LOOPS2; i++)
               {
                  count = lst.Count;
               }
               sw.Stop();
               Console.Out.WriteLine("Took " + (sw.ElapsedMilliseconds) + "ms - 1.");
      
      
      
               sw.Reset();
               //GC.Collect();
               sw.Start();
               for (int i = 0; i < NUM_LOOPS2; i++)
               {
                  count = ilst.Count;
               }
               sw.Stop();
               Console.Out.WriteLine("Took " + (sw.ElapsedMilliseconds) + "ms - 2.");
      
      
            }
      }
      

      请注意,垃圾回收似乎不会影响此测试。

      【讨论】:

        【解决方案7】:

        IDictionaryDictionary慢的原因here详细解释。

        如果您枚举IDictionary,您确实会产生垃圾,这些垃圾会在某个时候被 GC 收集。这就是您看到性能差异的地方。如果你问我,private members 应该总是用他们的concrete type 声明,至少允许编译器在类中考虑特定类型的优化(如枚举)。

        【讨论】:

          猜你喜欢
          • 2012-11-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-06-11
          • 2023-03-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多