Czy korzystanie z AggressiveInlining w przypadku prostych właściwości jest wadą?

16

Założę się, że mógłbym sam na to odpowiedzieć, gdybym wiedział więcej o narzędziach do analizy zachowania C # / JIT, ale ponieważ nie, proszę o wyrozumiałość.

Mam prosty kod taki jak ten:

    private SqlMetaData[] meta;

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

Jak widać, umieściłem AggressiveInlining, ponieważ uważam, że powinienem być podkreślony.
Myślę. Nie ma żadnej gwarancji, że JIT wprowadziłby to inaczej. Czy się mylę?

Czy robienie tego rodzaju rzeczy może zaszkodzić wydajności / stabilności / czegokolwiek?

Serge
źródło
2
1) Z mojego doświadczenia wynika, że ​​takie prymitywne metody zostaną wprowadzone bez atrybutu. Uznałem, że atrybut ten jest użyteczny w przypadku nietrywialnych metod, które nadal powinny być podkreślone. 2) Nie ma gwarancji, że metoda ozdobiona tym atrybutem będzie również wstawiona. To tylko wskazówka dla JITtera.
CodesInChaos
Nie wiem wiele o nowym atrybucie inliningu, ale umieszczenie go tutaj prawie na pewno nie wpłynie na wydajność. Wszystko, co robisz, to zwracanie odwołania do tablicy, a JIT prawie na pewno już dokonuje właściwego wyboru tutaj.
Robert Harvey
14
3) Zbyt duże wstawianie oznacza, że ​​kod staje się większy i może już nie pasować do pamięci podręcznej. Chybienia w pamięci podręcznej mogą być znaczącym spadkiem wydajności. 4) Polecam nie używać tego atrybutu, dopóki test porównawczy nie wykaże, że poprawia wydajność.
CodesInChaos
4
Przestań się martwić. Im bardziej próbujesz przechytrzyć kompilator, tym bardziej znajdzie on sposób na przechytrzenie ciebie. Znajdź coś innego, o co będziesz się martwić
david.pfx
1
W przypadku moich dwóch centów widziałem duże korzyści w trybie zwalniania, szczególnie gdy wywołuje się większą funkcję w ciasnej pętli.
jjxtra

Odpowiedzi:

22

Kompilatory to inteligentne bestie. Zwykle automatycznie wyciskają jak najwięcej wydajności z dowolnego miejsca.

Próba przechytrzenia kompilatora zwykle nie robi dużej różnicy i ma duże szanse na odwrót. Na przykład wstawianie powoduje, że Twój program jest większy, ponieważ wszędzie kopiuje kod. Jeśli twoja funkcja jest używana w wielu miejscach w całym kodzie, może faktycznie być szkodliwa, jak wskazano @CodesInChaos. Jeśli jest oczywiste, że funkcja powinna być wbudowana, możesz się założyć, że kompilator to zrobi.

W przypadku wahania możesz nadal robić oba i porównywać, jeśli występuje jakiś wzrost wydajności, to jedyny pewny sposób na teraz. Ale założę się, że różnica będzie nieistotna, kod źródłowy będzie po prostu „głośniejszy”.

Dagnele
źródło
3
Myślę, że „hałas” jest tutaj najważniejszy. Utrzymuj swój kod w czystości i ufaj swojemu kompilatorowi, że zrobi właściwą rzecz, dopóki nie zostanie udowodnione inaczej. Cała reszta to niebezpieczna przedwczesna optymalizacja.
5gon12eder,
1
Jeśli kompilatory są tak inteligentne, to po co próbować przechytrzyć kompilator?
Little Endian,
11
Kompilatory nie są inteligentne . Kompilatory nie robią „właściwej rzeczy”. Nie przypisuj inteligencji tam, gdzie jej nie ma. W rzeczywistości kompilator C # / JITer jest zbyt głupi. Na przykład nie wstawi niczego po 32 bajtach IL lub przypadków zawierających structs jako parametry - w wielu przypadkach powinien i powinien. Oprócz pominięcia setek oczywistych optymalizacji - w tym między innymi - unikania niepotrzebnych kontroli granic i alokacji między innymi.
JBeurer,
4
@DaveBlack Bounds sprawdź, czy eluzja w C # dzieje się w bardzo małej liście bardzo podstawowych przypadków, zwykle na najbardziej podstawowej sekwencji wykonywanych pętli, a nawet wtedy wiele prostych pętli nie jest optymalizowanych. Wielowymiarowe pętle tablicowe nie uzyskują eliminacji sprawdzania granic, pętle iterowane w kolejności malejącej nie, pętle na nowo przydzielonych tablicach nie. Bardzo wiele prostych przypadków, w których można oczekiwać, że kompilator wykona swoją pracę. Ale tak nie jest. Bo to wszystko, ale mądre. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer
3
Kompilatory nie są „inteligentnymi bestiami”. Po prostu stosują szereg heurystyk i dokonują kompromisów, aby znaleźć równowagę dla większości scenariuszy przewidywanych przez twórców kompilatorów. Sugeruję przeczytanie: docs.microsoft.com/en-us/previous-versions/dotnet/articles/…
cdiggins
8

Masz rację - nie ma sposobu, aby zagwarantować, że metoda zostanie wstawiona - Wyliczenie metody MSDN MethodImplOptions , SO MethodImplOptions.AggressiveInlining vs. TargetedPatchingOptOut .

Programiści są bardziej inteligentni niż kompilator, ale pracujemy na wyższym poziomie, a nasze optymalizacje są produktami pracy jednego człowieka - naszej. Jitter widzi, co dzieje się podczas egzekucji. Może analizować zarówno przepływ wykonania, jak i kod zgodnie z wiedzą wprowadzoną przez jego projektantów. Możesz lepiej poznać swój program, ale lepiej znają CLR. A kto będzie bardziej poprawny w swoich optymalizacjach? Nie wiemy tego na pewno.

Dlatego powinieneś przetestować każdą dokonaną optymalizację. Nawet jeśli jest to bardzo proste. I weź pod uwagę, że środowisko może się zmienić, a Twoja optymalizacja lub dezoptymalizacja może przynieść całkiem nieoczekiwany rezultat.

Eugene Podskal
źródło
8

EDYCJA: Zdaję sobie sprawę, że moja odpowiedź nie odpowiedziała dokładnie na pytanie, podczas gdy nie ma prawdziwego minusa, z moich wyników czasowych też nie ma prawdziwej plusu. Różnica między wbudowanym narzędziem do pobierania właściwości wynosi 0,002 sekundy ponad 500 milionów iteracji. Mój przypadek testowy może również nie być w 100% dokładny, ponieważ używa struktury, ponieważ istnieją pewne zastrzeżenia do fluktuacji i podkreślenia struktur.

Jak zawsze, jedynym sposobem, aby naprawdę wiedzieć, jest napisanie testu i wymyślenie go. Oto moje wyniki z następującą konfiguracją:

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

Opróżnij projekt z następującymi ustawieniami:

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

Wyniki

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

Testowane z tym kodem:

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);
        }
    }
}
Chris Phillips
źródło
5

Kompilatory wykonują wiele optymalizacji. Inlining jest jednym z nich, niezależnie od tego, czy programista chciał, czy nie. Na przykład MethodImplOptions nie ma opcji „wbudowanej”. Ponieważ wstawianie jest automatycznie wykonywane przez kompilator w razie potrzeby.

Wiele innych optymalizacji jest szczególnie wykonywanych, jeśli włączono je w opcjach kompilacji, lub zrobi to tryb „wydania”. Ale te optymalizacje są w pewnym sensie „działały dla Ciebie, świetnie! Nie działały, zostaw to” optymalizacje i zwykle dają lepszą wydajność.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

jest tylko flagą kompilatora, że ​​naprawdę pożądana jest tutaj operacja wstawiania. Więcej informacji tutaj i tutaj

Odpowiedzieć na Twoje pytanie;

Nie ma żadnej gwarancji, że JIT wprowadziłby to inaczej. Czy się mylę?

Prawdziwe. Bez gwarancji; Żaden C # nie ma opcji „wymuszania”.

Czy robienie tego rodzaju rzeczy może zaszkodzić wydajności / stabilności / czegokolwiek?

W tym przypadku nie, jak powiedziano w piśmie Pisanie aplikacji zarządzanych o wysokiej wydajności: podkład

Metody pobierania i ustawiania właściwości są ogólnie dobrymi kandydatami do wstawiania, ponieważ wszystko, co robią, to zazwyczaj inicjowanie prywatnych członków danych.

myuce
źródło
1
Oczekuje się, że odpowiedzi w pełni odpowiedzą na pytanie. Jest to początek odpowiedzi, ale tak naprawdę nie wchodzi w głębię oczekiwanej odpowiedzi.
1
Zaktualizowałem moją odpowiedź. Mam nadzieję, że to pomoże.
myuce