Производительность: тип, производный от универсального


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

полный список по сути

3 59

3 ответа:

обновление:
Есть ответ от команды CLR на Microsoft Connect

это связано с поиском словаря в общем коде generics. Эвристика во время выполнения и 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 () являются не любой из них. Но если вы закомментируете методы Run2 или Run3 - вы получите штраф за производительность, как и раньше.

есть что-то, связанное с выравниванием стека или размером таблицы методов, я думаю.

P. S. Вы можете заменить

 Enumerable.Empty<T>();
 // with
 var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);

все та же ошибка.

после некоторых экспериментов, я обнаружил, что Enumerable.Empty<T> всегда медленно, когда T класс тип; если это тип значения, он быстрее, но зависит от размера структуры. Я тестировал object, string, int, PointF, RectangleF, DateTime, Guid.

глядя на то, как это реализовано, я пробовал разные альтернативы, и нашел некоторые, которые работают быстро.

Enumerable.Empty<T> полагается на внутренний класс EmptyEnumerable<TElement> ' s Instance статический свойства.

это свойство делает мелочах:

  • проверяет, если частный статический!--8-->летучие поле имеет значение null.
  • присваивает пустой массив полю один раз (только если он пуст).
  • возвращает значение поля.

то, что Enumerable.Empty<T> на самом деле делает-это только возвращает пустой массив т.

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

принятие статического поля, инициализированного T[0] вместо Enumerable.Empty<T> как

public static readonly T[] EmptyArray = new T[0];

проблема исчезла. Обратите внимание, что модификатор readonly не является определителем. Имея то же самое статическое поле, объявленное с летучие или через свойства вызывает проблему.

С уважением, Даниеле.

кажется, есть проблема с оптимизатором CLR. Выключите "оптимизировать код" на вкладке сборки и попробуйте запустить тест еще раз.