Y a-t-il un inconvénient à utiliser AggressiveInlining sur des propriétés simples?

16

Je parie que je pourrais répondre moi-même si j'en savais plus sur les outils pour analyser le comportement de C # / JIT, mais comme je ne le fais pas, veuillez me le demander.

J'ai un code simple comme celui-ci:

    private SqlMetaData[] meta;

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

Comme vous pouvez le voir, j'ai mis AggressiveInlining parce que je pense qu'il devrait être intégré.
Je pense. Il n'y a aucune garantie que le JIT l'inclurait autrement. Ai-je tort?

Est-ce que faire ce genre de chose pourrait nuire aux performances / stabilité / quoi que ce soit?

Serge
la source
2
1) D'après mon expérience, ces méthodes primitives seront alignées sans l'attribut. J'ai principalement trouvé l'attribut utile avec des méthodes non triviales qui devraient encore être intégrées. 2) Il n'y a aucune garantie qu'une méthode décorée avec l'attribut sera également intégrée. C'est simplement un indice pour le JITter.
CodesInChaos
Je ne sais pas grand-chose sur le nouvel attribut inlining, mais en mettre un ici ne va certainement pas faire de différence dans les performances. Tout ce que vous faites, c'est renvoyer une référence à un tableau, et le JIT fera presque certainement déjà le bon choix ici.
Robert Harvey
14
3) Un alignement trop important signifie que le code devient plus grand et peut ne plus tenir dans les caches. Les échecs de cache peuvent être un impact significatif sur les performances. 4) Je recommande de ne pas utiliser l'attribut tant qu'un test de performance n'a pas montré qu'il améliore les performances.
CodesInChaos
4
Arrête de t'inquiéter. Plus vous essayez de déjouer le compilateur, plus il trouvera des moyens de vous déjouer. Trouvez autre chose à craindre.
david.pfx
1
Pour mes deux cents, j'ai vu de grands gains en mode de libération, en particulier lors de l'appel d'une fonction plus grande dans une boucle serrée.
jjxtra

Réponses:

22

Les compilateurs sont des bêtes intelligentes. Habituellement, ils extraient automatiquement autant de performances que possible de n'importe où.

Essayer de déjouer le compilateur ne fait généralement pas une grande différence et a beaucoup de chances de se retourner. Par exemple, l'inlining agrandit votre programme car il duplique le code partout. Si votre fonction est utilisée à de nombreux endroits dans le code, elle pourrait en fait être préjudiciable, comme l'a souligné @CodesInChaos. S'il est évident que la fonction doit être intégrée, vous pouvez parier que le compilateur le fera.

En cas d'hésitation, vous pouvez toujours faire les deux et comparer s'il y a un gain de performance, c'est le seul moyen certain pour l'instant. Mais mon pari est que la différence sera infaillible, le code source sera juste "plus bruyant".

dagnelies
la source
3
Je pense que le «bruit» est le point le plus important ici. Gardez votre code bien rangé et faites confiance à votre compilateur pour faire la bonne chose jusqu'à preuve du contraire. Tout le reste est une optimisation prématurée dangereuse.
5gon12eder
1
Si les compilateurs sont si intelligents, alors pourquoi essayer de déjouer le retour de flamme du compilateur?
Little Endian
11
Les compilateurs ne sont pas intelligents . Les compilateurs ne font pas «la bonne chose». N'attribuez pas l'intelligence là où elle n'est pas. En fait, le compilateur C # / JITer est excessivement stupide. Par exemple, il n'inclura rien au-delà de 32 octets IL ou des cas impliquant structs comme paramètres - où dans de nombreux cas, il devrait et pourrait. En plus de manquer des centaines d' optimisations évidentes - y compris, mais sans s'y limiter - éviter les vérifications et les allocations de limites inutiles, entre autres.
JBeurer
4
@DaveBlack Bounds vérifier l'élusion en C # se produit dans une très petite liste de cas très basiques, généralement sur la séquence la plus basique pour les boucles effectuées, et même alors, de nombreuses boucles simples ne sont pas optimisées. Les boucles de tableau multidimensionnelles n'obtiennent pas d'élimination de vérification des limites, les boucles itérées dans l'ordre décroissant ne le font pas, les boucles sur les tableaux nouvellement alloués ne le font pas. De très nombreux cas simples où vous vous attendez à ce que le compilateur fasse son travail. Mais ce n'est pas le cas. Parce que c'est n'importe quoi, mais intelligent. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer
3
Les compilateurs ne sont pas des «bêtes intelligentes». Ils appliquent simplement un tas d'heuristiques et font des compromis pour essayer de trouver un équilibre pour la majorité des scénarios anticipés par les auteurs du compilateur. Je suggère de lire: docs.microsoft.com/en-us/previous-versions/dotnet/articles/…
cdiggins
8

Vous avez raison - il n'y a aucun moyen de garantir que la méthode sera intégrée - Énumération MSDN MethodImplOptions , SO MethodImplOptions.AggressiveInlining vs TargetedPatchingOptOut .

Les programmeurs sont plus intelligents qu'un compilateur, mais nous travaillons à un niveau supérieur et nos optimisations sont le produit du travail d'un seul homme - le nôtre. Jitter voit ce qui se passe pendant l'exécution. Il peut analyser à la fois le flux d'exécution et le code selon les connaissances qui y sont apportées par ses concepteurs. Vous pouvez mieux connaître votre programme, mais ils connaissent mieux le CLR. Et qui sera plus correct dans ses optimisations? Nous ne savons pas avec certitude.

C'est pourquoi vous devriez tester toute optimisation que vous faites. Même si c'est très simple. Et tenez compte du fait que l'environnement peut changer et que votre optimisation ou désoptimisation peut avoir un résultat assez inattendu.

Eugene Podskal
la source
8

EDIT: Je me rends compte que ma réponse n'a pas répondu exactement à la question, alors qu'il n'y a pas de réel inconvénient, d'après mes résultats temporels, il n'y a pas de véritable avantage non plus. La différence entre un getter de propriété en ligne est de 0,002 seconde sur 500 millions d'itérations. Mon cas de test peut également ne pas être précis à 100% car il utilise une structure car il y a des mises en garde à la gigue et à l'inclusion de structures.

Comme toujours, la seule façon de vraiment savoir est d'écrire un test et de le comprendre. Voici mes résultats avec la configuration suivante:

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

Projet vide avec les paramètres suivants:

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

Résultats

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

Testé avec ce 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
la source
5

Les compilateurs font beaucoup d'optimisations. Inline est l'un d'entre eux, que le programmeur le veuille ou non. Par exemple, MethodImplOptions n'a pas d'option "en ligne". Parce que l'inline est fait automatiquement par le compilateur si nécessaire.

De nombreuses autres optimisations sont spécialement effectuées si elles sont activées à partir des options de construction, ou le mode "release" le fera. Mais ces optimisations sont en quelque sorte des optimisations "travaillées pour vous, génial! Non travaillées, laissez-le" et donnent généralement de meilleures performances.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

est juste un indicateur pour le compilateur qu'une opération d'inline est vraiment souhaitée ici. Plus d'infos ici et ici

Pour répondre à ta question;

Il n'y a aucune garantie que le JIT l'inclurait autrement. Ai-je tort?

Vrai. Aucune garantie; Aucun C # n'a une option "forcer l'inline".

Est-ce que faire ce genre de chose pourrait nuire aux performances / stabilité / quoi que ce soit?

Dans ce cas, non, comme il est dit dans Écriture d'applications gérées hautes performances: une introduction

Les méthodes d'obtention et de définition de propriétés sont généralement de bons candidats pour l'inlining, car elles ne font généralement qu'initialiser les membres de données privées.

myuce
la source
1
Il est prévu que les réponses répondent pleinement à la question. Bien qu'il s'agisse d'un début de réponse, cela n'entre pas vraiment dans la profondeur attendue d'une réponse.
1
Mis à jour ma réponse. J'espère que cela vous aidera.
myuce