Optimiser (Partie 1): Les opérations lourdes


  • Modérateurs

    Sommaire

    Introduction

    Bonjour chers lecteurs,

    Votre code est-il lent ? Vous voulez l'optimiser? Vous êtes arrivés à bon port! Découvrez l'Optimisator2000!

    -Equipe Marketing de Minecraft Forge France

    Ce tutoriel, qui sera découpé en plusieurs parties pour des questions de lisibilité et de simplicité, va vous expliquer certaines méthodes générales pour optimiser votre code.

    Attention: Toutes les optimisations ne sont pas forcément bonnes à faire et je vous conseille de faire fonctionner votre mod avant de penser à l'optimisation!

    La création d'objets

    Une des premières raisons des ralentissements sur Minecraft est le Ramasse-Miettes de la JVM (Garbage Collector).
    Le Ramasse-Miettes est, pour simplifier, un module de la JVM qui s'occupe de nettoyer la mémoire qui n'est plus utilisable (inaccessible pour votre code).

    Exemple:

    void f() {
        uneAutreFonction();
        // l'objet n'est plus accessible, donc la mémoire qu'il utilise est candidate pour être réutilisée
    }
    
    void uneAutreFonction() {
        Object o = new Object(); // une portion de la mémoire disponible est allouée pour cet objet
        System.out.println(o);
    }
    

    Cependant, le Ramasse-Miettes n'est pas parfait: il ne s'active que de temps en temps, et le plus souvent en urgence parce qu'il n'y a plus de place (pour MC).
    Ces activations à répétitions ont un coût: le Ramasse-Miettes doit réorganiser la mémoire utilisée, et cela prend un peu de temps.

    Comment éviter au Ramasse-Miettes tout ce travail?
    Il existe plusieurs façons de gérer le problème, qui dépendent de vos préférences, de si on peut réutiliser l'objet et plein d'autres paramètres dépendants du contexte. Je vais vous en présenter quelque-unes.

    Moins d'objets

    Ne pas créer d'objets quand vous n'avez pas besoin.
    Une proposition vague, certes, mais c'est la base.

    Exemple (avec la librairie JOML)

    Vector3f myVec = …;
    myVec.add(Vector3f(1f, 45f, 42f));
    

    peut être réécrit en:

    Vector3f myVec = ...;
    myVec.add(1f, 45f, 42f);
    

    Créez certains objets qu'une seule fois!
    Lorsqu'un objet n'est utilisé qu'à l'intérieur d'une fonction, réfléchissez à s'il est possible d'en faire une variable externe à la fonction.
    Exemple avec encore JOML

    void tourneMonVecteur(Vector3f vec) {
        Matrix3f myMat = new Matrix3f();
        myMat.identity().rotate((float)Math.PI, 1f, 0f, 0f);
        myMat.transform(vec);
    }
    

    peut se réécrire:

    private Matrix3f myMat = new Matrix3f().identity().rotate((float)Math.PI, 1f, 0f, 0f); // on profite des mécanismes de chaînage de JOML ici
    void tourneMonVecteur(Vector3f vec) {
        myMat.transform(vec);
    }
    

    Lorsque l'objet doit être configuré en plus d'être instancié, pensez aux constructeurs et au bloc 'static' (lorsque votre fonction est statique)

    Reprise de l'exemple précédent

    private Matrix3f myMat = new Matrix3f();
    
    public MaClasse() {
        myMat.identity();
        myMat.rotate((float)Math.PI, 1f, 0f, 0f);
    }
    
    void tourneMonVecteur(Vector3f vec) {
        myMat.transform(vec);
    }
    

    ou

    private static Matrix3f myMat = new Matrix3f();
    
    // Exécuté lors du chargement de la classe par la JVM
    static {
        myMat.identity();
        myMat.rotate((float)Math.PI, 1f, 0f, 0f);
    }
    
    static void tourneMonVecteur(Vector3f vec) {
        myMat.transform(vec);
    }
    

    Utilisez les singletons!
    Lorsque possible, utilisez le pattern Singleton: créez un objet dont l'instance sera la seule utilisée. Je vais simplement vous donner la structure la plus utilisée en Java:

    public class Singleton {
    
        private static Singleton instance;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if(instance == null)
                instance = new Singleton();
            return instance;
        }
    }
    

    Vous pouvez l'utiliser en appelant 'getInstance' (c'est ce pattern qu'utilise Minecraft pour la classe... "Minecraft")

    La réutilisation d'objets

    Une autre option est de réutiliser les mêmes objets. Imaginons que vous voulez faire une arme à feu à l'aide de projectiles, vous allez sûrement avoir besoin de créer un grand nombre de projectiles si votre arme a une cadence élevée. La méthode naïve consiste à créer un objet à chaque tir:

    public class MonProjectile {
    
        private float tempsVivant;
        private float maxTempsVivant = 42f;
    
        public void onUpdate(float dt) {
        // …
            tempsVivant += dt; // vous pouvez aussi utiliser des ticks
            if(tempsVivant > maxTempsVivant) {
                this.meurt();
            }
        }
    
        public void meurt() {
            // ...
        }
    
        public void prepare() {
            // ...
        }
    
        public void spawn() {
            // ...
        }
    }
    
    // ...
    
    public class MaSuperArme {
    
        public void tire() {
            MonProjectile projectile = new MonProjectile();
            projectile.prepare();
            projectile.spawn();
        }
    }
    

    Vous remarquerez sans doute que l'on va créer un nombre considérable d'objets dont on n'aura plus besoin quelques secondes après. Une solution est de remettre à zéro les objets lors qu'ils devraient être détruits et de les garder de côté pour les réutiliser et la méthode 'tire' ressemblerait à ceci:

    public void tire() {
        MonProjectile projectile = recycle(); // c'est là que la magie va se passer!
        projectile.prepare();
        projectile.spawn();
    }
    

    ❓ Mais comment implémenter cette méthode 'recycle', me direz-vous ?
    ❗ Et bien on va utiliser ce qui s'appelle une piscine (Pool en anglais)!

    Il existe plusieurs façons de les implémenter, et j'ai choisi de vous présenter une version simple avec une liste:

    /**
    * License: Do whatever you want with this. -Xavier 'jglrxavpok' Niochaut
    */
    
    import java.util.LinkedList;
    import java.util.List;
    
    public abstract class Pool<T>{
    
        // On va utiliser une LinkedList parce que l'on va faire beaucoup d'ajouts et de retraits dans cette liste
        private List<T>ready = new LinkedList<>();
    
        /**
        * 'Libère' un objet ie. le prépare pour sa réutilisation plus tard lors d'un appel à {@link #get()} et le
        * rajoute à cette piscine
        * @param object L'objet à réutiliser plus tard
        */
        public void free(T object) {
            ready.add(object);
        }
    
        /**
        * Renvoie un objet qui a été soit:
        *
        * - recyclé
        * - créé car il n'y avait plus d'objets dans la piscine
        * @return l'objet recyclé ou créé
        */
        public T get() {
            if(ready.isEmpty()) {
                return create();
            }
            return ready.remove(0); // on retire le premier élément de la liste
        }
    
        /**
        * Crée un nouvel objet de type 'T' au cas où il n'y en aurait plus de libre. Ne le rajoute pas à la piscine.
        * @return l'object créé
        */
        protected abstract T create();
        }
    }
    

    Créons notre piscine pour les projectiles:

    public class ProjectilePool extends Pool <monprojectile>{
    
        @Override
        protected MonProjectile create() {
            return new MonProjectile();
        }
    }
    

    Et utilisons-la:

    public class MaSuperArme {
    
        private ProjectilePool projectilePool = new ProjectilePool();
    
        public void tire() {
            MonProjectile projectile = recycle();
            projectile.prepare();
            projectile.spawn();
        }
    
        public MonProjectile recycle() {
            return projectilePool.get();
        }
    }
    
    public class MonProjectile {
    
        private float tempsVivant;
        private float maxTempsVivant = 42f;
        private ProjectilePool pool;
    
        public void onUpdate(float dt) {
            // ...
            tempsVivant += dt; // vous pouvez aussi utiliser des ticks
            if(tempsVivant > maxTempsVivant) {
                this.meurt();
            }
        }
    
        public void meurt() {
            pool.free(this);
        }
    
        public void prepare(ProjectilePool pool) {
            // ...
            this.pool = pool;
        }
    
        public void spawn() {
            // ...
        }
    }
    

    Et voilà! Vos projectiles sont maintenant recyclés!

    Attention! N'oubliez pas de réinitialiser tous les propriétés (position, vitesse, vie, etc.) de vos objets avant de les utiliser!

    Et voilà pour ce petit tutoriel sur la base gestion de la mémoire utilisée par votre mod, j'espère qu'il vous aura plu!