Лучше ли работать с конкретным типом, а не с интерфейсом

Я столкнулся с некоторыми правилами (рекомендациями), чтобы использовать конкретный список и словарь, а не IList и IDictionary, учитывая образцы тестов, которые показывают, что через интерфейс он немного медленнее. Например, добавив 10000 значений в список, а затем сделав подсчет в списке 1 миллиард раз, это означает, что выполнение этого через интерфейс в 28 раз медленнее, чем через конкретный класс. т.е. через конкретный класс он занимает 80 мс, через интерфейс требуется 2800 мс, что показывает, как очень медленно это происходит через интерфейс. Учитывая это, разумно использовать конкретный класс. Есть ли причина, по которой интерфейс настолько медленнее (вероятно, больше направлен на кого-то, кто знает больше о внутренностях .net).

спасибо Скотт

Ответ 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 в цикле. Конечно, это быстрее.

Но помните: обычно вы делаете что-то с содержимым вашего списка, вы не просто перебираете его. Обычно это занимает намного больше времени, чем один вызов функции, поэтому вызовы методов интерфейса редко вызывают проблемы с производительностью.

Ответ 2

Основной причиной использования интерфейсов является гибкость, разделение проблем и т.д.

Поэтому я бы по-прежнему советовал использовать интерфейсы в большинстве случаев (не все, где это необходимо), и только подумайте о переходе на конкретные классы при возникновении реальной проблемы с производительностью.

И после запуска теста без GC.Collect() и в режиме Release я получаю 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

Ответ 4

Это действительно проблема в вашем приложении, интерфейсы могут сделать код лучше и проще для повторного использования/поддержки

Если вам действительно нужно улучшить производительность, попробуйте сначала улучшить алгоритм, например, вам действительно нужно подсчитать количество элементов в 1 миллиард раз? вы не можете хранить счет где-то и иметь какой-то флаг, указывающий, что элементы были изменены, и вам нужно пересчитать его?

При этом вопрос Влияние производительности на общие интерфейсы указывает на производительность интерфейсов

Ответ 5

Не было очевидной разницы в производительности в неоптимизированном режиме DEBUG и примерно на 35-40% в режиме RELEASE с .Net 3.5, Visual Stusio 2008.

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.");


      }
}

Обратите внимание, что сбор мусора, похоже, не влияет на этот тест.