Почему кастинг Перечисляет <T> в IList <T> результат в уменьшенной производительности?

Кто-нибудь знает, как это сделать правильно?

Я отправил вопрос в AspNetCore Github.

Вот ссылка для этого https://github.com/aspnet/AspNetCore/issues/10249

Я думаю, что это на самом деле ошибка (ошибка VS или ошибка dotnet, Я не знаю)

Посмотрим, что будет дальше.

14
задан SuperBiasedMan 17 August 2015 в 16:23
поделиться

7 ответов

Прежде всего хочу поблагодарить всех за ответы. Это было действительно важно на пути к пониманию того, что происходит. Особая благодарность @kentaromiura, которая нашла ключ, необходимый для понимания сути вещей.

Источником замедления использования List через интерфейс IList является отсутствие возможности JIT complier, чтобы встроить функцию get свойства Item. Использование виртуальных таблиц, вызванное доступом к списку через его интерфейс IList, предотвращает это.

В качестве доказательства я написал следующий код:

      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; } 
          }

      }

и изменил классы DoOne и DoTwo следующим образом:

      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();
         }

      }

Конечно, время функции теперь очень похоже на предыдущее:

 DoOne took 0.01273598 seconds.
 DoTwo took 8.524558 seconds.

Теперь, если вы удалите комментарии перед MethodImpl в классе C (заставляя JIT не встраиваться) - время становится следующим:

DoOne took 8.734635 seconds.
DoTwo took 8.887354 seconds.

Вуаля - методы требуют почти одинакового времени. Вы все еще можете видеть, что метод DoOne все еще немного быстр, что соответствует дополнительным накладным расходам виртуальной функции.

8
ответ дан 1 December 2019 в 06:02
поделиться

Примечание для всех, кто пытается тестировать подобные вещи.

Не забывайте, что код не изменяется до первого запуска . Это означает, что при первом запуске метода стоимость выполнения этого метода может определяться временем, затрачиваемым на загрузку IL, анализ IL и преобразование его в машинный код, особенно если это тривиальный метод.

Если вы пытаетесь сравнить «предельные» затраты времени выполнения двух методов, рекомендуется запустить оба из них дважды и рассматривать только вторые прогоны для сравнения.

27
ответ дан 1 December 2019 в 06:02
поделиться

Я вижу некоторые значительные штрафы для версии интерфейса, но совсем не те, которые вы наблюдаете.

Можете ли вы опубликовать небольшую полную программу, которая демонстрирует поведение вместе с тем, как именно вы ее компилируете и какую именно версию фреймворка вы используете?

4
ответ дан 1 December 2019 в 06:02
поделиться

Я запускал это с помощью Помощника по тестированию Jon Skeet , и я не вижу результатов, которые вы видите, время выполнения этих двух методов примерно одинаковое.

2
ответ дан 1 December 2019 в 06:02
поделиться

Профилирование один на один:

Тестирование с помощью компилятора сниппета.

с использованием результатов вашего кода:

0,043 с против 0,116 с

без временного L

0,043 с vs 0.116s - ininfluent

путем кеширования A.count в cmax для обоих методов

0.041s vs 0.076s

     IList<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,cmax=A.Count;c< cmax;  c++) s += A[c];
     }

Теперь я попытаюсь замедлить DoOne, сначала попробую, приведя к IList перед добавлением:

for (int i = 0; i < 200; i++) ((IList<int>)A).Add(i);

0,041 с 0,076 с - добавление не влияет

, поэтому остается только одно место, где может произойти замедление: s + = A [c]; поэтому я пробую следующее:

s += ((IList<int>)A)[c];

0,075 с 0,075 с - TADaaan!

так кажется, что доступ к счетчику или элементу индекса медленнее в интерфейсной версии:

РЕДАКТИРОВАТЬ: Ради интереса взгляните на это:

 for (int c = 0,cmax=A.Count;c< cmax;  c++) s += ((List<int>)A)[c];

0,041 с 0,050 с

так что это не проблема приведения, а проблема отражения!

9
ответ дан 1 December 2019 в 06:02
поделиться

Мои тесты показывают, что версия интерфейса примерно в 3 раза медленнее при компиляции в режиме выпуска. При компиляции в режиме отладки они почти совпадают.

--------------------------------------------------------
 DoOne Release (ms) |  92 |  91 |  91 |  92 |  92 |  92
 DoTwo Release (ms) | 313 | 313 | 316 | 352 | 320 | 318
--------------------------------------------------------
 DoOne Debug (ms)   | 535 | 534 | 548 | 536 | 534 | 537
 DoTwo Debug (ms)   | 566 | 570 | 569 | 565 | 568 | 571
--------------------------------------------------------

EDIT

В своих тестах я использовал слегка измененную версию метода DoTwo , так что он был напрямую сопоставим с ] Doone . (Это изменение не оказало заметного влияния на производительность.)

private static void DoTwo()
{
    IList<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];
    }
}

Единственное различие между IL, созданным для DoOne и (измененным) DoTwo , заключается в том, что Инструкции callvirt для Добавить , get_Item и get_Count используют IList и ICollection вместо Сам Список .

I '

3
ответ дан 1 December 2019 в 06:02
поделиться

I Вы считаете, что проблема заключается в ваших показателях времени, что вы используете для измерения прошедшего времени?

Для записи, вот мои результаты:

DoOne() -> 295 ms
DoTwo() -> 291 ms

И код:

        Stopwatch sw = new Stopwatch();

        sw.Start();
        {
            DoOne();
        }
        sw.Stop();

        Console.WriteLine("DoOne() -> {0} ms", sw.ElapsedMilliseconds);

        sw.Reset();

        sw.Start();
        {
            DoTwo();
        }
        sw.Stop();

        Console.WriteLine("DoTwo() -> {0} ms", sw.ElapsedMilliseconds);
5
ответ дан 1 December 2019 в 06:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: