Производительность: тип, полученный из общих

Я столкнулся с одной проблемой производительности, которую я не могу понять. Я знаю, как это исправить, но я не понимаю, почему это происходит. Это просто для удовольствия!
Позвольте говорить о кодах. Я упростил код настолько, насколько смог, чтобы воспроизвести проблему.
Предположим, что мы имеем общий класс. Он содержит пустой список внутри и делает что-то с 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. Выключите "Оптимизировать код" на вкладке "Сборка" и попробуйте снова запустить тест.