Accelerator, la suite…

Retour sur l’article précédent

A la fin du billet précédent, l’exemple de Microsoft fonctionnait correctement, et on avait vu comment l’algorithme était réalisé par le programme. Le but ici est de recréer le même programme en C#, afin de voir si les performances sont vraiment au rendez-vous.

Pour rappel, le programme en F# était le suivant :

open System
open Microsoft.ParallelArrays

[<EntryPoint>]
let main(args) = 
    let rawSize = 4000
    let filterSize = 5
    let random = Random()

//    let rawData = [| for i in 1 .. rawSize -> float32 (random.NextDouble() * float (random.Next(1, 100))) |]
    let rawData = [| for i in 1 .. rawSize -> float32 (i) |]

    use rawArray = new FloatParallelArray(rawData) 
    let filter = [1.0f .. float32 filterSize] 
    let rec expr i = let e = filter.[i] * ParallelArrays.Shift(rawArray, i)
                     if i = 0 then
                       e
                     else
                       e + expr (i-1)

    use dx9Target = new DX9Target()
    let debut = System.DateTime.Now
    let resDX = dx9Target.ToArray1D(expr (filterSize-1));

    printfn “F# Accelerator DX9”
    printfn “%A” resDX.[0 .. 9]
    printfn “Temps passé : %A” (DateTime.Now – debut)

    0

Equivalent en C#

Voici le code équivalent en C#. On pourra objecter qu’on doit pouvoir optimiser le code ci-dessous, mais on pourrait dire la même chose pour le code F# auquel on va le comparer. J’ai essayé de ne pas avoir trop de duplication des array, en accomplissant le plus possible de transformation directement sur l’array, sans le cloner.

using System;
using System.Diagnostics;

namespace TestPerfConvolutionPourComparaisonAccelerator
{
    class Program
    {
        static void Main(string[] args)
        {
            int rawSize = 8000;
            int filterSize = 800;

            Random random = new Random();
   
            double[] rawData = new double[rawSize];
            for (int index = 0; index < rawData.Length; index++)
                rawData[index] = (double)(index + 1);
//                rawData[index] = random.NextDouble() * random.Next(1, 100);

            int[] filter = new int[filterSize];
            for (int index = 0; index < filter.Length; index++)
                filter[index] = index + 1;

            Stopwatch Chrono = Stopwatch.StartNew();
            double[] resultat = ExprRecursive(rawData, filter, filterSize – 1);
            Chrono.Stop();

            Console.WriteLine(“C# sur CLR.NET”);
            Console.Write(“[“);
            for (int index = 0; index < Math.Min(resultat.Length, 10); index++)
                Console.Write(” {0:F1}f;”, resultat[index]);
            Console.WriteLine(” ]”);
            Console.WriteLine(“Temps passé : ” + Chrono.Elapsed.ToString());
        }

        private static void MultiplierArray(double[] liste, int multiplicateur)
        {
            for (int index = 0; index < liste.Length; index++)
                liste[index] *= multiplicateur;
        }

        private static void AjouterArray(double[] cible, double[] ajout)
        {
            for (int index = 0; index < cible.Length; index++)
                cible[index] += ajout[index];
        }

        private static void DecalageArray(double[] cible, int offset)
        {
            for (int index = 0; index < cible.Length; index++)
                cible[index] = index + offset < cible.Length ? cible[index + offset] : cible[cible.Length – 1];
        }

        private static double[] ExprRecursive(double[] rawData, int[] filter, int index)
        {
            double[] e = (double[])rawData.Clone();
            DecalageArray(e, index);
            MultiplierArray(e, filter[index]);
            if (index > 0) AjouterArray(e, ExprRecursive(rawData, filter, index – 1));
            return e;
        }
    }
}

Premiers essais

Avec une taille d’array de 4000 et une taille de filtre égale à 5, on obtient les résultats suivants en F# avec Accelerator, en mode DirectX 9 :

image

Voici ce que ça donne en C# :

image

Avant de parler de performance, on vérifie que les résultats sont bien les mêmes. Oui, je me suis déjà fait avoir, à réaliser des comparaisons de performances entre deux méthodes dont une ne faisait que la moitié du travail, et à devoir tout refaire. Ici, pas de souci, on est bien sur les mêmes nombres, et ça se confirmera avec les autres cas de calcul ci-dessous.

Maintenant, côté performances, beaucoup de différence, mais pas dans le sens où on l’attendait : C# est beaucoup plus rapide… Par contre, dans l’absolu, il s’agit de calculs très rapides, donc ça ne veut pas dire grand chose. Il faudrait gonfler la taille de l’array de donnée et celle du filtre pour voir comment ça évolue.

Montée en charge

On essaie de taper très haut dès le début, en passant à 40 000 au lieu de 4 000 pour la taille du tableau de donnée. Problème : on plante en F# !

image

On a peut-être poussé un peu fort. Voyons un peu ce que ça donne en C# :

image

Bon, on s’éloigne de ce qu’on cherchait à démontrer… Essayons en modifiant l’autre paramètre sur lequel on peut jouer, à savoir la taille du filtre, qu’on va passer à 1000, tout en ramenant la taille de l’array de données à 4 000. En F#, on a désormais un autre problème, de dépassement de pile, ce coup-ci :

image

En C#, ça passe toujours bien, et avec des temps pas dégradés du tout :

image

Restons dans des tailles faibles

Je ne vais pas me lancer dans du débogage pour savoir pourquoi ces données sont trop fortes. Pour l’instant, le but est de comparer les deux méthodes, donc on va plutôt prendre des couples de valeurs qui fonctionnent bien dans les deux langages.

Si on pousse au maximum, on peut aller sur du 8000 / 800. Dans ce cas, F# prend 1,86 secondes, tandis que C# prend à peine 0,33 secondes, soit six fois moins !

image

image

Du coup, pas la peine de se casser la tête à faire des courbes de comparaison en fonction des tailles : il semble que quelles que soient les valeurs, C# garde un fort avantage sur F# pour cet exemple.

<minuteDesproges>Attention, ne me faites pas dire ce que je n’ai pas dit !</minuteDesproges> : il y a plein d’exemples, dont certains démontrés récemment aux TechDays, montrant qu’on a bien une forte amélioration de performances. La question est de savoir pourquoi ce cas de test ne montre pas du tout cette amélioration. Il se pourrait que la surcharge de mise en place du target soit telle que, sur des opérations de l’ordre de la seconde, on perde plus de temps qu’on en gagne.

Conclusion à ce jour

Bref, l’étape suivante va être de trouver un exemple un peu plus costaud pour réellement faire travailler en profondeur les algorithmes. Mais il faut bien se rendre à l’évidence que pour ce type de calcul, C# est bien plus efficace que F# avec Accelerator. Ca me chiffonne un peu, mais je ne vais pas, contrairement à ce que font certains sur le climat, modifier mes données dans l’Allégresse 😉 pour qu’elles collent à ce que je veux démontrer…

Je pense qu’il faut tout simplement que je me documente plus sur Accelerator, et que je recommence ce travail de décorticage sur un autre exemple qui a été démontré comme allant plus vite en F# qu’en C#. Je suis tombé sur Internet sur un exemple avec le jeu de la vie, et c’est un des cas qui a été présenté aux TechDays, bref un bon candidat pour mon besoin. Si ça marche, ça devrait également me donner un peu plus de bille pour comprendre pourquoi l’exemple montré ici est moins performant en mode Accelerator qu’avec du code C#. Quoi qu’il en soit, je vous tiendrai au courant sur ce blog.

Une autre piste : si on prend des tailles de données et de filtre très petites (10 et 3), on reste à 0,22 secondes en F#, donc il y a bien une part incompressible d’initialisation du moteur, mais si on retranche des 1,86 secondes sur l’exemple avec le maximum de charge, il reste tout de même un facteur de 5 avec C# !

Un dernier truc

Je ne me lasse pas d’admirer F#, et en particulier, comme il devient rapidement intuitif. Je sais, au début ça paraît bizarre, mais on se prend vite à essayer des choses. Exemple : de la même manière que j’affichais seulement les 10 premières entrées du résultat avec le code C# suivant, je voulais faire pareil en F#.

for (int index = 0; index < Math.Min(resultat.Length, 10); index++)
                Console.Write(” {0:F1}f;”, resultat[index]);

Je savais que pour retrouver un élément d’un array, il fallait utiliser un point suivi de la notation avec des crochets. Par exemple, avec les paramètres à 8000 et 800, printfn “%A” resultat.[0] renvoyait 170986496.0f.

Et comme je le faisais remarquer dans le billet précédent, on peut utiliser la syntaxe a .. b pour retrouver tous les éléments entre a et b. Par exemple, on chargeait le filtre avec def filter = [1.0f .. 5.0f], ce qui le remplissait avec 5 éléments à savoir 1.0f, 2.0f, 3.0f, 4.0f et 5.0f. Ce qui est déjà génial…

Maintenant, si on mélange les deux, F# ne va pas comprendre… Les deux syntaxes sont certainement incompatibles… Et bien non ! Si on écrit printfn “%A” resultat.[0 .. 9], F# va bien afficher dans la console les 10 premières entrées de la liste !!! C’est tout de même rare qu’un langage anticipe à ce point ce que l’utilisateur veut lui faire faire. Du coup, je viens de comprendre pourquoi on est obligé de mettre un point avant d’utiliser l’indexeur. Ce n’est pas un manque de ‘sucre syntaxique’, mais bien que le point est en lui-même une méthode prenant en compte la suite comme un paramètre analysé de manière intelligente. Chapeau, F#…

[Edit]

Pingback de Sébastien Courtois :

http://sebastiencourtois.wordpress.com/2010/05/08/ms-research-devlabs-accelerator-v2-exploiter-vos-gpu-facilement-en-net/

About JP Gouigoux

Jean-Philippe Gouigoux est Architecte Logiciel, MVP Connected Systems Developer. Il intervient régulièrement à l'Université de Bretagne Sud ainsi qu'à l'Agile Tour. Plus de détails sur la page "Curriculum Vitae" de ce blog.
This entry was posted in .NET, F#, GPGPU and tagged . Bookmark the permalink.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha Captcha Reload