Gibt es einen Nachteil bei der Verwendung von AggressiveInlining für einfache Eigenschaften?

16

Ich wette, ich könnte das selbst beantworten, wenn ich mehr über Tools zur Analyse des C # / JIT-Verhaltens wüsste.

Ich habe einfachen Code wie diesen:

    private SqlMetaData[] meta;

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

Wie Sie sehen können, setze ich AggressiveInlining, weil ich der Meinung bin, dass es inline sein sollte.
Meiner Ansicht nach. Es gibt keine Garantie dafür, dass das JIT es ansonsten inline setzen würde. Liege ich falsch?

Könnte so etwas die Leistung / Stabilität / irgendetwas beeinträchtigen?

Serge
quelle
2
1) Nach meiner Erfahrung werden solche primitiven Methoden ohne das Attribut inline gesetzt. Ich fand das Attribut hauptsächlich bei nicht trivialen Methoden nützlich, die noch inline gesetzt werden sollten. 2) Es kann nicht garantiert werden, dass eine Methode, die mit dem Attribut verziert ist, ebenfalls eingebettet wird. Es ist nur ein Hinweis für den JITter.
CodesInChaos
Ich weiß nicht viel über das neue Inlining-Attribut, aber es wird mit ziemlicher Sicherheit keinen Einfluss auf die Leistung haben, wenn Sie eines hier einfügen. Sie geben lediglich einen Verweis auf ein Array zurück, und die JIT wird mit ziemlicher Sicherheit hier bereits die richtige Wahl treffen.
Robert Harvey
14
3) Zu viel Inlining bedeutet, dass der Code größer wird und möglicherweise nicht mehr in Caches passt. Cache-Fehler können einen erheblichen Leistungseinbruch bedeuten. 4) Ich empfehle, das Attribut erst zu verwenden, wenn ein Benchmark zeigt, dass es die Leistung verbessert.
CodesInChaos
4
Hör auf, dir Sorgen zu machen. Je mehr Sie versuchen, den Compiler zu überlisten, desto mehr Möglichkeiten finden Sie, Sie zu überlisten. Finden Sie etwas anderes, um das Sie sich Sorgen machen müssen.
david.pfx
1
Für meine zwei Cent habe ich große Gewinne im Release-Modus gesehen, besonders wenn ich eine größere Funktion in einer engen Schleife aufrufe.
jjxtra

Antworten:

22

Compiler sind intelligente Bestien. Normalerweise werden sie automatisch so viel Leistung wie möglich von jedem Ort aus herausholen, an dem sie können.

Der Versuch, den Compiler auszutricksen, macht normalerweise keinen großen Unterschied und birgt eine Menge Chancen auf Fehlzündungen. Durch Inlining wird Ihr Programm beispielsweise größer, da der Code überall dupliziert wird. Wenn Ihre Funktion an vielen Stellen im Code verwendet wird, kann dies nachteilig sein, wie unter @CodesInChaos erläutert. Wenn es offensichtlich ist, dass die Funktion eingebettet sein sollte, können Sie wetten, dass der Compiler dies tut.

Wenn Sie zögern, können Sie immer noch beides tun und vergleichen, ob es einen Leistungsgewinn gibt. Dies ist der einzig sichere Weg bis jetzt. Aber ich wette, der Unterschied wird vernachlässigbar sein, der Quellcode wird nur "lauter" sein.

dagnelies
quelle
3
Ich denke, der „Lärm“ ist hier der wichtigste Punkt. Halten Sie Ihren Code sauber und vertrauen Sie darauf, dass Ihr Compiler das Richtige tut, bis das Gegenteil bewiesen ist. Alles andere ist eine gefährliche vorzeitige Optimierung.
5gon12eder
1
Wenn Compiler so schlau sind, warum sollten sie dann versuchen, das Backfire des Compilers auszutricksen?
Little Endian
11
Compiler sind nicht schlau . Compiler machen nicht "das Richtige". Schreiben Sie Intelligenz nicht dort zu, wo sie nicht ist. In der Tat ist C # -Compiler / JITer übermäßig dumm. Zum Beispiel wird nichts über 32 Bytes IL oder Fälle mit structs als Parametern eingebunden - wo dies in vielen Fällen der Fall sein sollte und könnte. Neben dem Fehlen von Hunderten offensichtlicher Optimierungen, einschließlich, aber nicht beschränkt auf, das Vermeiden unnötiger Grenzüberprüfungen und Zuweisungen unter anderem.
JBeurer
4
@ DaveBlack Bounds überprüfen, ob die Auslöschung in C # in einer sehr kleinen Liste sehr grundlegender Fälle erfolgt, in der Regel in der grundlegendsten Folge für durchgeführte Schleifen, und selbst dann können viele einfache Schleifen nicht optimiert werden. Mehrdimensionale Array-Loops werden nicht entfernt, Loops, die in absteigender Reihenfolge durchlaufen werden, nicht, Loops auf neu zugewiesenen Arrays nicht. Sehr viele einfache Fälle, in denen man erwarten würde, dass der Compiler seinen Job macht. Aber das tut es nicht. Weil es alles andere als schlau ist. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer
3
Compiler sind keine "smarten Biester". Sie wenden einfach eine Reihe von Heuristiken an und machen Kompromisse, um ein Gleichgewicht für die meisten Szenarien zu finden, die von den Compiler-Autoren erwartet wurden. Ich schlage vor zu lesen: docs.microsoft.com/en-us/previous-versions/dotnet/articles/…
cdiggins
8

Sie haben Recht - es gibt keine Möglichkeit, zu garantieren, dass die Methode eingebettet ist - MSDN MethodImplOptions Enumeration , SO MethodImplOptions.AggressiveInlining vs TargetedPatchingOptOut .

Programmierer sind intelligenter als Compiler, aber wir arbeiten auf einer höheren Ebene und unsere Optimierungen sind Produkte der Arbeit eines Mannes - unserer eigenen. Jitter sieht, was während der Hinrichtung passiert. Es kann sowohl den Ausführungsfluss als auch den Code nach den Kenntnissen seiner Designer analysieren. Sie können Ihr Programm besser kennen, aber sie kennen die CLR besser. Und wer wird in seinen Optimierungen korrekter sein? Wir wissen es nicht genau.

Aus diesem Grund sollten Sie alle vorgenommenen Optimierungen testen. Auch wenn es sehr einfach ist. Beachten Sie auch, dass sich die Umgebung ändern kann und Ihre Optimierung oder Desoptimierung zu einem unerwarteten Ergebnis führen kann.

Eugene Podskal
quelle
8

EDIT: Mir ist klar, dass meine Antwort die Frage nicht genau beantwortet hat, obwohl es keinen wirklichen Nachteil gibt, gibt es nach meinen Timing-Ergebnissen auch keinen wirklichen Vorteil. Die Differenz zwischen einem Inline-Eigenschafts-Getter und 500 Millionen Iterationen beträgt 0,002 Sekunden. Mein Testfall ist möglicherweise auch nicht 100% ig genau, da er eine Struktur verwendet, da der Jitter und das Inlining mit Strukturen einige Vorbehalte aufweisen.

Wie immer ist der einzige Weg, es wirklich zu wissen, einen Test zu schreiben und ihn herauszufinden. Hier sind meine Ergebnisse mit der folgenden Konfiguration:

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

Leeres Projekt mit folgenden Einstellungen:

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

Ergebnisse

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

Getestet mit diesem Code:

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
quelle
5

Compiler führen viele Optimierungen durch. Inlining ist eine davon, ob der Programmierer es wollte oder nicht. Zum Beispiel hat MethodImplOptions keine "Inline" -Option. Weil das Inlining vom Compiler bei Bedarf automatisch durchgeführt wird.

Viele andere Optimierungen werden vor allem dann durchgeführt, wenn sie über die Build-Optionen aktiviert werden. Andernfalls wird dies im "Release" -Modus ausgeführt. Aber diese Optimierungen sind sozusagen "für Sie gearbeitet, großartig! Nicht funktioniert, lassen Sie es" -Optimierungen und bieten normalerweise eine bessere Leistung.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

ist nur ein Flag für den Compiler, dass hier eine Inlining-Operation wirklich gewünscht ist. Mehr Infos hier und hier

Zur Beantwortung Ihrer Frage;

Es gibt keine Garantie dafür, dass das JIT es ansonsten inline setzen würde. Liege ich falsch?

Wahr. Keine Garantie; Weder C # hat eine Option "Inlining erzwingen".

Könnte so etwas die Leistung / Stabilität / irgendetwas beeinträchtigen?

In diesem Fall nein, wie unter Schreiben von verwalteten Hochleistungsanwendungen beschrieben: Ein Primer

Get- und Set-Methoden für Eigenschaften sind im Allgemeinen gute Kandidaten für Inlining, da sie in der Regel nur private Datenelemente initialisieren.

myuce
quelle
1
Es wird erwartet, dass Antworten die Frage vollständig beantworten. Dies ist zwar ein Anfang für eine Antwort, geht jedoch nicht in die Tiefe, die für eine Antwort erwartet wird.
1
Aktualisiert meine Antwort. Hoffe, es wird helfen.
myuce