Contexte et problème
Dans les langages dynamiques comme Python, Ruby ou JavaScript, les objets sont souvent présentés comme des tables de hachage où chaque nom d’attribut pointe vers une valeur. Le code d’exemple montre la création d’un objet obj avec les attributs a et b dans les trois langages. Cette représentation naïve implique que chaque lecture obj.a déclenche plusieurs accès mémoire : recherche du bucket, comparaison de clés, puis lecture de la valeur. L’article indique que « littéralement chaque opération a.b exécute des centaines ou des milliers d’instructions CPU », ce qui pénalise fortement les interprètes à haute fréquence d’accès.
Mécanisme des maps (shapes)
Pour réduire ce coût, les machines virtuelles introduisent des maps, aussi appelées shapes, hidden classes ou layouts. Une map décrit la disposition en mémoire des attributs d’un objet : elle associe chaque nom à un offset fixe dans un tableau de stockage. Le texte précise que la map agit comme un « index de base de données » ou un cache en écriture, permettant de connaître l’emplacement d’un attribut sans recomposer la table de hachage. Chaque objet commence avec une EMPTY_MAP et un tableau storage vide. Lorsqu’un attribut est ajouté, la VM crée une nouvelle map en suivant les arêtes d’un arbre persistant (trie), garantissant l’immuabilité de chaque version de la map.
class Instance(Instance):
def __init__(self, cls: "Class"):
super().__init__(cls)
self.map = EMPTY_MAP
self.storage = []
def _read_dict(self, fieldname):
index = self.map.get_index(fieldname)
if index == -1:
return MISSING
return self.storage[index]
def _write_dict(self, fieldname, value):
index = self.map.get_index(fieldname)
if index != -1:
self.storage[index] = value
else:
# création d'une nouvelle map et extension du stockage
...
Le fragment ci‑dessus montre comment la lecture utilise directement l’index retourné par la map, évitant ainsi la seconde charge mémoire.
Impact sur les performances
Le principal gain provient de la capacité à réutiliser l’offset précédemment calculé grâce à l’inline cache. Une fois que le JIT ou l’interpréteur a confirmé que la map d’un objet n’a pas changé, il peut accéder à l’attribut en une instruction de chargement direct. Le texte souligne que tant que la map reste stable, le coût se réduit à un seul accès, alors que la version hash‑table nécessite plusieurs chargements dépendants. Cette optimisation se traduit par une amélioration notable du débit d’instructions, surtout dans les boucles où les mêmes attributs sont accédés de façon répétée.
Limites et considérations
Le modèle repose sur l’immuabilité de la map : chaque mutation d’attribut entraîne la création d’une nouvelle map, ce qui implique une surcharge de gestion de structures persistantes. Si un objet change fréquemment de forme, les bénéfices s’estompent car le système doit constamment invalider les caches et allouer de nouvelles maps. De plus, la complexité de l’arbre de maps peut croître avec le nombre d’attributs distincts, augmentant la consommation mémoire. Enfin, l’article note que les implémentations réelles dans les VM commerciales sont souvent plus complexes, avec des cas particuliers qui ne sont pas couverts par le prototype simplifié présenté.