Интересное падение производительности
От: Димчанский Литва http://dimchansky.github.io/
Дата: 28.11.14 08:09
Оценка: 80 (10)
Коллеги наткнулись на интересное падение производительности. Подняли тему на stackoverflow.
Наткнулись в реальном коде, им удалось воссоздать минимальный пример.

Есть generic класс
public class BaseClass<T>
{
    private List<T> _list = new List<T>();

    public BaseClass()
    {
    // если убрать эту строку из конструктора, то падения производительности в Run не происходит
        Enumerable.Empty<T>();
        // или Enumerable.Repeat(new T(), 10);
        // или даже new T();
        // или foreach (var item in _list) {}
        // или что-либо, что ссылается на тип T
    }

    public void Run()
    {
        for (var i = 0; i < 8000000; i++)
        {
            _list.Any();
        }
    }
}


создается наследник

public class DerivedClass : BaseClass<object>
{
}


Далее делают замеры метода Run из обоих классов. Предвосхищая вопросы о методах замера, они пробовали и с разогревом и меняя местами строки Measure.
Компилировали в 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);
    }
}


Вызов метода Run в наследованном классе DerivedClass в 4 раза медленнее, чем в BaseClass<object>.
Есть мысли, почему так может быть?
Re: Интересное падение производительности
От: Sharov Россия  
Дата: 28.11.14 09:08
Оценка:
Здравствуйте, Димчанский, Вы писали:

А если сделать
public class DerivedClass : BaseClass<int>
{
}


то производительность будет не хуже, а может и чуточку лучше. .net 4.0, версия debug
Кодом людям нужно помогать!
Re: Еще интересные детали
От: Димчанский Литва http://dimchansky.github.io/
Дата: 28.11.14 09:09
Оценка: 1 (1) +1
Если скомпилировать в 64-битном режиме (убрать галку prefer 32-bit), то
вариант Main без разогрева
        public static void Main()
        {
            Measure(new DerivedClass());
            Measure(new BaseClass<object>());
        }

Выдает в 64-битном режиме ускорение наследника

528
666

Но если сделать разогрев:
        public static void Main()
        {
            var derivedClass = new DerivedClass();
            var baseClass = new BaseClass<object>();
            derivedClass.Run();
            baseClass.Run();

            Measure(derivedClass);
            Measure(baseClass);
        }

то в 64-битном режиме получаем все то же падение скорости у наследника

1894
666

Re: Интересное падение производительности
От: Sinix  
Дата: 28.11.14 09:42
Оценка: 1 (1)
Здравствуйте, Димчанский, Вы писали:

Д>Коллеги наткнулись на интересное падение производительности. Подняли тему на stackoverflow.

Д>Наткнулись в реальном коде, им удалось воссоздать минимальный пример.

Немного поиграю в кэпа

1. Проблема с энумераторами. Чтобы проверить: заменяем list.Any(); на _list.GetEnumerator().MoveNext() и получаем ещё более забавные результаты.
2. JIT-выхлоп для самих вызовов не отличается, если я ничего не пропустил. Остаётся выравнивание на стеке/попадание в линии кэша процессора. Но чтобы так стабильно воспроизводилось на разных машинах?

Будет время — покопаюсь ещё.

UPD. в общем, вот вывод disassembly для кода,
            [MethodImpl(MethodImplOptions.NoInlining)]
            public void Run()
            {
                for (var i = 0; i < 8000000; i++)
                {
                    var a = _list.GetEnumerator();
                    a.MoveNext();
                }
            }

кому интересно — разбирайтесь
(инструкции для настройки студии для просмотра disassembly — тынц)
Отредактировано 28.11.2014 10:05 Sinix . Предыдущая версия .
Re: Интересное падение производительности
От: Sinix  
Дата: 28.11.14 11:07
Оценка: 18 (1)
Здравствуйте, Димчанский, Вы писали:

Д>Вызов метода Run в наследованном классе DerivedClass в 4 раза медленнее, чем в BaseClass<object>.

Д>Есть мысли, почему так может быть?

JIT чудит.
        public class BaseClass<T>
        {
            private static readonly T[] stub = new T[0];
            private List<T> _list = new List<T>();

            public BaseClass()
            {
                var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);

                //_list.Add(default(T));
                // or Enumerable.Repeat(new T(), 10);
                // or even new T();
                // or foreach (var item in _list) {}
                // or something that relates to T
            }

            public void Run()
            {
                for (var i = 0; i < 8000000; i++)
                {
                    _list.Any();
                }
            }
            public void Run2() // не вызывается
            {
                for (var i = 0; i < 8000000; i++)
                {
                    _list.Any();
                }
            }
            public void Run3() // не вызывается
            {
                for (var i = 0; i < 8000000; i++)
                {
                    _list.Any();
                }
            }
        }


Комментируем Run2 или Run3 — получаем старые результаты. Не комментируем — всё ок.
Что-то не то с размером methodTable или с выравниванием?
Re: Интересное падение производительности
От: vorona  
Дата: 28.11.14 12:55
Оценка: +1
Здравствуйте, Димчанский, Вы писали:

using System;
using System.Runtime.CompilerServices;

public static class Foo
{
    public static void Bar<T>() { }
    public static void Bar2<T>() { }
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static void Bar3<T>() { }
}
public class A<T>
{
    public void Test()
    {
        Foo.Bar<T>();
        //Foo.Bar3<T>();
        Foo.Bar2<T>();
        for (int i = 0; i < 1000000; i++)
            Foo.Bar3<T>();
    }
}
public sealed class B : A<object>
{
}

public class Program
{
    public static void Main()
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        new A<object>().Test();
        sw.Stop();
        Console.WriteLine("A<object> " + sw.ElapsedMilliseconds);

        sw.Restart();
        new B().Test();
        sw.Stop();
        Console.WriteLine("B " + sw.ElapsedMilliseconds);

        Console.ReadLine();
    }
}


A<object> 11
B 110

если раскомментировать Foo.Bar3<T>()
A<object> 2
B 2
Re[2]: Интересное падение производительности
От: Sinix  
Дата: 28.11.14 13:48
Оценка:
Здравствуйте, vorona, Вы писали:

V>если раскомментировать Foo.Bar3<T>()

V>A<object> 2
V>B 2

Ага. Это упрощённый пример топикстартера. И "лечится" точно так же:
    public class A<T>
    {
        public void Test()
        {
            Foo.Bar<T>();
            //Foo.Bar3<T>();
            Foo.Bar2<T>();
            for (int i = 0; i < 1000000; i++)
                Foo.Bar3<T>();
        }
        public void Test2()
        {
        }
        public void Test3()
        {
        }
    }


убираем Test2 или Test3 — всё снова ломается.
Отредактировано 28.11.2014 13:49 Sinix . Предыдущая версия .
Re[3]: Интересное падение производительности
От: vorona  
Дата: 28.11.14 14:09
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Ага. Это упрощённый пример топикстартера. И "лечится" точно так же:

S>
S>    public class A<T>
S>    {
S>        public void Test()
S>        {
S>            Foo.Bar<T>();
S>            //Foo.Bar3<T>();
S>            Foo.Bar2<T>();
S>            for (int i = 0; i < 1000000; i++)
S>                Foo.Bar3<T>();
S>        }
S>        public void Test2()
S>        {
S>        }
S>        public void Test3()
S>        {
S>        }
S>    }
S>


S>убираем Test2 или Test3 — всё снова ломается.


На каждую добавленную функцию TestN() надо добавить Foo.BarN<T>()
и все повторяется
Re[2]: Интересное падение производительности
От: vorona  
Дата: 01.12.14 15:15
Оценка: 69 (3)
00290126 8b0e            mov     ecx,dword ptr [esi]
00290128 8b4120          mov     eax,dword ptr [ecx+20h]
0029012b 8b00            mov     eax,dword ptr [eax]
0029012d 8b4008          mov     eax,dword ptr [eax+8]
00290130 85c0            test    eax,eax
00290132 750a            jne     0029013e
00290134 badc1a1900      mov     edx,191ADCh
00290139 e8cebc5f73      call    clr!JIT_GenericHandleClass (7388be0c)
0029013e 8bc8            mov     ecx,eax
00290140 ff15c03b1900    call    dword ptr ds:[193BC0h] (Foo.Bar2[[System.__Canon, mscorlib]](), mdToken: 06000002)

00290146 8b0e            mov     ecx,dword ptr [esi]
00290148 ba441b1900      mov     edx,191B44h
0029014d e8babc5f73      call    clr!JIT_GenericHandleClass (7388be0c)
00290152 8bc8            mov     ecx,eax
00290154 ff15703c1900    call    dword ptr ds:[193C70h] (Foo.Bar3[[System.__Canon, mscorlib]](), mdToken: 06000003)

в правильном случае генерируется код на проверку вызова clr!JIT_GenericHandleClass (test eax, eax),
в медленном этот метод вызывается всегда, для наследника он работает долго
Re[3]: Интересное падение производительности
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 01.12.14 15:47
Оценка:
Здравствуйте, vorona, Вы писали:

Оказывается проблема не нова https://connect.microsoft.com/VisualStudio/feedback/details/752766/generic-collections-slow-down-on-remove-operations
и солнце б утром не вставало, когда бы не было меня
Re[3]: Интересное падение производительности
От: Sinix  
Дата: 02.12.14 06:13
Оценка: 13 (2) :)
Здравствуйте, vorona, Вы писали:

V>в правильном случае генерируется код на проверку вызова clr!JIT_GenericHandleClass (test eax, eax),

V>в медленном этот метод вызывается всегда, для наследника он работает долго


А вот объяснение от саппорта MS Connect:

It is related to dictionary lookups in shared generics code. The heuristic in runtime and JIT do not work well for this particular test. We will take a look what can be done about it.

In the meantime, you can workaround it by adding two dummy methods to the BaseClass (do not even need to be called). It will cause the heuristic to work as one would expect.

 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.