Cocoa.fr

Developpement Mac, Objective-C, Cocoa et Swift

Migration cocoa.fr

Un blog Mac qui garde l'esprit historique, avec une base moderne.

Theme modernise a partir de la palette d'origine. Architecture prete pour importer les anciens contenus Objective-C, Cocoa et Swift.

03 March 2009 Pas à pas

Générateur d’images fractales (6)

Nous continuons aujourd’hui l’optimisation de la génération de l’image.

Les bitmaps

J’ai vaguement expliqué ce qu’était une bitmap, me contentant de dire qu’il s’agissait d’une grille de pixels. Intéressons-nous à leur organisation en mémoire.

32 bits par pixel

Utiliser 32 bits pour stocker les composantes d’un pixel est des plus classiques:

Mode 32 bits/pixel

Chaque composante utilise un octet, et peut donc contenir une valeur de 0 à 255. Doser les quantités de rouge, de vert et de bleu permet de choisir la teinte du pixel; la composante alpha correspond à l’opacité du pixel.

256 niveaux de gris

Notre générateur utilise une bitmap en 256 niveaux de gris:

Mode 8 bits/pixel

C’est le même principe: la valeur de l’octet fournit la nuance; 0 correspond au noir et 255 au blanc, voilà pourquoi nous utilisons la ligne:

NSUInteger nuance = n * 255 / MAX_ITERATIONS;

pour déterminer la nuance du pixel calculé.

Organisation en mémoire

Voyons maintenant la relation entre les pixels et les coordonnées:

Bitmap en 256 niveaux de gris

Dans cet exemple, la bitmap mesure 400 pixels de large et 300 de haut. Ce qui est intéressant, c’est que les pixels se suivent en mémoire. Ainsi, si nous disposons de l’adresse à laquelle est stockée la bitmap, adresseBitmap:

  • Le pixel de coordonnées (0,0) se trouve à l’adresse adresseBitmap.
  • Le dernier pixel de la première ligne (399, 0) se trouve à l’adresse adresseBitmap + 399 (puisqu’un pixel prend exactement un octet).
  • Le premier pixel de la deuxième ligne (0, 1), se trouve à l’adresse adresseBitmap + 400.
  • etc.

En généralisant nous obtenons:

adressePixel = adresseBitmap + (400 * y) + x

ou en généralisant d’avantage:

adressePixel = adresseBitmap + (largeurBitmap * y) + x

Il nous suffit d’écrire la nuance à cette adresse pour modifier le point. Vous devez maintenant avoir une bonne idée du fonctionnement de la méthode -[NSBitmapImageRep setPixel:atX:y];

Row Bytes

À vrai dire, j’ai simplifié la figure précédente, en omettant un détail. Voici une figure plus juste:

Bitmap en 256 niveaux de gris + rowBytes

En effet, pour des raisons de performances, des octets inutilisés (row bytes) sont ajoutés à la fin de chaque ligne. Ils servent à aligner la bitmap en mémoire. On ne connaît pas leur nombre a priori: cela dépend de plusieurs paramètres.

Toujours est-il qu’il faut en tenir compte.

L’optimisation

Dans notre boucle de rendu, il ne nous reste plus grand chose que nous puissions améliorer si ce n’est cette ligne:

[bitmapRep setPixel:&nuance atX:x y:y];

Cette méthode est tout de même appelée un million de fois (pour notre rendu en 1000 x 1000 pixels). Les bénéfices attendus en écrivant directement dans la bitmap sont les suivants:

  • Ne plus appeler la méthode [setPixel:atX:y:]. Les appels de méthodes sont encore plus lents que les appels de fonctions. Nous ferons entre-autres l’économie de l’exécution de la fonction objc_msgSend_rtp().
  • La méthode [setPixel:atX:y:] est forcément plus compliquée qu’un écriture directe dans la bitmap, ne serait-ce que par son côté généraliste. De plus, si vous vous rappelez l’article précédent, Shark listait des appels aux méthodes -[NSBitmapImageRep _setCGImageRef:] -[NSBitmapImageRep getBitmapDataPlanes:] -[NSBitmapImageRep _acquireRetainesCGImageRef] Je pense que ceci va nous permettre de nous en passer.

Le code

Inverser les énumérations des x et y

À cause de la manière dont est stockée la bitmap, il est nécessaire les énumérations de x et y :

for(y = 0; y < hauteurBitmap; y++)
{
    for(x = 0; x < largeurBitmap; x++)
    {

                        c.reel += incX;
    }

    c.reel = premierPoint.reel;
    c.imag += incY;
}

Adresser la bitmap

Avant le calcul:

// Obtenir la bitmap
unsigned char* bitmapPtr = [bitmapRep bitmapData];

On demande l’adresse de la bitmap.

unsigned int rowBytes = [bitmapRep bytesPerRow] - largeurBitmap;

La méthode [bitmapRep bytesPerRow] renvoie le nombre d’octets utilisés pour stocker une ligne. Comme nous savons que largeurBitmap octets sont utiles, nous en déduisons le nombre d’octets d’alignement (rowBytes).

for(y = 0; y < hauteurBitmap; y++)
{
    for(x = 0; x < largeurBitmap; x++)
    {

                    …

        // Donner le niveau de gris au pixel
        *bitmapPtr = n * 255 / MAX_ITERATIONS;

Nous conservons la formule du calcul de la nuance, que nous écrivons dans la bitmap à l’adresse du pixel courant.

        bitmapPtr++;

Puis nous passons au pixel suivant.

        c.reel += incX;
    }

    c.reel = premierPoint.reel;
    c.imag += incY;
    bitmapPtr += rowBytes;  // Sauter les octets inutilisés en fin de ligne

À la fin de chaque ligne, nous sautons les octets inutilisés.

}

Résultat

50 images rendues en 12.864695 secondes.
Moyenne = 3.886606 images/s

Nous tournons aux alentours de 4 images/s. Je vous rappelle que la première mesure donnait 0,79 images/s… la vitesse a été multipliée par 5 !

Nous arrivons aux limites de ce que nous pouvons ainsi optimiser. Nous allons nous arrêter là.

Et si ce n’était que le début ?

Améliorer la vitesse demanderait maintenant de recourir à des astuces.

Voici une idée: vous pouvez remarquer que deux pixels qui se suivent sont très souvent de la même nuance. On pourrait ne calculer qu’un pixel sur deux:

  • Si le pixel n°3 n’est pas de la même nuance que le pixel n°1, alors on calcule le pixel 2
  • Sinon, on lui donne la nuance du pixel 3.

On ne manquerait que des variations brusques (<1 pixel), peu perceptibles.

Cependant, tout cela est sans intérêt, parce que la vraie façon d’améliorer la vitesse est de déléguer le calcul… à la carte graphique! Je crois que n’importe quelle carte est aujourd’hui capable de faire ce calcul en temps réel. Ce sera peut-être pour une prochaine fois (quand j’aurais appris à me servir d’OpenGL Shading Language). En attendant, l’application est utilisable. Nous allons pouvoir nous balader dans l’ensemble de Mandelbrot.

À bientôt pour la suite.

Le projet XCode complet à télécharger.

Renaud Pradenc Céroce.com

02 March 2009 Cocoa

En vrac

Aujourd’hui ça va du développement de jeu, aux livres en passant par des interviews de développeurs :

26 February 2009 Liens

L’actualité du développement web

Si vous suivez l’actualité Mac, vous avez certainement vu que Apple vient de rendre disponible une version bêta de Safari 4. Parmi les nouveautés, quelques une nous intéresse tout particulièrement :

Pour finir, je voudrais aussi signaler les actualités concernant Cappuccino, qui proposera d’ici peu un nouveau thème et une application web de type XCode pour développer des applications Objective-J / Cappuccino :

Vous trouverez ci-dessous la vidéo de Atlas, qui mérite d’être vue tellement on a l’impression d’avoir à faire à un XCode en ligne :

25 February 2009 Logiciel

Gérer une base de données MySQL

Lorsque l’on développe une application, et plus particulièrement dans le cadre d’une application web, on utilise souvent une base de données pour stockés les données de l’application. Une des solutions est d’utiliser phpMyAdmin qui est souvent installé par défaut sur les hébergements ou les packages permettant de faire du développement web, mais il faut avouer que son interface n’est pas toujours des plus facile à utiliser. L’alternative est de passer par un client MySQL installé sur votre ordinateur, c’est pour ça que je vais aujourd’hui vous en présenter quelques-uns :

  • Sequel Pro qui est utilisable à partir de Mac OS X 10.5 et qui est gratuit et sous licence GNU GPL.
  • Querious, un logiciel qui coûte 25$ et qui me semble plus facile à utiliser et avec une interface plus dans la philosophie Mac.
  • Navicat MySQL, qui coûte entre 79$ et 149$ et qui propose une version lite gratuite. Elle n’est pas la plus orienté Mac, mais elle propose la gestion des tunnels SSH ce qui est très pratique pour gérer les serveurs MySQL distants. C’est en ce qui me concerne le logiciel que j’ai choisi (la version lite). Il existe en plus une version pour PostgreSQL et une version pour Oracle si vous n’utilisez pas MySQL.

Edit : Un test de Querious et Sequel Pro est disponible sur MySQL Showdown: Querious vs. Sequel Pro

23 February 2009 Pas à pas

Générateur d’images fractales (5)

Nous étions restés la fois précédente sur une version un peu lente de notre générateur. Nous allons tenter d’améliorer cela.

Une mise au point s’impose

Je lis fréquemment des gens qui tiennent à peu près ce discours: “Les programmeurs ne tirent pas partie de la puissance des machines, s’ils programmaient en assembleur, les programmes iraient super vite. Ils programment comme des porcs, uniquement pour des raisons financières”.

Premièrement, les raisons financières restent de bonnes raisons. Deuxièmement, il faut toujours faire des compromis: programmer efficacement en assembleur est très long, exige des connaissances pointues et le code n’est absolument pas portable, ce qui pose problème quand un logiciel doit être maintenu pendant des années.

Mais surtout, troisièmement, le postulat que les programmes tout-assembleur seraient bien plus rapides est faux ! Dans une application habituelle, 90% du temps est passé dans 10% du code. Cela signifie qu’améliorer ces 10% du code va suffire à accélérer grandement le programme. La bonne stratégie est d’écrire de la façon la plus lisible possible et de n’optimiser que les parties les plus critiques.

L’optimisation… dans l’ordre

  • Choisissez les bons algorithmes C’est le moyen le plus sûr d’accélérer un programme. À quoi bon programmer au plus près de la machine s’il existe un algorithme plus efficace par son principe ?
  • Exploitez au mieux le matériel Utilisez tous les cœurs de votre micro-processeurs, déléguez les traitements à la carte graphique, profitez des instructions vectorielles
  • Sachez comment fonctionne votre Unix C’est le système d’exploitation qui gère les ressources de votre applications, en particulier la mémoire et les fichiers. Ses contraintes peuvent avoir un impact important sur les performances.
  • En dernier recours seulement, travaillez sur le bas niveau Dans certaines applications (jeux, calculs 3D), tailler le code au plus près du microprocesseur reste nécessaire.

Dans tous les cas: ME-SU-REZ ! Optimisez seulement après avoir localisé les sources de lenteurs: elles ne sont pas toujours évidentes. En outre, seules les mesures valident l’efficacité des optimisations.

Mesurons notre appli

Commençons donc par mesurer les temps d’exécution:

Mesure avec Shark

  • Sous Xcode, Choisissez l’article du menu Run > Start with Performance Tool > Shark.
  • Cliquez sur le bouton Start.
  • Une fenêtre apparaît (le chemin de notre exécutable est déjà réglé). Cliquez sur OK.
  • L’appli se lance. Redimensionnez la fenêtre continuellement pendant 30 secondes, jusqu’à ce que Shark prenne la main.

Shark avant optimisation

Le résultat n’est pas très surprenant, on trouve en tête nos fonctions qui génèrent l’image. Nous savons où attaquer: ces cinq premières méthodes totalisent plus de 80% du temps d’exécution.

Ensuite, on trouve objc_msgSend_rtp. Il s’agit de la fonction du runtime Objective-C qui permet d’envoyer des messages entre les objets. On peut éventuellement le réduire en envoyant moins de messages… À vrai dire, ce qui m’étonne sont les 2,7% de setPixel:atX:y:, je pensais que ce serait plus. Il me paraît aussi assez surprenant que _setCGImageRef: apparaisse si haut dans le classement. Voilà pourquoi vous devez mesurer: où les optimisations doivent être faîtes n’est pas toujours évident.

Principe de mesure de Shark Shark utilise une interruption. Chaque fois qu’elle est déclenchée (toutes les millisecondes, par défaut), Shark note quelle fonction ou méthode est en train d’être exécutée. À la fin, il n’a plus qu’à compter pour établir un classement. Notez qu’il s’agit d’une approche statistique: évaluer le même programme plusieurs fois ne donnera pas les mêmes résultats (les interruptions ne sont pas synchronisées avec le lancement du programme). Il faut admettre une marge d’environ 2%.

Une mesure absolue

L’inconvénient de la mesure avec Shark, c’est qu’elle nous fournit des proportions du temps passé. Ce qui serait intéressant serait d’avoir une mesure du temps de rendu pour mesurer l’amélioration. J’ai donc créé une nouvelle classe, CFMesurePerf pour cela:

#import "CFRMesurePerf.h"

#define IMAGES_A_RENDRE     50

Nous calculons la même image 50 fois de suite. Le temps d’exécution varie: notre programme ne tourne pas tout seul, il y a d’autres processus en parallèle, il faut du temps pour que le système décide de sortir du mode économie d’énergie, etc. . Calculer plusieurs fois permet de lisser les différences.

@implementation CFRMesurePerf

- (id) init
{
    if(self = [super init])
    {
        CFRMandelbrotRender* render = [[CFRMandelbrotRender alloc] init];
        [render setLargeurBitmap:1000];
        [render setHauteurBitmap:1000];

Nous créons un CFRMandelbrotRender. Il ne sera pas affiché à l’écran; ce qui nous intéresse est son temps de calcul. Nous demandons le calcul d’un million de points.

        NSDate* dateDepart = [NSDate date];

        // Rendre l'image IMAGES_A_RENDRE fois
        int image;
        for(image = 0; image < IMAGES_A_RENDRE; image++)
        {
            [render bitmapImageRep];
        }

        NSDate* dateFin = [NSDate date];

Nous notons la date de début, rendons les 50 images, puis notons la date de fin.

        float secondesEcoulees = [dateFin timeIntervalSinceDate:dateDepart];
        int imagesARendre = IMAGES_A_RENDRE;
        NSLog(@"%d images rendues en %f secondes.", imagesARendre, secondesEcoulees);
        NSLog(@"Moyenne = %f images/s", IMAGES_A_RENDRE/secondesEcoulees);

La différence entre les deux dates nous fournit la durée du calcul. Nous pouvons en déduire la moyenne.

    }

    return self;
}

@end

J’ai choisi d’instancier cette classe à partir de MyDocument.xib. Je vous laisse faire.

Dorénavant, lorsque le programme se lance, il va calculer 50 images. C’est assez long, soyez patients! Il faut plus d’une minute sur mon G5, avec une moyenne de 0,79 images/seconde.

Appels à complexeAvecCoordBitmapX:y:

Passons maintenant à une optimisation de l’algorithme. La méthode -[complexeAvecCoordBitmapX:y] est actuellement appelée pour chaque point. Ce n’est absolument pas nécessaire. Il nous suffit de calculer de combien il faut incrémenter les coordonnées pour passer d’un point à un autre du plan.

Ainsi: incrementX = (dernierPoint.reel - premierPoint.reel) / largeurBitmap; incrementY = (dernierPoint.imag - premierPoint.imag) / hauteurBitmap;

Nous obtenons alors la méthode de rendu suivante:

// Créer la bitmap
NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc]
    initWithBitmapDataPlanes:NULL
    pixelsWide:largeurBitmap
    pixelsHigh:hauteurBitmap
    bitsPerSample:8
    samplesPerPixel:1
    hasAlpha:NO
    isPlanar:NO
    colorSpaceName:NSDeviceWhiteColorSpace
    bytesPerRow:0
    bitsPerPixel:8];

// Déterminer les incréments des coordonnées
Complexe_t premierPoint, dernierPoint;
premierPoint = [self complexeAvecCoordBitmapX:0 y:0];
dernierPoint = [self complexeAvecCoordBitmapX:largeurBitmap-1 y:hauteurBitmap-1];

Calculer les coordonnées des points extrêmes se fait encore avec notre bonne vieille méthode.

double incX = (dernierPoint.reel - premierPoint.reel) / largeurBitmap;
double incY = (dernierPoint.imag - premierPoint.imag) / hauteurBitmap;

Voir la formule plus haut.

// Calculer l'ensemble de Mandelbrot:
// Parcourir tous les points de la bitmap
double x, y;
Complexe_t c = premierPoint;

c est maintenant initialisé avec le premier point.

for(x = 0; x < largeurBitmap; x++)
{
    for(y = 0; y < hauteurBitmap; y++)
    {
        // Initialiser z[0]
        Complexe_t z = {0.0, 0.0};

        NSUInteger n;
        for(n=0; n < MAX_ITERATIONS; n++)
        {
            // z[n+1] = z[n+1]^2 + c
            z = Additionner(Carre(z), c);

            // La suite diverge si |z| > 2
            if(ModuleAuCarre(z) > 4.0)
                break;
        }

        // Donner le niveau de gris au pixel
        NSUInteger nuance = n * 255 / MAX_ITERATIONS;
        [bitmapRep setPixel:&nuance atX:x y:y];

        c.imag += incY;

Nous incrémentons donc l’ordonnée ici.

    }

    c.imag = premierPoint.imag;
    c.reel += incX;

Il ne faut pas oublier de replacer l’ordonnée en haut du plan. Ensuite, nous incrémentons l’abscisse.

}

[bitmapRep autorelease];
return bitmapRep;

Au niveau des performances, j’atteins maintenant les 1,06 images/s, soit un gain de 34%. Pas mal !

Placer les fonctions mathématiques “en ligne”

En mettant un compteur dans la boucle for, vous sauriez qu’elle est exécutée 4 239 692 fois. Autant dire que tout ce qui s’y trouve est critique. Or, un appel de fonction réserve de la mémoire sur la pile et copie les paramètres. Il ne s’agit pas d’opérations particulièrement lourdes, mais quand on le fait 4 millions de fois, cela devient très significatif. Nous n’allons donc plus faire d’appels aux fonctions, mais les incorporer:

Complexe_t z = {0.0, 0.0};
Complexe_t zCarre;
NSUInteger n;
for(n=0; n < MAX_ITERATIONS; n++)
{
    // z[n+1] = z[n+1]^2 + c
        // Mettre z au carré
    zCarre.reel = z.reel*z.reel - z.imag*z.imag;
    zCarre.imag = 2.0 * z.reel * z.imag;
        // Ajouter c
    z.reel = zCarre.reel + c.reel;
    z.imag = zCarre.imag + c.imag;

    // La suite diverge si |z| > 2
    if( (z.reel*z.reel + z.imag*z.imag) > 4.0)
        break;
}

Le résultat est sans appel: il ne faut plus que 24 s pour rendre les 50 images, soit une moyenne de 2,06 images/s — quasiment deux fois plus vite. La méthode est par contre moins lisible: c’est habituel dès que l’on optimise. Il s’agit toujours d’un compromis entre la vitesse et la maintenabilité du code.

La suite

Il nous reste une dernière optimisation à faire, mais comme elle nécessite des explications, je m’arrête là pour cette fois. À bientôt.

Le projet XCode complet à télécharger.

Renaud Pradenc Céroce.com

23 February 2009 Liens

Les bureaux des développeurs

On parle souvent des machines et des logiciels que les développeurs utilisent pour faire leur travail, mais il ne faut pas oublier que l’environnement fait aussi beaucoup, que ce soit le bureau, la chaise ou de manière plus générale les locaux. Je vous invite donc à découvrir les bureaux de certaines entreprises du secteur :

(via MacGeneration)

23 February 2009 Livres

Succès des livres de développement Mac

O’Reilly vient de publier l’état du marché des livres informatiques pour l’année 2008, et parmis les livres concernant le développement, la plus grosse augmentation concerne le développement sur Mac qui est en augmentation de plus de 85%. La sortie du SDK iPhone et les nouvelles éditions de livres comme Programmation Cocoa sous Mac OS X ou la version originale en anglais (Cocoa Programming for Mac OS X) n’est certainement pas étrangère à cette augmentation.