32 votes

Quels conseils d'optimisation puis-je donner au compilateur/JIT ?

J'ai déjà établi un profil, et je cherche maintenant à tirer le maximum de performances de mon point chaud.

Je suis au courant [MethodImplOptions.AggressiveInlining] et le Classe ProfileOptimization . Y en a-t-il d'autres ?


[Edit] Je viens de découvrir [TargetedPatchingOptOut] également. Pas grave, apparemment cette dernière n'est pas nécessaire .

31voto

Hans Passant Points 475940

Vous avez épuisé les options ajoutées dans .NET 4.5 pour agir directement sur le code éjecté. L'étape suivante consiste à examiner le code machine généré pour repérer toute inefficacité évidente. Pour ce faire, utilisez le débogueur, en l'empêchant d'abord de désactiver l'optimiseur. Outils + Options, Débogage, Général, décochez l'option "Supprimer l'optimisation JIT au chargement du module". Définissez un point d'arrêt sur le code chaud, Debug + Disassembly pour l'examiner.

Il n'y en a pas tant que ça à prendre en compte, l'optimiseur de gigue fait en général un excellent travail. Une chose à rechercher est l'échec des tentatives d'élimination de la vérification des limites d'un tableau, la fonction Correction de Le mot-clé est une solution de contournement peu sûre pour cela. Un cas particulier est une tentative ratée d'inlining d'une méthode et le jitter n'utilisant pas efficacement les registres du processeur, un problème avec le jitter x86 et corrigé avec MethodImplOptions.NoInlining. L'optimiseur n'est pas très efficace pour extraire le code invariant d'une boucle, mais c'est une chose que l'on considère presque toujours en premier lieu lorsqu'on regarde le code C# et qu'on cherche des moyens de l'optimiser.

La chose la plus importante à vouloir savoir est quand vous êtes fait et ne peut pas espérer aller plus vite. Vous ne pouvez vraiment y arriver qu'en comparant des pommes et des oranges et en écrivant le code chaud en code natif en utilisant C++/CLI. Assurez-vous que ce code est compilé avec #pragma unmanaged en vigueur afin qu'il bénéficie de tout l'amour de l'optimiseur. Il y a un coût associé au passage du code géré à l'exécution du code natif, alors assurez-vous que le temps d'exécution du code natif est suffisamment important. Sinon, ce n'est pas forcément facile à faire et vous n'aurez certainement pas de garantie de succès. Toutefois, le fait de savoir que vous avez terminé peut vous faire gagner beaucoup de temps en vous engageant dans des voies sans issue.

31voto

atlaste Points 4658

Oui, il y a d'autres astuces :-)

En fait, j'ai fait pas mal de recherches sur l'optimisation du code C#. Jusqu'à présent, voici les résultats les plus significatifs :

  1. Les fonctions et les actions qui sont transmises directement sont souvent intégrées par le JIT'ter. Notez que vous ne devez pas les stocker en tant que variables, car ils sont ensuite appelés en tant que délégués. Voir aussi ce poste pour plus de détails.
  2. Faites attention aux surcharges. Appeler Equals sans utiliser IEquatable<T> est généralement un mauvais plan - donc si vous utilisez, par exemple, un hachage, assurez-vous d'implémenter les bonnes surcharges et interfaces, car cela vous permettra de gagner beaucoup en performance.
  3. Les génériques appelés depuis d'autres classes sont jamais en ligne. La raison en est que la "magie" décrite aquí .
  4. Si vous utilisez une structure de données, essayez d'utiliser un tableau à la place :-) Vraiment, ces choses sont rapides comme l'enfer par rapport à ... eh bien, à peu près tout, je suppose. J'ai optimisé pas mal de choses en utilisant mes propres tables de hachage et en utilisant des tableaux au lieu de listes.
  5. Dans de nombreux cas, la consultation de tables est plus rapide que le calcul ou l'utilisation de constructions telles que la consultation de vtable, les commutateurs, les instructions if multiples et même les calculs. C'est aussi une bonne astuce si vous avez des branches ; une prédiction de branche ratée peut souvent devenir un gros problème. Voir aussi ce poste - C'est une astuce que j'utilise souvent en C# et qui fonctionne très bien dans de nombreux cas. Oh, et les tables de consultation sont des tableaux, bien sûr.
  6. Expérimentez la création de (petites) classes structes. En raison de la nature des types de valeurs, certaines optimisations sont différentes pour les struct que pour les class. Par exemple, les appels de méthode sont plus simples, parce que le compilateur sait exactement quelle méthode va être appelée. De même, les tableaux de structures sont généralement plus rapides que les tableaux de classes, car ils nécessitent une opération mémoire de moins par opération de tableau.
  7. N'utilisez pas de tableaux multidimensionnels. Bien que je préfère Foo[] même Foo[][] est normalement plus rapide que Foo[,] .
  8. Si vous copiez des données, préférez Buffer.BlockCopy à Array.Copy tous les jours de la semaine. Soyez également prudent avec les chaînes de caractères : les opérations sur les chaînes de caractères peuvent réduire les performances.

Il existait également un guide intitulé "Optimisation pour le processeur Intel Pentium" contenant un grand nombre d'astuces (comme le décalage ou la multiplication au lieu de la division). Bien que le compilateur fasse un bel effort de nos jours, cela aide aussi parfois un peu.

Bien entendu, il ne s'agit que d'optimisations ; les gains de performance les plus importants résultent généralement d'une modification de l'algorithme et/ou de la structure des données. Assurez-vous de vérifier les options qui vous sont offertes et ne vous limitez pas trop au cadre .NET... j'ai aussi une tendance naturelle à me méfier de l'implémentation .NET jusqu'à ce que j'aie vérifié le code décompilé par moi-même... il y a une tonne de choses qui auraient pu être implémentées beaucoup plus rapidement (la plupart du temps pour de bonnes raisons).

HTH


Alex m'a fait remarquer que Array.Copy est en fait plus rapide selon certaines personnes. Et comme je ne sais pas vraiment ce qui a changé au fil des ans, j'ai décidé que la seule façon de procéder était de créer un nouveau benchmark et de le mettre à l'épreuve.

Si vous êtes juste intéressé par les résultats, allez-y. Dans la plupart des cas, l'appel à Buffer.BlockCopy surpasse clairement les performances Array.Copy . Testé sur un Intel Skylake avec 16 Go de mémoire (>10 Go libres) sur .NET 4.5.2.

Code :

static void TestNonOverlapped1(int K)
{
    long total = 1000000000;
    long iter = total / K;
    byte[] tmp = new byte[K];
    byte[] tmp2 = new byte[K];
    for (long i = 0; i < iter; ++i)
    {
        Array.Copy(tmp, tmp2, K);
    }
}

static void TestNonOverlapped2(int K)
{
    long total = 1000000000;
    long iter = total / K;
    byte[] tmp = new byte[K];
    byte[] tmp2 = new byte[K];
    for (long i = 0; i < iter; ++i)
    {
        Buffer.BlockCopy(tmp, 0, tmp2, 0, K);
    }
}

static void TestOverlapped1(int K)
{
    long total = 1000000000;
    long iter = total / K;
    byte[] tmp = new byte[K + 16];
    for (long i = 0; i < iter; ++i)
    {
        Array.Copy(tmp, 0, tmp, 16, K);
    }
}

static void TestOverlapped2(int K)
{
    long total = 1000000000;
    long iter = total / K;
    byte[] tmp = new byte[K + 16];
    for (long i = 0; i < iter; ++i)
    {
        Buffer.BlockCopy(tmp, 0, tmp, 16, K);
    }
}

static void Main(string[] args)
{
    for (int i = 0; i < 10; ++i)
    {
        int N = 16 << i;

        Console.WriteLine("Block size: {0} bytes", N);

        Stopwatch sw = Stopwatch.StartNew();

        {
            sw.Restart();
            TestNonOverlapped1(N);

            Console.WriteLine("Non-overlapped Array.Copy: {0:0.00} ms", sw.Elapsed.TotalMilliseconds);
            GC.Collect(GC.MaxGeneration);
            GC.WaitForFullGCComplete();
        }

        {
            sw.Restart();
            TestNonOverlapped2(N);

            Console.WriteLine("Non-overlapped Buffer.BlockCopy: {0:0.00} ms", sw.Elapsed.TotalMilliseconds);
            GC.Collect(GC.MaxGeneration);
            GC.WaitForFullGCComplete();
        }

        {
            sw.Restart();
            TestOverlapped1(N);

            Console.WriteLine("Overlapped Array.Copy: {0:0.00} ms", sw.Elapsed.TotalMilliseconds);
            GC.Collect(GC.MaxGeneration);
            GC.WaitForFullGCComplete();
        }

        {
            sw.Restart();
            TestOverlapped2(N);

            Console.WriteLine("Overlapped Buffer.BlockCopy: {0:0.00} ms", sw.Elapsed.TotalMilliseconds);
            GC.Collect(GC.MaxGeneration);
            GC.WaitForFullGCComplete();
        }

        Console.WriteLine("-------------------------");
    }

    Console.ReadLine();
}

Résultats sur x86 JIT :

Block size: 16 bytes
Non-overlapped Array.Copy: 4267.52 ms
Non-overlapped Buffer.BlockCopy: 2887.05 ms
Overlapped Array.Copy: 3305.01 ms
Overlapped Buffer.BlockCopy: 2670.18 ms
-------------------------
Block size: 32 bytes
Non-overlapped Array.Copy: 1327.55 ms
Non-overlapped Buffer.BlockCopy: 763.89 ms
Overlapped Array.Copy: 2334.91 ms
Overlapped Buffer.BlockCopy: 2158.49 ms
-------------------------
Block size: 64 bytes
Non-overlapped Array.Copy: 705.76 ms
Non-overlapped Buffer.BlockCopy: 390.63 ms
Overlapped Array.Copy: 1303.00 ms
Overlapped Buffer.BlockCopy: 1103.89 ms
-------------------------
Block size: 128 bytes
Non-overlapped Array.Copy: 361.18 ms
Non-overlapped Buffer.BlockCopy: 219.77 ms
Overlapped Array.Copy: 620.21 ms
Overlapped Buffer.BlockCopy: 577.20 ms
-------------------------
Block size: 256 bytes
Non-overlapped Array.Copy: 192.92 ms
Non-overlapped Buffer.BlockCopy: 108.71 ms
Overlapped Array.Copy: 347.63 ms
Overlapped Buffer.BlockCopy: 353.40 ms
-------------------------
Block size: 512 bytes
Non-overlapped Array.Copy: 104.69 ms
Non-overlapped Buffer.BlockCopy: 65.65 ms
Overlapped Array.Copy: 211.77 ms
Overlapped Buffer.BlockCopy: 202.94 ms
-------------------------
Block size: 1024 bytes
Non-overlapped Array.Copy: 52.93 ms
Non-overlapped Buffer.BlockCopy: 38.84 ms
Overlapped Array.Copy: 144.39 ms
Overlapped Buffer.BlockCopy: 154.09 ms
-------------------------
Block size: 2048 bytes
Non-overlapped Array.Copy: 45.64 ms
Non-overlapped Buffer.BlockCopy: 30.11 ms
Overlapped Array.Copy: 118.33 ms
Overlapped Buffer.BlockCopy: 109.16 ms
-------------------------
Block size: 4096 bytes
Non-overlapped Array.Copy: 30.93 ms
Non-overlapped Buffer.BlockCopy: 30.72 ms
Overlapped Array.Copy: 119.73 ms
Overlapped Buffer.BlockCopy: 104.66 ms
-------------------------
Block size: 8192 bytes
Non-overlapped Array.Copy: 30.37 ms
Non-overlapped Buffer.BlockCopy: 26.63 ms
Overlapped Array.Copy: 90.46 ms
Overlapped Buffer.BlockCopy: 87.40 ms
-------------------------

Résultats sur x64 JIT :

Block size: 16 bytes
Non-overlapped Array.Copy: 1252.71 ms
Non-overlapped Buffer.BlockCopy: 694.34 ms
Overlapped Array.Copy: 701.27 ms
Overlapped Buffer.BlockCopy: 573.34 ms
-------------------------
Block size: 32 bytes
Non-overlapped Array.Copy: 995.47 ms
Non-overlapped Buffer.BlockCopy: 654.70 ms
Overlapped Array.Copy: 398.48 ms
Overlapped Buffer.BlockCopy: 336.86 ms
-------------------------
Block size: 64 bytes
Non-overlapped Array.Copy: 498.86 ms
Non-overlapped Buffer.BlockCopy: 329.15 ms
Overlapped Array.Copy: 218.43 ms
Overlapped Buffer.BlockCopy: 179.95 ms
-------------------------
Block size: 128 bytes
Non-overlapped Array.Copy: 263.00 ms
Non-overlapped Buffer.BlockCopy: 196.71 ms
Overlapped Array.Copy: 137.21 ms
Overlapped Buffer.BlockCopy: 107.02 ms
-------------------------
Block size: 256 bytes
Non-overlapped Array.Copy: 144.31 ms
Non-overlapped Buffer.BlockCopy: 101.23 ms
Overlapped Array.Copy: 85.49 ms
Overlapped Buffer.BlockCopy: 69.30 ms
-------------------------
Block size: 512 bytes
Non-overlapped Array.Copy: 76.76 ms
Non-overlapped Buffer.BlockCopy: 55.31 ms
Overlapped Array.Copy: 61.99 ms
Overlapped Buffer.BlockCopy: 54.06 ms
-------------------------
Block size: 1024 bytes
Non-overlapped Array.Copy: 44.01 ms
Non-overlapped Buffer.BlockCopy: 33.30 ms
Overlapped Array.Copy: 53.13 ms
Overlapped Buffer.BlockCopy: 51.36 ms
-------------------------
Block size: 2048 bytes
Non-overlapped Array.Copy: 27.05 ms
Non-overlapped Buffer.BlockCopy: 25.57 ms
Overlapped Array.Copy: 46.86 ms
Overlapped Buffer.BlockCopy: 47.83 ms
-------------------------
Block size: 4096 bytes
Non-overlapped Array.Copy: 29.11 ms
Non-overlapped Buffer.BlockCopy: 25.12 ms
Overlapped Array.Copy: 45.05 ms
Overlapped Buffer.BlockCopy: 47.84 ms
-------------------------
Block size: 8192 bytes
Non-overlapped Array.Copy: 24.95 ms
Non-overlapped Buffer.BlockCopy: 21.52 ms
Overlapped Array.Copy: 43.81 ms
Overlapped Buffer.BlockCopy: 43.22 ms
-------------------------

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X