Défis Advent of Code#
Systèmes sur puce et informatique embarquée
Défi Docking Data#
l’amarrage — masques et opérations bit à bit
Jour 14
Fichiers à télécharger
Consignes
-
L’énoncé se lit sur le site (voir le QR code) : https://adventofcode.com/2020/day/14 . On peut le lire en anglais ou le faire traduire par le navigateur (clic droit, Traduire en français).
-
Avec un compte (GitHub, Google…), le bouton get your puzzle input donne votre fichier de données, à enregistrer sous
input.txtà côté de votre programme. Sans compte : le dossier du défi (lien ci-dessus) contient des données maison (input.txt) et le programmeverifier.py, qui dit si vos réponses sont justes. -
La partie 2 n’apparaît qu’une fois la partie 1 validée sur le site.
-
L’IA peut aider à comprendre un message d’erreur, jamais à écrire la solution.
Ce défi met en scène l’ordinateur embarqué d’un ferry, un système comme ceux de ce chapitre : il initialise sa mémoire adresse par adresse. Le masque, appliqué bit par bit, rappelle le masque de sous-réseau vu au chapitre 9 ; l’approfondissement 1 réinvestit la récursivité du chapitre 1.
Lire l’énoncé#
Vocabulaire utile pour lire le texte original :
| bitmask | masque de bits | overwrite | écraser, remplacer |
| unchanged | inchangé | memory address | adresse mémoire |
| 36-bit unsigned | entier positif sur 36 bits | most significant bit | bit de poids fort |
| initialization | initialisation | sum | somme |
| floating | flottant (bit qui prend les deux valeurs) | decoder | décodeur |
Résumé de l’énoncé (partie 1)
Contexte. À l’approche du port, l’ordinateur du ferry doit initialiser sa mémoire, mais il ne comprend pas le système de masques de bits utilisé par le port. Vous allez émuler ce système. Le fichier est un petit programme : chaque ligne change le masque courant ou écrit une valeur en mémoire.
Ce qu’il faut faire.
-
Une ligne
mask =est suivie de 36 caractères parmi0,1,X; une lignemem[a] = vécrit la valeur \(v\) à l’adresse \(a\). Adresses et valeurs sont des entiers positifs sur 36 bits. -
Le masque s’écrit bit de poids fort à gauche (\(2^{35}\)) et reste en vigueur jusqu’au
masksuivant. -
Juste avant chaque écriture, on applique le masque à la valeur : un
0ou un1du masque impose ce bit, unXlaisse le bit de \(v\) tel quel. Autrement dit, le masque ne retouche que certains bits. -
Toutes les cases valent 0 au départ ; écrire à nouveau à la même adresse remplace l’ancienne valeur. La réponse est la somme de toutes les valeurs présentes en mémoire à la fin.
Exercice 1 — Comprendre ce qui est demandé #
Questions de vérification, à traiter sur le cahier :
-
Si le masque ne contient que des
X, quelle valeur est écrite ? -
Deux lignes
memécrivent à la même adresse : que contient la case à la fin ? -
Pourquoi ne peut-on pas représenter la mémoire par une liste de \(2^{36}\) cases ?
Exercice 2 — À la main, sur un petit exemple #
On considère le programme (inventé) suivant :
mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1X0X0
mem[3] = 6
mem[10] = 21
mem[3] = 13
Écrire 6, 21 et 13 en binaire sur 6 bits, appliquer le masque et donner la valeur écrite à chaque fois. Vérification : la réponse est 40.
Programmer la partie 1#
Exercice 3 — Lire une ligne #
Outil Python : strip (nettoyer une ligne lue)
Chaque ligne renvoyée par readlines() se termine par le caractère de fin de ligne "\n" (sauf parfois la dernière). ligne.strip() renvoie une copie de la ligne sans les espaces ni les fins de ligne placés au début et à la fin.
ligne = "1721\n"
print(ligne.strip()) # 1721
print(len(ligne), len(ligne.strip())) # 5 4
Intérêt. Comparer ou découper une ligne sans que la fin de ligne ne gêne : "abc\n" == "abc" vaut False.
Écrire une fonction analyser(ligne) qui renvoie ("mask", chaine) ou ("mem", adresse, valeur) selon la ligne.
Coup de pouce
ligne.split(" = ") sépare la partie gauche de la valeur. Pour l’adresse, on peut découper "mem[3]" avec une tranche qui enlève les 4 premiers caractères et le dernier.
Outil Python : opérations bit à bit et conversions binaires (&, |, int(s, 2), bin)
Les opérateurs & (et) et | (ou) travaillent sur l’écriture binaire des entiers, bit par bit, tous les bits en même temps. int(chaine, 2) lit une chaîne binaire ; bin(n) écrit n en binaire avec le préfixe 0b. Une tranche retire le préfixe, et une boucle complète par des zéros à gauche pour obtenir 36 chiffres.
a = int("1100", 2) # 12 : chaine binaire -> entier
b = int("1010", 2) # 10
print(a & b) # 8 (1000) : 1 la ou les deux bits valent 1
print(a | b) # 14 (1110) : 1 la ou au moins un des bits vaut 1
print(bin(a & b)) # 0b1000 : entier -> chaine binaire, prefixe 0b
print(bin(5)[2:]) # 101 : la tranche retire le prefixe
def en_binaire(n, nb_bits):
"""Renvoie l'ecriture binaire de n sur nb_bits chiffres (0 a gauche)."""
chaine = bin(n)[2:]
while len(chaine) < nb_bits:
chaine = "0" + chaine
return chaine
print(en_binaire(5, 36)) # 36 caracteres, completes par des 0 a gauche
Intérêt. x & m met à 0 tous les bits de x là où m a un 0 ; x | m met à 1 tous les bits de x là où m a un 1. Une seule opération, très rapide, remplace une boucle sur les 36 bits.
À essayer. Calculer à la main 13 & 6 et 13 | 6 (écrire 13 et 6 sur 4 bits), puis vérifier avec Python.
Méthode : parcourir une chaîne caractère par caractère
Pour remplacer, compter ou repérer un caractère dans une chaîne, on la parcourt avec une boucle. Une chaîne n’est pas modifiable : on en construit une nouvelle par concaténations successives.
def remplacer(s, a, b):
"""Renvoie une copie de s ou chaque caractere a est remplace par b."""
resultat = ""
for c in s:
if c == a:
resultat += b
else:
resultat += c
return resultat
def premier_indice(s, a):
"""Renvoie l'indice de la premiere occurrence de a dans s, ou -1."""
i = 0
while i < len(s) and s[i] != a:
i += 1
if i == len(s):
return -1
return i
m = "X1X0"
print(remplacer(m, "X", "0")) # 0100 : une NOUVELLE chaine
print(premier_indice(m, "X")) # 0 : indice de la premiere occurrence
print(premier_indice(m, "Z")) # -1 : absent
Intérêt. Pour compter les X, on reprend la même boucle avec un compteur. Le coût de ces fonctions est proportionnel à la longueur de la chaîne ; premier_indice s’arrête dès qu’il a trouvé.
À essayer. Après remplacer(m, "X", "0"), que vaut m ? Pourquoi ?
Exercice 4 — Appliquer le masque avec deux opérations bit à bit #
Écrire une fonction appliquer(masque, valeur). On construira deux entiers à partir du masque : l’un pour forcer des bits à 1, l’autre pour forcer des bits à 0. Vérifier : appliquer(m, 6) vaut 18 avec le masque de l’exemple.
Coup de pouce
En Python, a | b (ou bit à bit) et a & b (et bit à bit) agissent sur chaque bit ; int("101", 2) convertit une chaîne binaire en entier.
Coup de pouce 2 (début de solution)
Remplacer (par une boucle sur les caractères) les X du masque par des 0 donne un entier ou_masque à combiner avec | ; les remplacer par des 1 donne un entier et_masque à combiner avec &.
Exercice 5 — La mémoire #
La mémoire a \(2^{36}\) cases : une liste est impossible. Utiliser un dictionnaire memoire (adresse \(\to\) valeur), exécuter le programme et afficher la somme des valeurs. Tester sur l’exemple du défi, puis sur vos données.
Partie 2#
Une fois la partie 1 validée, lire la suite de l’énoncé sur le site.
Résumé de la partie 2 (à lire après avoir validé la partie 1)
Le programme ne fonctionne toujours pas : la version du système utilisée par le port est en fait un « décodeur d’adresses ».
-
Le masque agit maintenant sur l’adresse, et la valeur est écrite telle quelle : un
0laisse le bit de l’adresse inchangé, un1le force à 1, unXest flottant et prend les deux valeurs. -
Avec \(k\) bits flottants, la valeur est donc écrite dans \(2^k\) adresses. La réponse demandée est la même.
★★ Exercice 6 — Un masque sur les adresses #
Écrire une fonction adresses(masque, adresse) qui renvoie la liste de toutes ces adresses.
Pour tester, on utilise un autre programme inventé :
mask = 000000000000000000000000000000X10X0X
mem[5] = 4
mask = 0000000000000000000000000000000X0000
mem[1] = 10
Vérification : mem[5] = 4 écrit dans 8 adresses, de 16 à 53 ; la réponse finale est 48 (une adresse est écrite deux fois).
Coup de pouce
Travailler sur des chaînes de 36 caractères : en_binaire(adresse, 36) (encadré de la partie 1) donne l’écriture binaire complétée par des zéros. On construit d’abord la chaîne résultat avec ses X.
Coup de pouce 2 (début de solution)
Pour remplacer les \(k\) bits X : énumérer les entiers de \(0\) à \(2^k - 1\) et utiliser leurs \(k\) bits ; ou bien écrire une fonction récursive qui remplace le premier X par 0, puis par 1.
★★★ Exercice 7 — Défi : combien de X au maximum ? #
Afficher le plus grand nombre de X parmi les masques de vos données, et le nombre total d’adresses écrites. Pourquoi le dictionnaire reste-t-il de taille raisonnable alors que la mémoire a \(2^{36}\) cases ?
Approfondissement#
★★ Exercice 8 — Approfondissement 1 : énumérer les adresses par récursivité #
-
À la main, pour le modèle
X1X: remplacer le premierXpar0, puis par1, et recommencer sur chacune des deux chaînes obtenues. Dessiner l’arbre de ces remplacements et lister les chaînes finales dans l’ordre. -
Écrire une fonction récursive
remplacer_x(modele)qui renvoie la liste de toutes les chaînes obtenues en remplaçant lesXpar des0et des1. Quel est le cas de base ?Coup de pouce
Cas de base : le modèle ne contient aucun
X(premier_indicerenvoie \(-1\)) ; la liste ne contient alors que le modèle lui-même.Coup de pouce 2 (début de solution)
Sinon, avec
i = premier_indice(modele, "X"), construire par tranchesmodele[:i] + "0" + modele[i + 1:]et la même chaîne avec"1", puis concaténer les deux listes renvoyées par les appels récursifs. -
Utiliser
remplacer_xpour réécrireadresses; vérifier les 8 adresses de l’exemple de la partie 2 et la réponse48. -
Combien d’appels récursifs pour un modèle contenant \(k\) fois
X? Comparer avec la version qui énumère les entiers de \(0\) à \(2^k - 1\).