Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Auf Deutsch lesen

Section 0.2 sur 0.8 · Débutant · 16 min de lecture

Données, structures de données et algorithmes, et pourquoi la vitesse en dépend

Leçon vidéo

5 min · narré en anglais, avec sous-titres
Écouter seulement (audio)
Lire la transcription

Imaginez que vous cherchiez une lettre dans un bureau de poste. Si les lettres sont en tas, vous les vérifiez une par une, et un plus gros tas prend plus de temps. Si elles sont triées dans des casiers par code postal, vous allez droit au bon casier, quel que soit le nombre de lettres. C’est de cette différence que parle cette leçon.

D’abord, de quoi sont faites les données ? Les ordinateurs stockent tout sous forme de bits : des chiffres binaires qui valent zéro ou un. Huit bits forment un octet. À eux seuls, les bits ne signifient rien. Le même motif peut être le nombre soixante-cinq ou la lettre A. Un type de données, c’est la convention sur la façon de les lire. Les entiers sont des nombres sans virgule, qui servent à compter. Les booléens valent vrai ou faux. Les chaînes sont du texte. Les nombres à virgule flottante sont des nombres décimaux, mais seulement approchés : l’argent doit donc être stocké en pence ou en centimes entiers, ou avec un type décimal.

Une structure de données est une façon d’organiser de nombreuses valeurs pour que les opérations dont vous avez besoin soient rapides. En voici six courantes. Une liste garde les éléments dans l’ordre, comme une liste de courses numérotée. Obtenir l’élément situé à une position est instantané, mais trouver un nom oblige à vérifier les éléments un par un. Un dictionnaire range des valeurs sous des clés uniques, comme un vestiaire. Tendez le ticket C trois sept sept, et vous récupérez aussitôt le bon manteau. Une file, c’est premier entré, premier sorti, comme des commandes qui attendent la cuisine. Une pile, c’est dernier entré, premier sorti, comme une pile d’assiettes, ou le bouton Précédent de votre navigateur. Un arbre a une seule racine avec des branches en dessous, comme les dossiers de votre ordinateur. Un graphe relie des éléments selon n’importe quel schéma, comme une carte routière.

Un algorithme est une méthode précise, étape par étape, pour résoudre un problème, et qui se termine toujours. Différents algorithmes pour un même problème peuvent faire des quantités de travail très différentes. Voici une recherche dichotomique dans une liste triée de seize identifiants clients. Nous cherchons zéro trois sept sept. Comparez-le à l’élément du milieu. Il est plus grand, donc toute la première moitié est écartée. Comparez-le au milieu de ce qui reste. Cette fois, il est plus petit, donc la partie haute disparaît aussi. Chaque coup d’œil divise par deux ce qui reste. Encore deux coups d’œil, et il est trouvé. Pour un million de noms triés, la recherche dichotomique demande environ vingt coups d’œil au lieu de jusqu’à un million.

Les ingénieurs décrivent cela avec la notation grand O. Elle pose la question : si les données deviennent beaucoup plus volumineuses, combien de travail en plus faut-il ? Temps constant signifie que le travail n’augmente pas, comme pour une recherche dans un dictionnaire, en moyenne. Logarithmique signifie une étape de plus chaque fois que les données doublent, comme la recherche dichotomique. Linéaire signifie deux fois plus de données, deux fois plus de travail, comme le parcours d’une liste. Quadratique signifie deux fois plus de données, quatre fois plus de travail, ce qui arrive quand chaque élément est comparé à tous les autres. Un code qui fonctionnait bien avec mille clients peut s’enliser complètement avec un million.

Passons à un exemple commenté. Une app de livraison de repas stocke un million de clients, et chaque fois qu’une commande arrive, elle doit retrouver le client d’après son identifiant. La conception A parcourt une liste. En moyenne, cela fait environ un demi-million de vérifications, soit à peu près cinquante millisecondes par recherche. À deux cents commandes par seconde, cela fait dix secondes de travail du processeur chaque seconde. Le serveur ne peut pas suivre, alors les requêtes s’accumulent et expirent. La conception B utilise un dictionnaire indexé par identifiant. Elle va droit à l’entrée : environ une vérification, qu’il y ait mille clients ou un million. La conception B n’est pas gratuite. Elle utilise de la mémoire supplémentaire et doit être tenue à jour quand les clients changent. Mais elle transforme l’échec en marge confortable.

Voici la différence. En haut, le parcours compare l’identifiant recherché à chaque client, tour à tour. Un million de clients peut exiger un million de comparaisons. En bas, le dictionnaire fait passer la clé par une fonction de hachage, qui indique que C trois sept sept se trouve dans la case quatre, et il y va directement. Une seule étape, quel que soit le nombre de clients.

Le dictionnaire est plus rapide parce qu’il utilise de la mémoire supplémentaire pour organiser les données à l’avance. C’est l’un des compromis les plus courants en informatique : acheter de la vitesse avec de l’espace. Un index, comme celui qu’on trouve à la fin d’un livre, accélère les lectures, mais il occupe de l’espace et doit être mis à jour à chaque écriture. Un cache conserve une copie d’une réponse coûteuse dans un endroit rapide. Mais quand les données changent, à quel point la copie peut-elle être périmée ? Un prix périmé pendant quelques secondes peut passer. Un solde bancaire périmé, peut-être pas. Les architectes se demandent donc à quelles questions il faut répondre rapidement, et jusqu’où les données vont grossir. Aucune quantité de matériel supplémentaire ne sauvera longtemps un algorithme quadratique.

La section complète en dit plus sur le texte, et contient un tableau comparant les structures de données ainsi que les chiffres derrière la notation grand O. Ensuite, dans la section zéro point trois, nous passons des données au code, et à la façon dont il est organisé. À très vite.

Ce qu’il faut savoir

Tout n’est que bits

Les ordinateurs stockent tout sous forme de bits : des chiffres binaires qui valent soit 0, soit 1. Physiquement, un bit peut être une minuscule charge électrique, une zone magnétique sur un disque ou une impulsion lumineuse dans une fibre, mais pour le logiciel ce n’est qu’une valeur parmi deux. Huit bits forment un octet, qui peut contenir 28 = 256 motifs différents, de 00000000 à 11111111.

Les tailles se construisent à partir de là. Un kilooctet vaut environ mille octets, un mégaoctet environ un million, un gigaoctet environ un milliard et un téraoctet environ mille milliards. (En toute rigueur, les préfixes standard kilo, méga et giga désignent des puissances de 1000 ; la mémoire se mesure souvent en puissances de 1024, pour lesquelles existent les unités kibioctet, mébioctet et gibioctet. La différence compte rarement pour la conception, mais elle explique pourquoi un disque de « 1 To » affiche moins dans certains gestionnaires de fichiers.) Une page de texte brut fait quelques kilooctets, une photo prise au téléphone quelques mégaoctets, une heure de vidéo haute définition quelques gigaoctets.

Les bits ne signifient rien à eux seuls. Le motif 01000001 peut être le nombre 65, la lettre A, une partie d’une couleur dans une photo ou une partie d’une instruction pour le CPU. Ce qui donne un sens aux bits, c’est la convention sur la façon de les lire : c’est l’idée de type de données.

Les types de données : nombres, texte, vrai ou faux

Un type de données indique à un programme quel genre de valeur est une donnée, et donc quelles opérations ont un sens. Tout langage de programmation possède quelques types de base, et ceux de Python sont représentatifs.

Les entiers sont des nombres sans partie décimale, comme 3, 0 ou -42, qui servent à compter : les articles d’un panier, les sièges d’un vol. Les nombres à virgule flottante (flottants), comme 12,5 ou 0,2, représentent les nombres décimaux de façon approchée, en binaire. Comme beaucoup de nombres décimaux n’ont pas de forme binaire exacte, 0.1 + 0.2 donne 0.30000000000000004 en Python. Cela convient à la physique et aux graphismes, mais pas à l’argent : c’est pourquoi les systèmes financiers stockent les montants en nombres entiers de pence ou de centimes, ou utilisent un type décimal (Python dispose de decimal.Decimal). Les booléens n’ont que deux valeurs, True et False, et enregistrent des faits de type oui ou non, comme « cette commande a-t-elle été payée ? ». Les chaînes sont du texte, comme le nom d’un client. Les systèmes réels ajoutent d’autres types par-dessus : dates et heures, montants monétaires et données binaires comme les images.

Les types comptent au-delà d’un seul programme. Quand deux systèmes échangent des données, ils doivent s’accorder sur les types autant que sur les valeurs : un code postal est-il un nombre ou du texte (du texte : certains commencent par des lettres, et certains codes numériques commencent par zéro) ? Une date est-elle au format jour-mois ou mois-jour, et dans quel fuseau horaire ? Beaucoup de bugs d’intégration coûteux sont des désaccords sur les types.

Le texte et UTF-8

Le texte est stocké en attribuant un numéro à chaque caractère, puis en stockant ces numéros sous forme d’octets. La norme internationale Unicode attribue un numéro, appelé point de code, à bien plus de cent mille caractères issus des écritures du monde entier, plus des symboles et des emoji. Un encodage décide ensuite comment ces numéros deviennent des octets. Aujourd’hui, l’encodage dominant, sur le web et dans la plupart des nouveaux systèmes, est UTF-8.

UTF-8 utilise un nombre variable d’octets par caractère : un octet pour les lettres de base de l’anglais, les chiffres et la ponctuation (si bien qu’un texte qui n’utilise que ces caractères est identique, octet pour octet, en UTF-8 et dans l’encodage ASCII, bien plus ancien), deux octets pour des caractères comme é, trois pour des caractères comme € et la plupart des caractères chinois, japonais et coréens, et quatre pour les emoji et certaines écritures plus rares. Cela a une conséquence pratique : le nombre de caractères d’une chaîne et le nombre d’octets qu’elle occupe peuvent différer. Une colonne de base de données limitée à 50 octets ne pourra peut-être pas contenir un nom de 50 caractères écrit en grec.

Le bug de texte classique, ce sont les caractères illisibles (par exemple Café au lieu de Café), qui apparaissent quand des octets écrits dans un encodage sont lus dans un autre. Le remède est simple à énoncer : utiliser UTF-8 partout, et le dire explicitement chaque fois que du texte franchit une frontière, comme un fichier, une base de données ou un message réseau.

Les structures de données : des façons d’organiser les données

Les programmes traitent rarement une seule valeur à la fois. Une structure de données est une façon d’organiser de nombreuses valeurs en mémoire pour que les opérations dont vous avez besoin soient rapides. Le bon choix peut faire la différence entre une page qui se charge instantanément et une page qui ne se charge jamais. Chacune a son analogie dans la vie quotidienne.

Une liste (appelée tableau dans beaucoup de langages) garde les éléments dans l’ordre, comme une liste de courses numérotée. Obtenir le cinquième élément est instantané, car l’ordinateur peut calculer où il se trouve. Trouver un élément d’après sa valeur oblige à vérifier les éléments un par un. Un dictionnaire (appelé aussi map ou table de hachage) range des valeurs sous des clés uniques, comme un vestiaire : vous tendez le ticket C377 et vous récupérez aussitôt le bon manteau, sans regarder tous les manteaux. Un ensemble est une collection sans doublons, comme une liste d’invités, et répond rapidement à la question « cette personne y figure-t-elle ? ».

Une file fonctionne comme une file d’attente au guichet de la poste : premier entré, premier sorti. Les systèmes utilisent des files pour le travail en attente, comme les commandes qui attendent la cuisine. Une pile fonctionne comme une pile d’assiettes : dernier entré, premier sorti. Le bouton Précédent de votre navigateur en utilise une : la page que vous avez visitée le plus récemment est la première à laquelle vous revenez. Un arbre a une seule racine avec des branches en dessous, comme un arbre généalogique ou les dossiers de votre ordinateur ; c’est aussi grâce aux arbres que les bases de données gardent les données triées pour des recherches rapides. Un graphe est un ensemble d’éléments (nœuds) reliés par des liens (arêtes) selon n’importe quel schéma, comme une carte routière ou un réseau social, et répond à des questions comme « quel est le plus court chemin ? » ou « qui sont les amis de mes amis ? ».

Six structures de données courantes

Six structures de données usuelles : liste, dictionnaire, file, pile, arbre et grapheListe (tableau) : éléments en ordre, lus par positionAna0Ben1Chen2Dev3Eve4Dictionnaire (map) : valeur lue par clé"C102"Ana"C215"Ben"C377"ChenFile : 1er entré, 1er sorticde 4cde 3cde 2cde 1entreserviePile : le dernier sort 1erpage 3 (haut)page 2page 1push etpop en hautArbre : une racine, chaque nœud a des filsPlatsPizzaSushiCurryGraphe : éléments reliés librementABCDE
Chaque structure organise le même genre de données différemment, ce qui rend différentes questions rapides.

Ce pour quoi chaque structure est efficace

StructureRapide pourLente pourUsage typique
Liste / tableauObtenir l’élément situé à une position ; ajouter à la finTrouver un élément d’après sa valeur (parcours obligatoire) ; insérer près du débutUn panier d’articles ordonné ; une page de résultats de recherche
Dictionnaire / mapTrouver, ajouter ou supprimer d’après la cléTout ce qui exige les éléments dans un ordre triéClients par identifiant ; paramètres par nom ; un cache
EnsembleTester l’appartenance ; supprimer les doublonsConserver un ordre (la plupart des ensembles n’en ont pas)Utilisateurs ayant déjà voté ; étiquettes uniques
FileAjouter à l’arrière et prendre à l’avantAtteindre les éléments du milieuCommandes qui attendent la cuisine ; tâches qui attendent un worker
PileAjouter et prendre au sommetAtteindre les éléments plus basHistorique d’annulation ; le bouton Précédent ; suivi des appels de fonctions
Arbre (trié)Trouver d’après la valeur et lister une plage dans l’ordrePlus complexe à maintenir équilibréIndex de base de données ; dossiers ; menus
GrapheSuivre des relationsLes très grands graphes exigent un stockage spécialiséItinéraires et cartes ; réseaux sociaux ; recommandations
basket = ["pizza", "cola", "pizza"]            # list: ordered, duplicates allowed
print(basket[0])                                # by position: "pizza"

customers = {"C102": "Ana", "C377": "Chen"}      # dictionary: key to value
print(customers["C377"])                        # by key: "Chen"

voted = {"ana@example.com", "ben@example.com"}   # set: no duplicates
print("ana@example.com" in voted)              # True

from collections import deque
kitchen = deque(["order 1", "order 2"])         # queue
kitchen.append("order 3")                       # joins at the back
print(kitchen.popleft())                        # served first: "order 1"
Les mêmes structures en Python

Les algorithmes

Un algorithme est une méthode précise, étape par étape, pour résoudre un problème, et qui se termine toujours : comme une recette, mais sans aucune place pour l’interprétation. Trier une liste de noms, trouver le plus court chemin entre deux adresses, vérifier si un mot de passe correspond et décider quelle publicité afficher, tout cela est fait par des algorithmes.

Il existe généralement plus d’un algorithme pour un même problème, et la quantité de travail qu’ils font peut varier énormément. Imaginez que vous cherchiez un nom dans un annuaire téléphonique imprimé. Vous pourriez commencer à la première page et lire chaque entrée jusqu’à le trouver : c’est correct, mais lent. Ou vous pourriez ouvrir l’annuaire au milieu, regarder si le nom vient avant ou après cette page, écarter la moitié qui ne peut pas le contenir, et recommencer. Comme l’annuaire est trié, chaque coup d’œil divise par deux les pages restantes. Cette seconde méthode s’appelle la recherche dichotomique, et pour un annuaire d’un million de noms, elle demande environ 20 coups d’œil au lieu de jusqu’à un million.

La recherche dichotomique : diviser le problème par deux

La recherche dichotomique divise par deux, à chaque étape, la partie triée encore à explorerRecherche dichotomique dans une liste triée de 16 ID clients : cible 0377Étape 10102011801500167020102150240026303000311035003770402044404800499Étape 20102011801500167020102150240026303000311035003770402044404800499Étape 30102011801500167020102150240026303000311035003770402044404800499Étape 40102011801500167020102150240026303000311035003770402044404800499trouvéEn surbrillance : l’élément du milieu comparé à chaque étape. En gris : écarté. Restants : 16, 7, 3, 1
Dans une liste triée, chaque comparaison écarte la moitié de ce qui reste : 16 éléments demandent donc au plus 4 ou 5 coups d’œil.

Grand O : comment le travail augmente avec les données

Les ingénieurs décrivent comment le coût d’un algorithme augmente à l’aide de la notation grand O. Elle ignore le temps exact sur une machine particulière et pose une seule question : si la quantité de données, appelée n, devient dix ou mille fois plus grande, combien de travail en plus faut-il ? Elle décrit le pire cas, sauf mention contraire, et elle ignore les facteurs constants, ce qui fait à la fois sa force (elle saisit la forme de la croissance) et sa faiblesse (deux algorithmes en O(n) peuvent quand même différer d’un facteur dix en pratique).

O(1), temps constant : le travail ne dépend pas de n. Chercher une clé dans un dictionnaire est en O(1) en moyenne. O(log n), logarithmique : le travail augmente d’une étape chaque fois que n double, comme la recherche dichotomique. O(n), linéaire : deux fois plus de données, deux fois plus de travail, comme le parcours d’une liste. O(n log n) : un peu moins bien que linéaire ; les bons algorithmes de tri généralistes fonctionnent à ce rythme. O(n2), quadratique : deux fois plus de données, quatre fois plus de travail. Cela apparaît typiquement quand, pour chaque élément, un programme parcourt tous les autres éléments, par exemple pour comparer chaque client à tous les autres afin de trouver les doublons.

Nombre approximatif d’étapes de base quand les données augmentent

n (éléments)O(1)O(log n)O(n)O(n log n)O(n2)
101environ 310environ 33100
1,0001environ 101,000environ 10 0001,000,000
1,000,0001environ 201,000,000environ 20 000 0001,000,000,000,000

Lisez attentivement la dernière colonne. À un milliard d’étapes simples par seconde, un algorithme en million au carré a besoin de mille milliards d’étapes : environ un quart d’heure de calcul pur, et en pratique bien plus. Le même travail fait avec une méthode en O(n log n) prend une petite fraction de seconde. Un code qui semblait correct pendant les tests avec 1000 clients peut s’enliser complètement avec un million, et aucune quantité de matériel supplémentaire ne sauvera longtemps un algorithme quadratique.

Exemple commenté

Exemple commenté : retrouver un client parmi un million

Une app de livraison de repas stocke 1 000 000 de clients. Chaque fois qu’une commande arrive, elle doit retrouver le client d’après son identifiant. Comparez deux conceptions, en supposant que chaque vérification prend 0,1 microseconde.

Conception A : une listefor c in customers: if c.id == wanted: ... parcourt la liste jusqu’à trouver une correspondance.
Vérifications par recherche (A)Jusqu’à 1 000 000 ; en moyenne environ 500 000.
Temps par recherche (A)Environ 500 000 x 0,1 microseconde = 50 millisecondes.
À 200 commandes par seconde (A)200 x 50 ms = 10 secondes de travail CPU chaque seconde : le serveur ne peut pas suivre, alors les requêtes s’accumulent et expirent.
Conception B : un dictionnaire indexé par identifiantcustomers_by_id[wanted] calcule où se trouve l’entrée et y va directement.
Vérifications par recherche (B)Environ 1, qu’il y ait mille clients ou un million.
Temps par recherche (B)Bien moins d’une microseconde.
Coût de la conception BDe la mémoire supplémentaire pour le dictionnaire, et le travail de le construire puis de le tenir à jour chaque fois qu’un client est ajouté ou modifié.

Les deux conceptions donnent des réponses identiques ; seule la structure de données diffère, et cela fait passer le résultat de l’échec à une marge confortable.

Les bases de données offrent le même choix. Sans index, trouver une ligne d’après une colonne oblige à parcourir toute la table ; avec un index, il suffit de quelques étapes dans un arbre trié. La section sur le stockage des données y reviendra.

Parcours ou accès direct

Trouver un client en parcourant une liste vérifie les éléments un par un, alors qu’un dictionnaire va droit à la bonne caseParcourir une liste : vérifier chaque clientOù estC377C102C215C311C350C3775 comparaisons ici ; un million pour un million de clientsDictionnaire : calculer où se trouve la clé, puis y aller directementOù estC377HachageC377 en case 4cases 0 à 3 ...case 4 : Chen1 étape, quel quesoit le nombrede clients
Un parcours fait un travail proportionnel au nombre d’éléments ; un dictionnaire va directement à la bonne case.

L’espace contre le temps

Le dictionnaire de l’exemple commenté est plus rapide parce qu’il utilise de la mémoire supplémentaire pour organiser les données à l’avance. C’est l’un des compromis les plus courants en informatique : on peut souvent acheter de la vitesse avec de l’espace, ou économiser de l’espace en dépensant du temps.

Un index est un ensemble de données supplémentaires, conservé à côté des données principales, qui répond rapidement à un certain type de question, comme l’index à la fin d’un livre. Il accélère les lectures, mais il occupe de l’espace de stockage et doit être mis à jour à chaque écriture : une table dotée de dix index est donc lente pour les insertions. Un cache conserve une copie de la réponse à une question coûteuse dans un endroit rapide, par exemple en mémoire, pour que la personne suivante qui la pose l’obtienne immédiatement. Les caches expliquent pourquoi la page d’un produit populaire se charge vite même quand la base de données derrière est très sollicitée, mais ils soulèvent une question difficile : quand les données d’origine changent, à quelle vitesse la copie doit-elle changer aussi ? Afficher un prix périmé pendant quelques secondes peut être acceptable ; afficher un solde bancaire périmé ne l’est peut-être pas.

La compression est l’échange inverse : dépenser du temps CPU pour tasser les données dans moins d’octets, ce qui économise du stockage et du temps de transfert réseau.

Pourquoi c’est important pour l’architecture

Les architectes écrivent rarement des algorithmes de tri, mais ils prennent sans cesse des décisions dont les conséquences sont exactement celles-ci. À quelles questions le système devra-t-il répondre rapidement, et les données ont-elles les index nécessaires pour y répondre ? Jusqu’où les données auront-elles grossi dans trois ans, et quelles parties de la conception sont linéaires, ou pire, face à cette croissance ? Où la mise en cache vaut-elle sa complexité, et à quel point les données en cache peuvent-elles être périmées ? Quand le volume de données est multiplié par dix, quel composant casse en premier ?

Ce sont des questions de performances et de scalabilité, que le chapitre 2 précise, ainsi que de mise en cache et de choix de base de données, que le chapitre 7 traite en profondeur. La section suivante passe des données au code, et à la façon d’organiser le code pour que des personnes puissent le comprendre et le modifier.

Vérifiez votre compréhension

Pourquoi 0.1 + 0.2 ne donne-t-il pas exactement 0,3 dans la plupart des langages ?

Les nombres à virgule flottante sont stockés en binaire, et beaucoup de fractions décimales, dont 0,1 et 0,2, n’ont pas de forme binaire exacte : de minuscules erreurs d’arrondi apparaissent donc.

Quelle structure de données convient à un bouton « Annuler » dans un éditeur de texte, et pourquoi ?

Une pile : chaque action est empilée au sommet, et l’annulation dépile d’abord la plus récente (dernier entré, premier sorti).

Si un traitement en O(n) prend une seconde pour 100 000 éléments, combien de temps environ faut-il pour 1 000 000 ?

Environ dix secondes, car un travail linéaire augmente proportionnellement aux données.

Que payez-vous en échange de la vitesse que vous donne un index de base de données ?

Du stockage supplémentaire, et du travail supplémentaire à chaque insertion, mise à jour et suppression pour tenir l’index à jour.

Suivant · 0.3Organiser le codeComment les programmeurs découpent le code en fonctions, modules, objets et interfaces ; la différence entre une bibliothèque et un framework ; les avantages et les risques des dépendances ; et un premier aperçu du couplage et de la cohésion.Continuer →

Sources et lectures complémentaires

Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.

Demander : « Comment savoir si une page lente de notre app est due à un index de base de données manquant ou à autre chose ? »