Frédéric Mélantois
Manipulation d'images en C#, vers la performance
Nous chercherons à augmenter la performance de nos traitements via quelques petites « recettes »
Par Frédéric Mélantois publié le 01/02/2007 à 10:11, lu 8087 fois, 5 pages
 4 | Encore plus de performance
En matière de performance, où s'arrêter dans l'optimisation ? Doit-on avoir un comportement « jusqu'au boutiste » dans la recherche de performance, sachant que la plateforme .NET n'est pas la plus proche du langage machine ? Ce sont des questions que l'on peut effectivement se poser. Si on choisit la plateforme .NET, c'est pour ses avantages, en particulier, l'apparente facilité à développer rapidement et non pas pour la performance. Si on doit produire le code le plus performant possible sur cette plateforme, on ne doit pas perdre de vue la lisibilité, la réutilisabilité du code. En cela, on se doit d'être « jusqu'au boutiste » seulement si cela en vaut vraiment la peine. On portera en particulier attention aux instructions répétées, par exemple dans les boucles.
Eviter les calculs répétitifs dans une boucle par des tables de pré-calculs comme nous l'avons vu précédemment permet de réaliser un gain de performance non négligeable. Savoir éviter les opérations entre « float » ou « double » au lieu de simple « int » peuvent permettre de gagner un temps précieux. C'est ce que nous avons codé en multipliant par 256 les opérations de multiplication de nombres à virgule pour l'estimation du niveau de gris d'un pixel donné.
Les opérations de multiplication étant coûteuses, comment pouvons-nous les remplacer par un code équivalent plus performant ?

int* multiBleu = stackalloc int[256];

int* multiVert = stackalloc int[256];

int* multiRouge = stackalloc int[256];

 

for (int i = 0; i < 256; i++)

{

    multiBleu[i] = i * 76;

    multiVert[i] = i * 151;

    multiRouge[i] = i * 28;

    /* Par multiplication (Utilisation de Cordbg du SDK)

    [0077] imul        eax,esi,4Ch

    [007a] mov        edx,dword ptr [ebp-18h]

    [007d] mov        dword ptr [edx+esi*4],eax

    [0080] imul        eax,esi,97h

    [0086] mov        dword ptr [edi+esi*4],eax

    [0089] imul        eax,esi,1Ch

    [008c] mov        dword ptr [ebx+esi*4],eax

    */

}   

 

int R, V, B;

R = V = B = 0;

for (int i = 0; i < 256; i++)

{

    multiBleu[i] = B;

    multiVert[i] = V;

    multiRouge[i] = R;

    B += 76;

    V += 151;

    R += 28;

    /* Par addition (utilisation de Cordbg du SDK

    [008f] mov        eax,dword ptr [ebp-18h]

    [0092] mov        dword ptr [eax+esi*4],ebx

    [0095] mov        eax,dword ptr [ebp-1Ch]

    [0098] mov        dword ptr [eax+esi*4],edi

    [009b] mov        eax,dword ptr [ebp-20h]

    [009e] mov        edx,dword ptr [ebp-24h]

    [00a1] mov        dword ptr [eax+esi*4],edx

    [00a4] add        ebx,4Ch

    [00a7] add        edi,97h

    [00ad] add        dword ptr [ebp-24h],1Ch

    */

}

Le code précédent met en évidence les différences du code natif pour des codes similaires en c#. Si vous utilisez des outils pour tester la performance, le code utilisant les additions sera bien plus performant. Même si vous ne connaissez pas l'assembleur, vous remarquerez que les instructions « imul » sont « remplacées » par deux instructions « mov » et « add », celles-ci étant moins coûteuses en temps processeur. En clair, si nous transposons cette conclusion au traitement d'une images dont on parcours les pixels en multipliant leur valeur, le gain de temps représentera très vite de nombreuses millisecondes. Le temps gagné est alors fonction de la taille de l'image.
Bruno Nati (MVP C++ natif) alias Brunews n'a pas hésité à me suggérer d'autres améliorations, dont les gains en performance sont toutefois moindres par rapport à ce que je vous ai présenté jusqu'à maintenant. Voici un exemple :

int* multiBleu = stackalloc int[256];

                int* multiVert = stackalloc int[256];

                int* multiRouge = stackalloc int[256];

 

                int R, V, B, cpt;

                B = 19380;

                V = 38505;

                R = 7140;

                cpt = 255;

 

                do

                {

                    multiBleu[cpt] = B;

                    multiVert[cpt] = V;

                    multiRouge[cpt] = R;

                    B -= 76;

                    V -= 151;

                    R -= 28;

                }

                while (--cpt >= 0);

                /* Extrait de l'utilitaire Cordbg du SDK

                [008f] mov        eax,dword ptr [ebp-18h]

                [0092] mov        dword ptr [eax+esi*4],edi

                [0095] mov        eax,dword ptr [ebp-1Ch]

                [0098] mov        edx,dword ptr [ebp-24h]

                [009b] mov        dword ptr [eax+esi*4],edx

                [009e] mov        eax,dword ptr [ebp-20h]

                [00a1] mov        dword ptr [eax+esi*4],ebx

                [00a4] add        edi,0FFFFFFB4h

                [00a7] add        dword ptr [ebp-24h],0FFFFFF69h

                [00ae] add        ebx,0FFFFFFE4h

                [00b1] dec        esi

                [00b2] jns        FFFFFFDD               

                */

L'idée avec ce code est de faire tendre vers zéro et éviter la comparaison avec 255 via l'instruction « cmp » en utilisant la boucle « for » en c#. On gagne donc une instruction par boucle ce qui permet un gain de temps. Que se passe t-il, si vous remplacez la condition par :

while (cpt-- >= 0) ;

/* Extrait de l'utilitaire Cordbg du SDK

[008f] mov        eax,dword ptr [ebp-18h]

[0092] mov        dword ptr [eax+esi*4],edi

[0095] mov        eax,dword ptr [ebp-1Ch]

[0098] mov        dword ptr [eax+esi*4],ebx

[009b] mov        eax,dword ptr [ebp-20h]

[009e] mov        edx,dword ptr [ebp-24h]

[00a1] mov        dword ptr [eax+esi*4],edx

[00a4] add        edi,0FFFFFFB4h

[00a7] add        ebx,0FFFFFF69h

[00ad] add        dword ptr [ebp-24h],0FFFFFFE4h

[00b1] mov        dword ptr [ebp-28h],esi

[00b4] dec        esi

[00b5] cmp        dword ptr [ebp-28h],0

[00b9] jge        FFFFFFD6         

*/

Le changement de la condition montre très clairement que cela a une importance capitale dans le nombre d'instructions assembleur générées. L'instruction « cmp » est répétée 255 fois en plus par rapport au précédent code. A vous de voir jusqu'où vous voulez aller dans votre quête de la performance. La lisibilté du code peut être aussi un aspect important de nos choix. Les développeurs C, habitués à travailler très proche de la machine, n'auront auqu'un mal à réaliser des optimisations. Concernant, l'utilisation d'algorithme performant, je vous suggère de regarder cet ebook que je consulte régulièrement Algorithms for programmers.
 
» Démarrer une discussion
 
Discussion démarée par Frédéric Mélantois le 07/05/2007 à 11:28, 1 commentaire(s).
Discussion démarée par Frédéric Mélantois le 07/05/2007 à 11:21, 1 commentaire(s).