Есть ли недостаток в использовании AggressiveInlining для простых свойств?

16

Могу поспорить, что мог бы ответить на этот вопрос сам, если бы я знал больше об инструментах для анализа поведения C # / JIT, но, поскольку я не знаю, прошу терпеть меня.

У меня есть простой код, подобный этому:

    private SqlMetaData[] meta;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private SqlMetaData[] Meta
    {
        get
        {
            return this.meta;
        }
    }

Как видите, я поставил AggressiveInlining, потому что я чувствую, что он должен быть встроен.
Я думаю. Нет никакой гарантии, что JIT включит это в противном случае. Я ошибаюсь?

Могут ли подобные вещи навредить производительности / стабильности / чему-либо?

саржа
источник
2
1) По моему опыту, такие примитивные методы будут встроены без атрибута. В основном я нашел атрибут полезным для нетривиальных методов, которые все еще должны быть встроены. 2) Нет гарантии, что метод, украшенный атрибутом, также будет встроен. Это просто намек на JITter.
CodesInChaos
Я не знаю много о новом атрибуте inlining, но его размещение почти наверняка не повлияет на производительность. Все, что вы делаете, это возвращаете ссылку на массив, и JIT почти наверняка уже сделает правильный выбор здесь.
Роберт Харви
14
3) слишком большое встраивание означает, что код становится больше и может больше не помещаться в кеши. Промахи в кеше могут сильно повлиять на производительность. 4) Я рекомендую не использовать атрибут, пока тест не покажет, что он улучшает производительность.
CodesInChaos
4
Перестань беспокоиться. Чем больше вы пытаетесь перехитрить компилятор, тем больше он найдет способы перехитрить вас. Найдите что-то еще, чтобы волноваться.
david.pfx
1
Для моих двух центов я видел большой выигрыш в режиме релиза, особенно при вызове большой функции в тесном цикле.
JJXTRA

Ответы:

22

Компиляторы умные звери. Обычно они автоматически выжимают столько производительности, сколько могут из любого места.

Попытка перехитрить компилятор обычно не имеет большого значения, и у него есть много шансов получить ответный удар. Например, встраивание делает вашу программу больше, поскольку она дублирует код везде. Если ваша функция используется во многих местах кода, она может быть вредной, как указано @CodesInChaos. Если очевидно, что функция должна быть встроенной, вы можете поспорить, что компилятор сделает это.

В случае колебаний вы все равно можете сделать и то, и другое и сравнить, если есть какой-либо выигрыш в производительности, это единственный верный способ сейчас. Но моя ставка в том, что разница будет пренебрежимо мала, исходный код будет просто «шумнее».

dagnelies
источник
3
Я думаю, что «шум» является наиболее важным моментом здесь. Держите ваш код в чистоте и доверяйте своему компилятору делать правильные вещи, пока не доказано обратное. Все остальное - опасная преждевременная оптимизация.
5gon12eder
1
Если компиляторы настолько умны, то зачем пытаться перехитрить обратную реакцию компилятора?
Little Endian
11
Компиляторы не умны . Компиляторы не делают "правильную вещь". Не приписывайте интеллект там, где его нет. На самом деле, компилятор C # / JITer слишком тупой. Например, он не будет вставлять что-либо за пределами 32 байтов IL или случаев, включающих structs в качестве параметров - где во многих случаях он должен и мог. В дополнение к отсутствию сотен очевидных оптимизаций - включая, но не ограничиваясь - избегая ненужных проверок границ и выделений среди прочего.
JBeurer
4
@DaveBlack Ограничение проверки границ в C # происходит в очень небольшом списке самых простых случаев, обычно в самых основных последовательных циклах, для которых выполняется, и даже тогда многие простые циклы не могут быть оптимизированы. Циклы многомерного массива не получают исключения проверки границ, циклы, повторяющиеся в порядке убывания, не имеют, циклы на вновь распределенных массивах - нет. Очень много простых случаев, когда вы ожидаете, что компилятор выполнит свою работу. Но это не так. Потому что это все что угодно, но умно. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer
3
Компиляторы не "умные звери". Они просто применяют кучу эвристик и находят компромиссы, чтобы попытаться найти баланс для большинства сценариев, ожидаемых авторами компилятора. Я предлагаю прочитать: docs.microsoft.com/en-us/previous-versions/dotnet/articles/…
cdiggins
8

Вы правы - нет способа гарантировать, что метод будет встроен - перечисление MSDN MethodImplOptions , SO MethodImplOptions.AggressiveInlining vs TargetedPatchingOptOut .

Программисты умнее, чем компилятор, но мы работаем на более высоком уровне, и наши оптимизации являются продуктами работы одного человека - нашей собственной. Джиттер видит, что происходит во время казни. Он может анализировать как поток выполнения, так и код в соответствии со знаниями, заложенными в него его разработчиками. Вы можете лучше знать свою программу, но они лучше знают CLR. И кто будет более правильным в его оптимизации? Мы не знаем наверняка.

Вот почему вы должны проверить любую сделанную вами оптимизацию. Даже если это очень просто. И учтите, что среда может измениться, и ваша оптимизация или дезоптимизация может привести к совершенно неожиданному результату.

Евгений Подскал
источник
8

РЕДАКТИРОВАТЬ: я понимаю, что мой ответ не совсем ответил на вопрос, хотя нет никаких реальных недостатков, из моих результатов синхронизации также нет никакого реального преимущества. Разница между получателем встроенного свойства составляет 0,002 секунды за 500 миллионов итераций. Мой тестовый пример также может быть не точным на 100%, так как он использует структуру, потому что есть некоторые предостережения относительно дрожания и встраивания со структурами.

Как всегда, единственный способ узнать это - написать тест и разобраться. Вот мои результаты со следующей конфигурацией:

Windows 7 Home  
8GB ram  
64bit os  
i5-2300 2.8ghz  

Пустой проект со следующими настройками:

.NET 4.5  
Release mode  
Start without debugger attached - CRUCIAL  
Unchecked "Prefer 32-bit" under project build settings  

Результаты

struct get property                               : 0.3097832 seconds
struct inline get property                        : 0.3079076 seconds
struct method call with params                    : 1.0925033 seconds
struct inline method call with params             : 1.0930666 seconds
struct method call without params                 : 1.5211852 seconds
struct intline method call without params         : 1.2235001 seconds

Протестировано с этим кодом:

class Program
{
    const int SAMPLES = 5;
    const int ITERATIONS = 100000;
    const int DATASIZE = 1000;

    static Random random = new Random();
    static Stopwatch timer = new Stopwatch();
    static Dictionary<string, TimeSpan> timings = new Dictionary<string, TimeSpan>();

    class SimpleTimer : IDisposable
    {
        private string name;
        public SimpleTimer(string name)
        {
            this.name = name;
            timer.Restart();
        }

        public void Dispose()
        {
            timer.Stop();
            TimeSpan ts = TimeSpan.Zero;
            if (timings.ContainsKey(name))
                ts = timings[name];

            ts += timer.Elapsed;
            timings[name] = ts;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 4)]
    struct TestStruct
    {
        private int x;
        public int X { get { return x; } set { x = value; } }
    }


    [StructLayout(LayoutKind.Sequential, Size = 4)]
    struct TestStruct2
    {
        private int x;

        public int X
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get { return x; }
            set { x = value; }
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct3
    {
        private int x;
        private int y;

        public void Update(int _x, int _y)
        {
            x += _x;
            y += _y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct4
    {
        private int x;
        private int y;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Update(int _x, int _y)
        {
            x += _x;
            y += _y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct5
    {
        private int x;
        private int y;

        public void Update()
        {
            x *= x;
            y *= y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct6
    {
        private int x;
        private int y;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Update()
        {
            x *= x;
            y *= y;
        }
    }

    static void RunTests()
    {
        for (var i = 0; i < SAMPLES; ++i)
        {
            Console.Write("Sample {0} ... ", i);
            RunTest1();
            RunTest2();
            RunTest3();
            RunTest4();
            RunTest5();
            RunTest6();
            Console.WriteLine(" complate");
        }
    }

    static int RunTest1()
    {
        var data = new TestStruct[DATASIZE];
        var temp = 0;
        unchecked
        {
            //init the data, just so jitter can't make assumptions
            for (var j = 0; j < DATASIZE; ++j)
                data[j].X = random.Next();

            using (new SimpleTimer("struct get property"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        temp += data[j].X;
                    }
                }
            }
        }
        //again need variables to cross scopes to make sure the jitter doesn't do crazy optimizations
        return temp;
    }

    static int RunTest2()
    {
        var data = new TestStruct2[DATASIZE];
        var temp = 0;
        unchecked
        {
            //init the data, just so jitter can't make assumptions
            for (var j = 0; j < DATASIZE; ++j)
                data[j].X = random.Next();

            using (new SimpleTimer("struct inline get property"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        temp += data[j].X;
                    }
                }
            }
        }
        //again need variables to cross scopes to make sure the jitter doesn't do crazy optimizations
        return temp;
    }

    static void RunTest3()
    {
        var data = new TestStruct3[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct method call with params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update(j, i);
                    }
                }
            }
        }
    }

    static void RunTest4()
    {
        var data = new TestStruct4[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct inline method call with params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update(j, i);
                    }
                }
            }
        }
    }

    static void RunTest5()
    {
        var data = new TestStruct5[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct method call without params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update();
                    }
                }
            }
        }
    }

    static void RunTest6()
    {
        var data = new TestStruct6[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct intline method call without params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update();
                    }
                }
            }
        }
    }

    static void Main(string[] args)
    {
        RunTests();
        DumpResults();
        Console.Read();
    }

    static void DumpResults()
    {
        foreach (var kvp in timings)
        {
            Console.WriteLine("{0,-50}: {1} seconds", kvp.Key, kvp.Value.TotalSeconds);
        }
    }
}
Крис Филлипс
источник
5

Компиляторы делают много оптимизаций. Встраивание является одним из них, хотел ли программист или нет. Например, у MethodImplOptions нет опции «inline». Поскольку встраивание автоматически выполняется компилятором, если это необходимо.

Многие другие оптимизации особенно выполняются, если они включены в опциях сборки или в режиме «релиз». Но эти оптимизации как бы «работали для вас, отлично! Не работали, оставьте это» и обычно дают лучшую производительность.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

это просто признак для компилятора, что операция вставки здесь действительно нужна. Больше информации здесь и здесь

Ответить на ваш вопрос;

Нет никакой гарантии, что JIT включит это в противном случае. Я ошибаюсь?

Правда. Нет гарантии; Ни один из C # не имеет опции «принудительное включение».

Могут ли подобные вещи навредить производительности / стабильности / чему-либо?

В этом случае нет, как сказано в статье «Создание высокопроизводительных управляемых приложений: учебник для начинающих»

Методы get и set свойства, как правило, являются хорошими кандидатами для встраивания, поскольку все, что они делают, обычно инициализируют частные элементы данных.

myuce
источник
1
Ожидается, что ответы полностью ответят на вопрос. Хотя это начало ответа, на самом деле оно не идет вглубь, ожидаемую для ответа.
1
Обновил мой ответ. Надеюсь, это поможет.
Myuce