Employee of the Month

Employee of the Month

Mike Chambers a organisé une compétition de programmation AS3 la semaine dernière : AS3DTC_1. J’aime bien les concours simples qui ne prennent pas trop de temps. Monumentale erreur : j’ai du écrire beaucoup de code.

Du coup mes estimés collègues m’ont demandé d’écrire ce post sur la manière que j’ai utilisée pour prendre la 1ère place (yaisse !), en profiter pour en faire un cours sur l’optimisation, tout ça quoi …

L’objet du concours était d’avoir une scène découpée en cases (une grille donc), avec dessus un grand nombre de points (10000) et 4 objets à tester. Pour chacun des objets à tester, il est demandé de renvoyer les points voisins (points présents dans la case de l’objet et les 8 cases adjacentes). Grant Skinner a posté un exemple un peu plus visuel de l’interêt du problème à résoudre :

(Either JavaScript is not active or you are using an old version of Adobe Flash Player. Please install the newest Flash Player.)

A chaque appel (il faut imaginer à chaque frame), une liste complète des points est envoyée via un appel à une méthode update(). Ensuite pour chaque objet à tester les voisins sont demandés via getNeighbours(). Les participations sont départagées par leur vitesse d’exécution, donc il faut faire le code le plus speed possible.

update() est donc appelé une seule fois contre plusieurs pour getNeighbours(), et toujours avant, il faut donc effectuer un maximum de calculs dans update().

L’autre idée primordiale est de ne faire qu’une seule boucle sur les 10000 points.

L’implémentation la plus évidente à mes yeux (mais pas forcement la meilleure, il y a eu des méthodes innovantes à ce niveau, en particulier celle de Grant Skinner), est la suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cases = tableau[nombre_identifiant][points]
 
function update(points)
{
	for(point in points)
	{
		cases[identifiant_de_case].push( point )
	}
}
 
function getNeighbours(test)
{
	case_centrale = cases[identifiant_de_case]
	return case_centrale.concat( cases_voisines_1...8 )
}

Le calcul d’identifiant_de_case est effectué plus de 10000 fois, c’est le principal élément à optimiser. Dans sa forme la plus basique, on veut quelque chose du type :

1
identifiant_de_case = caseX * hauteurGrille + caseY

avec :

1
caseX = uint(Math.round(point.x/tailleGrille)) // et pareil pour caseY

>> on optimise ce bout de code et on obtient :

1
2
3
4
// les multiplications sont plus rapides que les divisions (à effectuer une seule fois, dans le constructeur)
ratioTailleGrille = 1/tailleGrille;
[...]
caseX = int(point.x*ratioTailleGrille)

Pour optimiser encore plus, il est possible d’utiliser des opérateurs sur les bits (les opérations au niveau bit sont très rapides). Rappel: le but de créer un identifiant unique pour chaque case : on cherche donc à obtenir un int qui contient en même temps caseX et caseY.

Pour expliquer la méthode, voir le code d’exemple suivant :

1
2
3
4
5
6
7
8
9
caseX = 12;    // en binaire : 00001100
caseY = 19;    // en binaire : 00010011
// on décale vers la gauche les bits de caseX pour laisser assez de place à caseY
// ici 8 bits (dans ma classe j'ai poussé le vice à calculer le nombre de bits nécessaires)
// puis on utilise le OU binaire ( | ) pour fusionner les 2 valeurs
identifiant_de_case =
		(caseX << 8)		// en binaire : 0001001100000000
			| caseY		// en binaire :         00010011
// identifiant_de_case =		   en binaire : 0010011000010011

Le stockage des items en lui même se fait dans un Vecteur à 2 dimensions :

1
var cases:Vector.<Vector.<DisplayObject>>

Un objet (ou Dictionary) liant chaque identifiant à son Vector.<DisplayObject> serait un choix plus logique pour le stockage, mais comme les valeur ne sont pas fortement typées dans un Object, c’est au final moins rapide.

Beaucoup d’autres optimisations sont connues (cf. wiki @joa)

  • remplacer les divisions par des multiplications tant que possible
  • for(i=0;i<n;i++) est moins efficace que i=n; while(i) { –i }
  • vector.push(item) est moins efficace que vector[vector.length]=item;
  • il faut fortement typer un objet avant d’appeler une méthode dessus. Par exemple :
    1
    2
    3
    4
    
    return cases[id1].concat(cases[id2])
    // est moins efficace que :
    var case:Vector.<DisplayObject> = cases[id1];
    return case.concat(cases[id2]);

Autre feinte :
Dans getNeighbours(), normalement il faudrait tester si on est sur les bords, pour savoir si il faut concaténer 9 cases ou moins.
Pour éviter ça, on crée une bande de cases vides autour de la grille réelle. Malheureusement il n’y a pas d’index négatif (pour la ligne du haut et la colonne de gauche) dans un stockage avec un Vector. Il faut encore tester ces deux cas.

Enfin, le vrai boost (50% de vitesse en plus environ) trouvé en fin de concours (et utilisé que dans mon code je crois) est de tester si les points ont bougé entre 2 appels à update(). Si rien n’a changé on évite de manipuler des données dans tous les sens et on gagne beaucoup de temps.

Depuis, la fonction update() ressemble plutôt à :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function update(points)
{
	if(points.length != oldPointLength)
	{
		// (ré)initialisation
		for(point in points)
		{
			cache[point] = identifiant_de_case
			cases[identifiant_de_case].push( point )
		}
	}
	else
	{
		for(point in points)
		{
			if(cache[point] != identifiant_de_case)
			{
				// ce point à bougé
				cache[point] = identifiant_de_case
				// effectuer le changement de case du point
				[...]
			}
		}
	}
}

Voilà, j’espère que ça va donner de bonnes idées pour optimiser du code AS3. En vérité, ça ne sert que rarement à grand chose de pousser les choses à ce point, sauf quand un petit bout de code pompe une grosse partie des performances espérées.

Leave a comment

Your comment