Чи є недолік використання AggressiveInlining на простих властивостях?


16

Б'юсь об заклад, що я міг би відповісти на це сам, якби знав більше про інструменти для аналізу поведінки C # / JIT, але оскільки я цього не роблю, будь ласка, попросіть мене запитати.

У мене такий простий код:

    private SqlMetaData[] meta;

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

Як ви бачите, я поставив AggressiveInlining, тому що я відчуваю, що його слід накреслити.
Я думаю. Немає жодної гарантії, що JIT змінює це інакше. Я помиляюся?

Чи може такий спосіб зашкодити продуктивності / стабільності / чомусь?


2
1) На мій досвід такі примітивні методи будуть вписані без атрибуту. В основному я вважав цей атрибут корисним для нетривіальних методів, які все ж слід накреслити. 2) Немає гарантії, що метод, прикрашений атрибутом, також буде вкладений. Це просто натяк на JITter.
CodesInChaos

Я мало знаю про новий вбудований атрибут, але розміщення його майже напевно не змінить продуктивність. Все, що ви робите, - це повернути посилання на масив, і JIT майже напевно вже зробить правильний вибір тут.
Роберт Харві

14
3) Якщо вбудовано занадто багато, це означає, що код стає більшим, і він більше не може входити в кеші. Пропуски кеш-пам'яті можуть бути значним хітом для продуктивності. 4) Я рекомендую не використовувати атрибут, поки показник не покаже, що він покращує продуктивність.
CodesInChaos

4
Киньте хвилюватися. Чим більше ви намагаєтесь перехитрити компілятора, тим більше знайде способів перехитрити вас. Знайдіть ще щось, про що слід потурбуватися.
david.pfx

1
За два мої центи я побачив значні вигоди в режимі випуску, особливо під час виклику більшої функції в щільному циклі.
jjxtra

Відповіді:


22

Компілятори - розумні звірі. Зазвичай вони автоматично видавлюють максимальну ефективність із будь-якого місця.

Намагання перехитрити компілятор, як правило, не має великого значення, і має багато шансів на те, щоб отримати зворотний спалах. Наприклад, вбудована лінія робить вашу програму більшою, оскільки вона скрізь дублює код. Якщо ваша функція використовується в багатьох місцях у всьому коді, це може бути фактично згубним, як вказувалося @CodesInChaos. Якщо очевидно, що функція повинна бути вписана, ви можете зробити ставку, що компілятор це зробить.

У разі коливання, ви все одно можете зробити і те, і порівняти, якщо є якісь підвищення продуктивності, це єдиний певний шлях до цього часу. Але я ставлю на думку, що різниця буде незначною, вихідний код буде просто "шумнішим".


3
Я думаю, що «шум» є найважливішим моментом тут. Тримайте свій код в порядку і довіряйте своєму компілятору робити правильно, поки не буде доведено інше. Все інше - небезпечна передчасна оптимізація.
5gon12eder

1
Якщо компілятори настільки розумні, то навіщо намагатися перехитрити зворотний спалах компілятора?
Little Endian

11
Компілятори не розумні . Компілятори не роблять «правильно». Не приписуйте інтелект там, де його немає. Насправді компілятор C # / JITer надмірно німий. Наприклад, він не буде вбудовувати нічого, що минуло 32 байт IL, або випадки, що включають structs як параметри - де в дуже багатьох випадках він повинен і міг. Окрім того, що не вистачає сотень очевидних оптимізацій - включаючи, але не обмежуючись цим, - уникати зайвих перевірок меж та розподілу між іншим.
JBeurer

4
@DaveBlack Bounds перевірка виключення в C # трапляється в дуже малому списку дуже основних випадків, як правило, в самому базовому послідовному циклі, що виконується, і навіть тоді багато простих циклів не вдається оптимізувати. Петлі багатовимірного масиву не отримують усунення меж перевірки, петлі, повторені в порядку зменшення, не мають циклів на щойно виділених масивах. Дуже багато простих випадків, коли ви очікуєте, що компілятор виконає свою роботу. Але це не так. Бо це все що завгодно, але розумно. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer

3
Укладачі - це не "розумні звірі". Вони просто застосовують купу евристики і роблять компроміси, щоб спробувати знайти баланс для більшості сценаріїв, які очікували автори-компілятори. Пропоную прочитати: docs.microsoft.com/en-us/previous-versions/dotnet/articles/…
cdiggins

8

Ви маєте рацію - немає способу гарантувати, що метод буде накреслений - MSDN MethodImplOptions Enumeration , SO MethodImplOptions.AggressiveInlining vs Tar targetPatchingOptOut .

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

Ось чому слід перевірити будь-яку оптимізацію. Навіть якщо це дуже просто. І врахуйте, що середовище може змінитися, і ваша оптимізація чи деоптимізація може мати досить несподіваний результат.


8

EDIT: Я усвідомлюю, що моя відповідь точно не відповіла на це питання, хоча реального недоліку немає, від моїх результатів часу немає і реального перелому. Різниця між вбудованим власником властивості становить 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 немає опції "вбудований". Тому що вбудовування автоматично робиться компілятором при необхідності.

Багато інших оптимізацій особливо робиться, якщо їх увімкнено з параметрів збірки, або режим "звільнення" зробить це. Але ці оптимізації начебто "працювали для вас, чудово! Не працювали, не залишайте" оптимізації і зазвичай дають кращі показники.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

- це лише прапор для компілятора, що операція вбудовування тут дуже потрібна. Більше інформації тут і тут

Щоб відповісти на ваше запитання;

Немає жодної гарантії, що JIT змінює це інакше. Я помиляюся?

Правда. Гарантії немає; Ні в C # немає опції "сила вбудовування".

Чи може такий спосіб зашкодити продуктивності / стабільності / чомусь?

У цьому випадку ні, як сказано в Написанні високоефективних керованих додатків: Підручник

Методи отримання та встановлення власності, як правило, є хорошими кандидатами для вставки, оскільки все, що вони роблять, як правило, ініціалізує членів приватних даних.


1
Очікується, що відповіді повністю відповідають на запитання. Хоча це початок відповіді, він дійсно не заглиблюється в глибину, яку очікує відповідь.

1
Оновлено мою відповідь. Сподіваюся, це допоможе.
мійс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.