Vitesse, mémoire et limites
Le budget d'instructions de chaque ordinateur et comment la rotation le fixe, ce qui coûte des instructions, la mémoire comptée en cellules, les limites fixes, et comment écrire des programmes qui restent rapides.
Chaque ordinateur est une machine avec une vitesse et une mémoire, comme les vrais de son époque : le Calculateur à tubes exécute 400 instructions par seconde, l'Ordinateur moderne 100 000. Cette page explique comment cette vitesse se compte, à quoi un programme la dépense, comment la mémoire se mesure, et quelles limites fixes valent sur tous les ordinateurs. Les connaître permet de choisir le bon ordinateur pour une tâche, et d'écrire des programmes qui restent vifs sur les plus lents.
Le budget d'instructions
Un programme s'exécute sous forme d'une suite de petites instructions (empiler une valeur, lire une variable, additionner deux nombres, sauter...). À chaque tick, un ordinateur peut en exécuter un nombre fixe, son budget. Quand le budget est épuisé, le programme s'arrête simplement là où il en est et reprend au tick suivant. Rien ne casse : un long calcul s'étale juste sur plus de ticks.
| ordinateur | instructions par tick | par seconde | mémoire (cellules) |
|---|---|---|---|
| Calculateur à tubes | 20 | 400 | 2 048 |
| Ordinateur à transistors | 80 | 1 600 | 8 192 |
| Mini-ordinateur | 300 | 6 000 | 32 768 |
| Micro-ordinateur | 1 200 | 24 000 | 131 072 |
| Microcontrôleur | 400 | 8 000 | 16 384 |
| Ordinateur moderne | 5 000 | 100 000 | 1 048 576 |
Ces chiffres sont atteints à 256 tr/min. Le budget suit la rotation : il est multiplié par (tr/min) / 256, donc un ordinateur à 128 tr/min exécute moitié moins d'instructions, à 32 tr/min un huitième. Le sens de rotation ne compte pas, et tourner plus vite que 256 tr/min n'apporte rien de plus. Les Lunettes d'ingénieur affichent les instructions par seconde d'un ordinateur à sa vitesse actuelle.
Les fractions sont reportées : un Calculateur à tubes à 16 tr/min reçoit 1,25 instruction par tick, il en exécute donc 1 pendant trois ticks sur quatre et 2 au quatrième. Même un ordinateur très lent avance toujours.
Deux règles encore :
- Le budget inutilisé est perdu. Quand le programme attend (
sleep,os.pull_event,read), ce qui reste du budget du tick n'est pas mis de côté. Attendre ne coûte rien, mais ne rapporte pas de crédit. - Une dette se rembourse sur les ticks suivants. Certaines fonctions intégrées coûtent plus d'une instruction (voir plus bas). Quand l'une d'elles coûte plus que ce qui reste, le budget passe sous zéro, et les ticks suivants remboursent d'abord la dette. Sur un Ordinateur à transistors, un
gfx.clear()de tout l'écran coûte environ 820 instructions : dix ticks de budget, si bien que le programme reste immobile une demi-seconde après lui.
Une rotation plus lente ralentit le programme, pas le temps : sleep(1) dure toujours 20 ticks, et les minuteries sonnent à l'heure (voir Temps et minuteries). Sans rotation, l'ordinateur est figé.
Ce qui coûte des instructions
Chaque pas de la machine virtuelle coûte 1. Une ligne de code fait une poignée de pas :
| code | instructions |
|---|---|
un tour de for vide | 3 |
un tour de for avec total = total + i | 7 |
un tour de while avec i = i + 1 et son test | 9 |
| appeler une petite fonction à vous | environ 5 de plus qu'écrire son corps sur place |
print("bonjour") | 5 |
Appeler une fonction intégrée coûte 1, plus un supplément pour celles qui travaillent en proportion de leurs données :
| fonction | supplément |
|---|---|
print, write, term.write | 1 par tranche de 16 caractères écrits |
string.find, string.split | 1 par tranche de 16 caractères du texte |
string.rep | 1 par tranche de 8 répétitions |
table.concat | 1 par tranche de 16 caractères du résultat |
table.insert et table.remove à une position | environ 1 par tranche de 8 éléments de la liste |
table.contains | 1 par tranche de 8 éléments |
table.sort | environ n × log2(n) pour n éléments (avec votre propre fonction de comparaison, c'est son code qui est compté) |
fs.read, fs.write, fs.append, fs.copy | 1 par tranche de 16 caractères |
import | 1 par tranche de 16 caractères du fichier |
les dessins gfx | 1 par tranche de 64 pixels dessinés |
gfx.text | 1 par caractère, fois l'échelle au carré |
peripheral.list, peripheral.find | 16, plus 4 par bloc du réseau câblé |
peripheral.wrap | 8 |
list, count, push d'un inventaire | 1 par emplacement |
stress, capacity d'un bloc cinétique | 1 par bloc du réseau cinétique |
net.send | 32, plus le parcours du réseau et la taille du message |
net.broadcast | 64, plus 8 par ordinateur atteint |
vehicle.position, velocity, speed | 8 |
os.memory | 1 par tranche de 16 cellules utilisées |
La section Périphériques détaille les coûts des périphériques. Tout le reste (les maths, la plupart des fonctions de chaînes, lire une face redstone, régler une sortie...) coûte 1.
Pourquoi le serveur ne rame jamais
Un ordinateur ne peut jamais ralentir le serveur, quoi que fasse son programme :
- Il exécute au plus son budget à chaque tick.
while true do endprend tout le budget de son propre ordinateur, pour toujours, et rien d'autre : les autres ordinateurs et le monde continuent comme d'habitude. - Les fonctions intégrées qui font beaucoup de travail le font payer, comme dans les tableaux ci-dessus : dessiner, copier du texte ou parcourir un réseau n'est jamais gratuit.
- Les changements du monde (sorties redstone, Liaisons de redstone, vitesse des Contrôleurs de vitesse de rotation) sont appliqués une fois par tick, avec la dernière valeur demandée : une boucle qui bascule une sortie mille fois ne coûte au monde qu'une mise à jour (voir
Redstone et liaisons de Create). - L'écran part vers les joueurs au plus tous les 2 ticks (10 fois par seconde), et seulement les parties qui ont changé.
- La soupape de sécurité. Au-delà d'un certain nombre d'ordinateurs exécutés dans le même tick (512 par défaut), les suivants ne tournent plus qu'un tick sur deux. Leurs programmes et leurs horloges vont alors à mi-vitesse : un
sleep(1)dure deux vraies secondes. Les administrateurs règlent ce nombre avecmaxComputersTickedPerTickdansworld/serverconfig/computingages-server.toml(l'intervalle d'envoi de l'écran estterminalSyncInterval, dans le même fichier).
La mémoire
La mémoire se compte en cellules. Le tableau du haut donne le total de chaque ordinateur. Ce qui prend des cellules :
| donnée | cellules |
|---|---|
| une table | 4, plus 2 par entrée (clé et valeur) |
| une chaîne | 1 + sa longueur / 8 (arrondi vers le bas) |
un nombre, true, false, nil | rien de plus que la place qui le contient |
| une valeur fonction | 4 |
| une variable du fichier, une locale, une valeur en cours de calcul | 1 |
| un appel en cours (un cadre) | 8 |
| une variable globale | 2, plus son nom |
| le code d'un fichier chargé | environ 1 pour 4 instructions de la machine virtuelle, plus 1 par constante |
| un évènement en attente dans la file | 6, plus 2 par champ, plus ses valeurs |
Seules comptent les données que le programme peut encore atteindre : une table vers laquelle plus rien ne pointe est de nouveau libre, sans rien faire. os.memory() mesure ce qui est utilisé maintenant et renvoie {used = ..., total = ...} ; la commande mem du shell affiche la même chose.
local avant = os.memory().used
local niveaux = {}
for i = 1, 100 do
niveaux[i] = i * 2
end
local milieu = os.memory().used
local noms = {}
for i = 1, 10 do
noms[i] = "plaque pressée n° " .. i
end
local apres = os.memory().used
print("100 nombres : " .. milieu - avant .. " cellules")
print("10 noms : " .. apres - milieu .. " cellules")100 nombres : 204 cellules 10 noms : 54 cellules
La liste de 100 nombres prend 4 cellules pour la table et 2 par entrée. Les 10 noms prennent 4 pour leur table, 2 par entrée, et 3 par chaîne de 19 ou 20 caractères. Une liste de 1000 nombres (2004 cellules) occupe presque toute la mémoire d'un Calculateur à tubes, et rien du tout pour un Micro-ordinateur.
La mesure parcourt tout ce que le programme garde : os.memory() coûte donc 1 instruction par tranche de 16 cellules utilisées. Appelez-la de temps en temps, pas dans une boucle serrée.
Quand les données vivantes dépassent vraiment le total, le programme s'arrête avec out of memory. Les causes habituelles sont une liste ou un journal qui grandit sans fin (gardez seulement les 100 dernières lignes, ou écrivez-les dans un fichier), et un gros fichier lu d'un seul coup. Les évènements en attente comptent aussi : un ordinateur qui reçoit des messages plus vite qu'il ne les lit refuse les nouveaux dès que la file occupe un quart de sa mémoire (net.send renvoie alors false du côté de l'envoyeur).
Les limites fixes
Quel que soit l'ordinateur, ces limites tiennent :
| limite | valeur | au-delà |
|---|---|---|
| appels imbriqués | 200 | stack overflow |
| longueur d'une chaîne | 65 536 caractères | string too long |
| évènements dans la file | 256 | le plus ancien est supprimé |
| minuteries en attente | 256 | too many timers |
| fréquences de Liaison de redstone | 32 | too many link frequencies (max 32) |
| fichiers et dossiers par support | 1 024 | too many files (1024 at most) |
| longueur d'un chemin | 128 caractères | path too long (128 characters at most) |
| un message réseau | 2 048 valeurs, 32 768 caractères | message too large |
| texte collé | 4 096 caractères | coupé |
| nom d'un ordinateur (label) | 32 caractères | bad argument #1 to 'label' (at most 32 characters) |
| redémarrages | 2 par seconde | retardés |
La page Limites les recense toutes.
Écrire des programmes rapides
Attendez au lieu de vérifier. Un programme qui attend dans os.pull_event ou sleep ne coûte rien. Une boucle qui vérifie une face sans arrêt brûle tout son budget pour le même résultat :
-- gaspillage : des milliers d'instructions par seconde pour rien
while rs.get("left") == 0 do
end
-- gratuit jusqu'à ce que la face change
repeat
os.pull_event("redstone")
until rs.get("left") > 0Faites wrap une seule fois. peripheral.find parcourt tout le réseau câblé à chaque appel : appelez-le au début et gardez l'objet (voir Périphériques).
Ne redessinez que ce qui change. Un term.clear() complet suivi d'un redessin, ou un gfx.clear(), à chaque tick coûte cher sur un grand écran, et les joueurs ne voient de toute façon que 10 images par seconde. Réécrivez la ligne dont la valeur a changé, redessinez l'écran sur une minuterie quelques fois par seconde, et sautez le redessin quand rien n'a changé.
Gardez les fonctions de bibliothèque à portée dans les boucles chaudes. Lire une globale ou une locale coûte pareil, mais math.floor(x) lit d'abord math puis floor : une copie locale économise une instruction par appel.
local floor = math.floor
for i = 1, 1000 do
local piles = floor(i / 64) -- une instruction de moins que math.floor
end**Préférez for à while** pour les boucles qui comptent (7 instructions par tour au lieu de 9 dans les mesures ci-dessus), et recopiez une toute petite fonction directement dans une boucle qui tourne des milliers de fois.
Choisissez l'ordinateur. Le même programme peut être trop lent pour un Calculateur à tubes et instantané sur un Micro-ordinateur. Un tableau de bord redessiné chaque seconde, un trieur qui parcourt un grand coffre à objet renforcé, un jeu : donnez-leur un ordinateur rapide, et un arbre qui tourne vite.
Exemple : mesurer le coût d'une boucle
Combien d'instructions une boucle coûte-t-elle vraiment ? Chronométrez-la avec os.clock, puis convertissez les ticks en instructions avec le budget de l'ordinateur. Enregistrez ceci sous le nom mesure et lancez-le sur un Micro-ordinateur qui tourne à 256 tr/min :
local IPT = 1200 -- instructions par tick : Micro-ordinateur à 256 tr/min
local N = 10000
local debut = os.clock()
local total = 0
for i = 1, N do
total = total + i
end
local ticks = math.round((os.clock() - debut) * 20)
print(string.format("%d ticks, environ %.1f instructions par tour", ticks, ticks * IPT / N))> mesure 58 ticks, environ 7.0 instructions par tour
La boucle fait 10 000 tours de 7 instructions : 70 000 instructions, environ 58 ticks d'un Micro-ordinateur, trois secondes. Un Calculateur à tubes mettrait presque trois minutes pour la même boucle, un Ordinateur moderne 14 ticks. Changez le corps de la boucle pour comparer deux façons d'écrire la même chose, et prenez un grand N : la mesure est précise au tick près.