C'è un aspetto negativo nell'utilizzo di AggressiveInlining su proprietà semplici?


16

Scommetto che potrei rispondere a me stesso se sapessi di più sugli strumenti per analizzare come si comporta C # / JIT, ma dato che non lo so, per favore sopporta di chiedermelo.

Ho un codice semplice come questo:

    private SqlMetaData[] meta;

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

Come puoi vedere, ho inserito AggressiveInlining perché sento che dovrebbe essere integrato.
Penso. Non vi è alcuna garanzia che JIT lo inserisca diversamente. Ho sbagliato?

Fare questo tipo di cose potrebbe danneggiare le prestazioni / la stabilità / qualcosa?


2
1) Nella mia esperienza tali metodi primitivi saranno delineati senza l'attributo. Principalmente ho trovato l'attributo utile con metodi non banali che dovrebbero essere ancora sottolineati. 2) Non vi è alcuna garanzia che anche un metodo decorato con l'attributo sia incorporato. È solo un suggerimento per JITter.
CodesInChaos,

Non so molto del nuovo attributo inline, ma metterne uno qui non farà sicuramente alcuna differenza nelle prestazioni. Tutto quello che stai facendo è restituire un riferimento a un array e la JIT farà quasi sicuramente la scelta giusta qui.
Robert Harvey,

14
3) Inline troppo significa che il codice diventa più grande e potrebbe non rientrare più nella cache. I mancati errori nella cache possono essere un notevole successo di prestazioni. 4) Consiglio di non utilizzare l'attributo fino a quando un benchmark non mostra che migliora le prestazioni.
CodesInChaos,

4
Smetti di preoccuparti. Più si tenta di superare in astuzia il compilatore, più troverà il modo di superare in astuzia. Trova qualcos'altro di cui preoccuparti.
david.pfx,

1
Per i miei due centesimi, ho visto grandi guadagni in modalità di rilascio, specialmente quando ho chiamato una funzione più grande in un ciclo stretto.
jjxtra,

Risposte:


22

I compilatori sono bestie intelligenti. Di solito, spremono automaticamente tutte le prestazioni che possono da qualsiasi luogo possibile.

Cercare di superare in astuzia il compilatore di solito non fa una grande differenza e ha molte possibilità di ritorcersi contro. Ad esempio, l'inline rende il tuo programma più grande poiché duplica il codice ovunque. Se la tua funzione viene utilizzata in molti punti del codice, potrebbe effettivamente essere dannosa come sottolineato @CodesInChaos. Se è ovvio che la funzione dovrebbe essere integrata, puoi scommettere che il compilatore lo farà.

In caso di esitazione, puoi ancora fare entrambe le cose e confrontare se c'è qualche miglioramento delle prestazioni, questo è l'unico modo certo per ora. Ma la mia scommessa è che la differenza sarà trascurabile, il codice sorgente sarà semplicemente "più rumoroso".


3
Penso che il "rumore" sia il punto più importante qui. Mantieni il tuo codice in ordine e fidati del tuo compilatore per fare la cosa giusta fino a prova contraria. Tutto il resto è una pericolosa ottimizzazione prematura.
5gon12eder,

1
Se i compilatori sono così intelligenti, perché provare a battere in astuzia il compilatore?
Little Endian,

11
I compilatori non sono intelligenti . I compilatori non fanno "la cosa giusta". Non attribuire intelligenza dove non lo è. In effetti, il compilatore / JITer C # è eccessivamente stupido. Ad esempio, non incorporerà nulla oltre 32 byte IL o casi che coinvolgono structs come parametri - dove in molti casi dovrebbe e potrebbe. Oltre a perdere centinaia di ovvie ottimizzazioni, tra cui, a titolo esemplificativo, evitare inutili controlli e allocazioni dei limiti tra le altre cose.
JBeurer il

4
@DaveBlack Bounds controlla che l'elusione in C # avvenga in un elenco molto piccolo di casi molto semplici, di solito sul sequenziale più semplice per i loop eseguiti e anche in questo caso molti semplici loop non vengono ottimizzati. I loop di array multidimensionali non ottengono l'eliminazione del controllo dei limiti, i loop iterati in ordine decrescente no, i loop su array appena allocati no. Molti semplici casi in cui ti aspetteresti che il compilatore faccia il suo lavoro. Ma non lo fa. Perché è tutto, ma intelligente. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer

3
I compilatori non sono "bestie intelligenti". Applicano semplicemente un mucchio di euristiche e fanno dei compromessi per cercare di trovare un equilibrio per la maggior parte degli scenari previsti dagli autori del compilatore. Suggerisco di leggere: docs.microsoft.com/en-us/previous-versions/dotnet/articles/…
cdiggins

8

Hai ragione - non c'è modo di garantire che il metodo sia integrato - MSDN MethodImplOptions Enumeration , SO MethodImplOptions.AggressiveInlining vs TargetedPatchingOptOut .

I programmatori sono più intelligenti di un compilatore, ma lavoriamo a un livello superiore e le nostre ottimizzazioni sono prodotti del lavoro di un solo uomo, il nostro. Jitter vede cosa sta succedendo durante l'esecuzione. Può analizzare sia il flusso di esecuzione che il codice in base alle conoscenze fornite dai suoi progettisti. Puoi conoscere meglio il tuo programma, ma conoscono meglio il CLR. E chi sarà più corretto nelle sue ottimizzazioni? Non lo sappiamo per certo.

Ecco perché dovresti testare qualsiasi ottimizzazione effettuata. Anche se è molto semplice. E tieni presente che l'ambiente potrebbe cambiare e l'ottimizzazione o la non ottimizzazione possono avere risultati piuttosto inaspettati.


8

EDIT: mi rendo conto che la mia risposta non ha esattamente risposto alla domanda, mentre non c'è un vero svantaggio, dai miei risultati di temporizzazione non c'è nemmeno un vero vantaggio. La differenza tra un getter di proprietà in linea è 0,002 secondi su 500 milioni di iterazioni. Il mio caso di test potrebbe anche non essere accurato al 100% poiché utilizza uno struct perché ci sono alcuni avvertimenti sul jitter e sull'allineamento con le strutture.

Come sempre, l'unico modo per sapere davvero è scrivere un test e capirlo. Ecco i miei risultati con la seguente configurazione:

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

Progetto vuoto con le seguenti impostazioni:

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

risultati

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

Testato con questo codice:

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

I compilatori fanno molte ottimizzazioni. Inline è uno di questi, che il programmatore lo voglia o no. Ad esempio, MethodImplOptions non ha un'opzione "inline". Perché inline viene automaticamente eseguito dal compilatore, se necessario.

Molte altre ottimizzazioni vengono eseguite in particolare se abilitate dalle opzioni di generazione o la modalità "release" lo farà. Ma queste ottimizzazioni sono in qualche modo ottimizzazioni "ha funzionato per te, fantastico! Non ha funzionato, lascialo" e di solito danno prestazioni migliori.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

è solo una bandiera per il compilatore che un'operazione di allineamento è davvero desiderata qui. Maggiori informazioni qui e qui

Per rispondere alla tua domanda;

Non vi è alcuna garanzia che JIT lo inserisca diversamente. Ho sbagliato?

Vero. Nessuna garanzia; Nessuno dei due C # ha un'opzione "force inlining".

Fare questo tipo di cose potrebbe danneggiare le prestazioni / la stabilità / qualcosa?

In questo caso no, come si dice in Scrivere applicazioni gestite ad alte prestazioni: un primer

I metodi di acquisizione e impostazione della proprietà sono in genere buoni candidati per l'inline, poiché tutto ciò che fanno è in genere inizializzare i membri di dati privati.


1
Si prevede che le risposte rispondano pienamente alla domanda. Mentre questo è un inizio per una risposta, in realtà non va nella profondità prevista per una risposta.

1
Aggiornato la mia risposta. Spero che possa essere d'aiuto.
myuce,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.