UKOnline

Style de programmation

Une première façon d'optimiser un code consiste à adopter un bon style de programmation. Il est dès lors important de directement prendre de bonnes habitudes lorsque vous apprenez un nouveau langage.

On peut distinguer deux principales catégories de règles de style, à savoir les générales qui ne dépendent pas d'un langage de programmation en particulier et celles spécifiques à un langage. On a déjà pu aborder cette seconde catégorie pour Python dans le premier chapitre. Cette section présente quelques règles générales permettant d'optimiser un code et qui sont applicables à n'importe quel langage de programmation.

Opération couteuse

Lorsque l'on écrit des programmes, il faut se rappeler que toutes les opérations, aussi élémentaires soient-elles, n'ont pas le même cout en temps d'exécution. Par exemple, une multiplication prend plus de temps qu'une addition et une division ou un modulo prend encore plus de temps. Aussi, les opérations sur des nombres à virgule flottante sont plus lentes à être exécutées que celles sur des nombres entiers. En particulier, les opérations trigonométriques, la racine carrée ou l'exponentiation nécessitent beaucoup de temps de calcul.

Concernant la mémoire, tout traitement où il n'est pas possible de connaitre la taille des structures de données à l'avance risque de consommer plus de mémoire que ce qui est vraiment nécessaire.

Multiplication

Les multiplications sont donc plus chères que les additions, de l'ordre de quatre fois plus. Lorsqu'il est possible de les éviter, en écrivant le code autrement, il faut le faire si on veut accélérer l'exécution. Voici une fonction qui permet de générer les $n$ premiers multiples de $10$ :

Dans cette fonction, la variable i démarre à $0$ pour finir à $n$ (exclu) et, à chaque tour de boucle, on calcule et affiche la valeur de i * 10.

Il est possible d'éviter cette multiplication en la remplaçant par une addition. En Python, on va simplement faire une boucle qui démarre de $0$ et qui va jusque $10n$ (exclu), par pas de $10$, c'est-à-dire une addition de $10$ à i à chaque itération :

La seconde fonction, qui ne fait plus qu'une seule multiplication, est plus rapide que la première. En effet, on passe de 194 ms à 143 ms avec un $n$ fixé à un million, soit une diminution de temps de 26 %.

Nombre à virgule flottante

De manière générale, les calculs effectués sur des nombres entiers (int) sont plus rapides à exécuter que des calculs effectués sur des nombres à virgule flottante (float). Lorsqu'il est possible d'éviter les calculs en nombres à virgule flottante, en écrivant son programme autrement, il faut donc le faire si l'on veut diminuer le temps d'exécution.

Par exemple, si vous écrivez un programme qui doit manipuler des prix, vous pourriez décider de stocker tous les prix en centimes et ainsi vous limiter à des nombres entiers. En plus d'éviter des erreurs d'arrondis liées au calcul en nombres à virgule flottante, l'exécution de vos programmes sera légèrement plus rapide. Cependant, cette accélération dépend du langage de programmation et elle sera significative, par exemple, pour des programmes écrits en C ou en Java. En Python, les choses sont un peu différentes comme on va le voir avec la fonction suivante qui permet de calculer la somme des éléments d'une liste de prix :

En exécutant cette fonction avec un million de prix en nombres à virgule flottante, on obtient un temps d'exécution de 39 ms. En appelant la même fonction avec les mêmes prix, mais qui ont été préalablement multipliés par 100, on se retrouve avec un temps d'exécution de 72 ms, soit 46 % de temps en plus.

La raison est que Python travaille en précision arbitraire pour représenter les nombres entiers. Il s'agit donc d'objets complexes pour lesquels les opérations peuvent s'avérer couteuses en temps d'exécution, contrairement à des langages comme le C ou le Java où les int sont représentés par un espace mémoire de taille fixe (typiquement 32 ou 64 bits) et les opérations directement effectuées par le processeur.

Répétition de code

Évidemment, ce qu'il faut éviter au maximum dans un programme, ce sont les répétitions d'un même code plusieurs fois, lorsque ce n'est pas nécessaire. Comme on l'a vu au chapitre précédent, une des techniques possibles consiste à stocker dans une variable locale le résultat d'une opération, pour ne pas la répéter plusieurs fois inutilement.

Par exemple, dans le cas d'une boucle, il faut essayer de sortir les calculs de valeurs constantes, c'est-à-dire qui ne changent pas entre les itérations. Voici une fonction qui permet de calculer le périmètre de plusieurs cercles dont on connait le rayon :

Dans ce tout simple exemple, on se rend compte que 2 * math.pi est répété à chaque itération de la boucle, alors que la valeur de cette expression reste toujours la même. Ce que l'on peut faire, c'est réaliser ce calcul une seule fois en dehors de la boucle, et stocker le résultat dans une variable locale :

La seconde fonction est bien plus rapide que la première. En effet, on passe de 294 ms à 179 ms pour calculer les périmètres d'un million de cercles, soit une diminution de temps de 39 %.

Test inutile

Une dernière catégorie de règles de style générales qu'il est possible d'adopter concerne les tests inutiles. Des tests, typiquement effectués dans les instructions conditionnelles (if-else) et répétitives (while), sont représentés par des expressions booléennes. Dans plusieurs cas, certains styles peuvent améliorer les performances.

Comparaison avec booléen

Dans une expression booléenne, il ne faut jamais comparer une variable directement à True ou False. En effet, si vous faites une telle comparaison, c'est que la variable est booléenne et vaut déjà True ou False. Par exemple, les conditions des deux instructions suivantes sont à éviter, car elles comportent de la redondance :

Elles peuvent simplement se simplifier comme suit, en utilisant directement la variable booléenne comme condition, ou en l'utilisant combinée avec l'opérateur NON logique, qui est le not en Python :

Cela peut avoir un effet non-négligeable sur les performances d'exécution de portions de code d'un programme. Voici deux fonctions qui permettent de compter le nombre d'éléments valant True dans une liste de booléens :

La seconde fonction est plus rapide que la première. En effet, on passe de 64 ms à 46 ms pour compter le nombre de valeurs divisibles par 2 ou par 3, entre 0 et un million, soit une diminution de temps de 28 %.

Dans le cas de Python, on peut également simplifier les comparaisons avec la valeur spéciale None en remplaçant var == None par var is None.

Instruction else inutile

Un autre élément parfois simplifiable dans le cas des instructions if-else, c'est le fait que l'on peut parfois se passer du else. En effet, lorsque le but d'une instruction if-else est d'affecter une valeur différente à une variable, selon qu'une condition soit satisfaite ou non, on se retrouve souvent avec un code comme :

Dans ce cas précis, on peut simplifier le code en initialisant la variable avec la valeur mise dans le else et en supprimant donc cette dernière clause else. Le code devient donc :

Si l'opération faite par défaut, c'est-à-dire avant l'instruction if, n'est pas trop lourde, alors écrire le code de cette manière peut accélérer sa vitesse d'exécution. On peut également retirer le else dans une fonction lorsque le contenu de ce dernier est un return, puisqu'un second effet d'un return est de quitter immédiatement la fonction. Voici deux fonctions qui permettent de calculer la valeur absolue d'un nombre :

Pour cet exemple, la seconde fonction est un peu plus rapide que la première. En effet, on passe de 187 ms à 184 ms pour calculer la valeur absolue d'un million de nombres, soit une diminution de temps de 2 %.