Кто-нибудь знает, как это сделать правильно?
Я отправил вопрос в AspNetCore Github.
Вот ссылка для этого https://github.com/aspnet/AspNetCore/issues/10249
Я думаю, что это на самом деле ошибка (ошибка VS или ошибка dotnet, Я не знаю)
Посмотрим, что будет дальше.
Прежде всего хочу поблагодарить всех за ответы. Это было действительно важно на пути к пониманию того, что происходит. Особая благодарность @kentaromiura, которая нашла ключ, необходимый для понимания сути вещей.
Источником замедления использования List
В качестве доказательства я написал следующий код:
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 все еще немного быстр, что соответствует дополнительным накладным расходам виртуальной функции.
Примечание для всех, кто пытается тестировать подобные вещи.
Не забывайте, что код не изменяется до первого запуска . Это означает, что при первом запуске метода стоимость выполнения этого метода может определяться временем, затрачиваемым на загрузку IL, анализ IL и преобразование его в машинный код, особенно если это тривиальный метод.
Если вы пытаетесь сравнить «предельные» затраты времени выполнения двух методов, рекомендуется запустить оба из них дважды и рассматривать только вторые прогоны для сравнения.
Я вижу некоторые значительные штрафы для версии интерфейса, но совсем не те, которые вы наблюдаете.
Можете ли вы опубликовать небольшую полную программу, которая демонстрирует поведение вместе с тем, как именно вы ее компилируете и какую именно версию фреймворка вы используете?
Я запускал это с помощью Помощника по тестированию Jon Skeet , и я не вижу результатов, которые вы видите, время выполнения этих двух методов примерно одинаковое.
Профилирование один на один:
Тестирование с помощью компилятора сниппета.
с использованием результатов вашего кода:
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 с
так что это не проблема приведения, а проблема отражения!
Мои тесты показывают, что версия интерфейса примерно в 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 '
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);