Create: Computing AgesDoc Brass
Piloter le monde

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.

ordinateurinstructions par tickpar secondemémoire (cellules)
Calculateur à tubes204002 048
Ordinateur à transistors801 6008 192
Mini-ordinateur3006 00032 768
Micro-ordinateur1 20024 000131 072
Microcontrôleur4008 00016 384
Ordinateur moderne5 000100 0001 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 :

codeinstructions
un tour de for vide3
un tour de for avec total = total + i7
un tour de while avec i = i + 1 et son test9
appeler une petite fonction à vousenviron 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 :

fonctionsupplément
print, write, term.write1 par tranche de 16 caractères écrits
string.find, string.split1 par tranche de 16 caractères du texte
string.rep1 par tranche de 8 répétitions
table.concat1 par tranche de 16 caractères du résultat
table.insert et table.remove à une positionenviron 1 par tranche de 8 éléments de la liste
table.contains1 par tranche de 8 éléments
table.sortenviron 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.copy1 par tranche de 16 caractères
import1 par tranche de 16 caractères du fichier
les dessins gfx1 par tranche de 64 pixels dessinés
gfx.text1 par caractère, fois l'échelle au carré
peripheral.list, peripheral.find16, plus 4 par bloc du réseau câblé
peripheral.wrap8
list, count, push d'un inventaire1 par emplacement
stress, capacity d'un bloc cinétique1 par bloc du réseau cinétique
net.send32, plus le parcours du réseau et la taille du message
net.broadcast64, plus 8 par ordinateur atteint
vehicle.position, velocity, speed8
os.memory1 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 end prend 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 avec maxComputersTickedPerTick dans world/serverconfig/computingages-server.toml (l'intervalle d'envoi de l'écran est terminalSyncInterval, 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éecellules
une table4, plus 2 par entrée (clé et valeur)
une chaîne1 + sa longueur / 8 (arrondi vers le bas)
un nombre, true, false, nilrien de plus que la place qui le contient
une valeur fonction4
une variable du fichier, une locale, une valeur en cours de calcul1
un appel en cours (un cadre)8
une variable globale2, 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 file6, 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.

Brass
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")
Écran
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 :

limitevaleurau-delà
appels imbriqués200stack overflow
longueur d'une chaîne65 536 caractèresstring too long
évènements dans la file256le plus ancien est supprimé
minuteries en attente256too many timers
fréquences de Liaison de redstone32too many link frequencies (max 32)
fichiers et dossiers par support1 024too many files (1024 at most)
longueur d'un chemin128 caractèrespath too long (128 characters at most)
un message réseau2 048 valeurs, 32 768 caractèresmessage too large
texte collé4 096 caractèrescoupé
nom d'un ordinateur (label)32 caractèresbad argument #1 to 'label' (at most 32 characters)
redémarrages2 par seconderetardé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 :

Brass
-- 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") > 0

Faites 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.

Brass
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 :

mesure
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))
Terminal
> 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.