Производительность: тип, полученный из общих
Я столкнулся с одной проблемой производительности, которую я не могу понять. Я знаю, как это исправить, но я не понимаю, почему это происходит. Это просто для удовольствия!
Позвольте говорить о кодах. Я упростил код настолько, насколько смог, чтобы воспроизвести проблему.
Предположим, что мы имеем общий класс. Он содержит пустой список внутри и делает что-то с T
в конструкторе. Он имеет метод Run
, который вызывает метод IEnumerable<T>
в списке, например. Any()
.
public class BaseClass<T>
{
private List<T> _list = new List<T>();
public BaseClass()
{
Enumerable.Empty<T>();
// or Enumerable.Repeat(new T(), 10);
// or even new T();
// or foreach (var item in _list) {}
}
public void Run()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
// or if (_list.Count() > 0)
// or if (_list.FirstOrDefault() != null)
// or if (_list.SingleOrDefault() != null)
// or other IEnumerable<T> method
{
return;
}
}
}
}
Тогда мы имеем производный класс, который пуст:
public class DerivedClass : BaseClass<object>
{
}
Пусть измеряется производительность запуска ClassBase<T>.Run
метода из обоих классов. Доступ из производного типа в 4 раза медленнее, чем из базового класса. И я не могу понять, почему это происходит. Скомпилированный в режиме Release, результат будет таким же, как и разогрев. Это происходит только для .NET 4.5.
public class Program
{
public static void Main()
{
Measure(new DerivedClass());
Measure(new BaseClass<object>());
}
private static void Measure(BaseClass<object> baseClass)
{
var sw = Stopwatch.StartNew();
baseClass.Run();
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
}
Полный список по сути
Ответы
Ответ 1
Update:
Там ответ от команды CLR на Microsoft Connect
Он связан с поиском словаря в коде общих дженериков. Эвристика во время выполнения и JIT не работают хорошо для этого конкретного теста. Мы посмотрим, что с этим можно сделать.
Тем временем вы можете обойти это, добавив два базовых метода в BaseClass (даже не нужно их вызывать). Это заставит эвристику работать так, как можно было бы ожидать.
Оригинал:
Ошибка JIT.
Можно исправить эту сумасшедшую вещь:
public class BaseClass<T>
{
private List<T> _list = new List<T>();
public BaseClass()
{
Enumerable.Empty<T>();
// or Enumerable.Repeat(new T(), 10);
// or even new T();
// or foreach (var item in _list) {}
}
public void Run()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
{
return;
}
}
}
public void Run2()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
{
return;
}
}
}
public void Run3()
{
for (var i = 0; i < 8000000; i++)
{
if (_list.Any())
{
return;
}
}
}
}
Обратите внимание, что Run2()/Run3() называются not из любой точки. Но если вы прокомментируете методы Run2 или Run3, вы получите штраф за производительность, как и раньше.
Я думаю, что что-то связано с выравниванием стека или размером таблицы методов.
P.S. Вы можете заменить
Enumerable.Empty<T>();
// with
var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);
все тот же самый баг.
Ответ 2
После некоторых экспериментов я обнаружил, что Enumerable.Empty<T>
всегда медленный, когда T - тип class; если это тип значения, он быстрее, но зависит от размера структуры.
Я тестировал объект, строку, int, PointF, RectangleF, DateTime, Guid.
Посмотрев, как он реализован, я попробовал разные альтернативы и нашел некоторые, которые работают быстро.
Enumerable.Empty<T>
полагается на внутренний класс EmptyEnumerable<TElement>
Instance
static свойство.
Это свойство делает мало вещей:
- Проверяет, является ли приватное статическое volatile поле нулевым.
- Назначает пустой массив в поле один раз (только если пустой).
- Возвращает значение поля.
Тогда то, что действительно делает Enumerable.Empty<T>
, возвращает только пустой массив из T.
Попробовав разные подходы, я обнаружил, что медленность вызвана и свойством и модификатором volatile.
Принятие статического поля, инициализированного T [0] вместо Enumerable.Empty<T>
, как
public static readonly T[] EmptyArray = new T[0];
проблема исчезла.
Обратите внимание, что модификатор readonly не является определяющим.
Наличие такого же статического поля, объявленного с помощью volatile или доступ через свойство , вызывает проблему.
С уважением,
Даниэла.
Ответ 3
Кажется, проблема оптимизатора CLR. Выключите "Оптимизировать код" на вкладке "Сборка" и попробуйте снова запустить тест.