Homemade Pixels

Des pixels frais qui sortent du four le blog de MrHelmut.

Théorie de l’émulation : la recompilation dynamique (dynarec)

Dans mes précédentes pérégrinations sur l’émulation, je vous expliquais à quel point j’étais épaté par les émulateurs modernes tels que Dolphin, PCSX2, nullDC, etc. En effet, ces émulateurs ont une approche particulière pour émuler une machine : ils recompilent les programmes à la volée. Enfin, pas tout à fait, mais on appelle communément cette méthode la recompilation dynamique (abrégée dynarec en anglais), ou parfois compilation just in time (JIT). Quel est l’intérêt de cette méthode ? C’est ultra performant.

Je vous propose donc d’explorer la recompilation dynamique dans ce billet et de comparer son fonctionnement à la méthode plus classique de l’interpréteur. Mais avant toutes choses, un peu de théorie sur l’émulation.

Les figures étant plutôt larges, je vous recommande une lecture sur une colonne, si vous n’êtes pas déjà dessus.

Pourquoi et comment émuler ?

Dans le fabuleux monde des processeurs, il existe une myriade de familles d’architectures, comme par exemple les suivantes :

  • x86 (les CPU Intel et AMD qui équipent vos PC/Mac)
  • PowerPC (toutes les consoles de salon de la génération actuelle)
  • ARM (dans tous vos smartphones et consoles portables)
  • Et bien d’autres plus obscures/obsolètes (MIPS, Alpha, Z80, 68000, SPARC…)

Chacune de ces architectures a une structure différente et notamment des instructions différentes qui sont parfois codées d’une façon différente. Cela fait beaucoup d’apparition du mot “différent” dans une seule phrase et c’est justement là le problème : un programme conçu pour une architecture donnée, ne pourra absolument pas être compris par une autre.

Intervient alors l’émulation, qui consiste à faire tourner un programme d’une architecture source sur une architecture cible, à travers un programme (l’émulateur) qui tourne sur l’architecture cible. Par définition, un émulateur “simule” ou “reproduit à l’identique” le comportement d’une architecture source.

Dans le domaine de l’émulation, il existe deux grandes approches :

  • L’interprétation, qui consiste à interpréter un programme source indépendamment de la cible;
  • La recompilation dynamique, qui consiste à transcrire un programme source en son équivalent cible.

Mais tout d’abord, qu’est-ce qu’un programme, et que fait un processeur ?

Un programme est une liste d’instructions à exécuter par le processeur. Ces instructions sont codées suivant un format propre à l’architecture pour laquelle le programme a été conçu. Le job d’un processeur est alors de décoder les instructions, et de les exécuter. Ci-dessous, un schéma très simplifié pour résumer.

emu1

Interpréter un programme

Un interpréteur consiste en un programme qui va simuler à haut niveau le comportement électronique d’un processeur. C’est-à-dire que si un processeur source a une instruction câblée qui fait “A = A + B”, alors l’émulateur possèdera une fonction (faites en C par exemple) qui simule cette opération. L’émulateur est ensuite compilé en langage machine pour l’architecture cible. Ci-dessous, un beau schéma.

emu2

La recompilation dynamique

L’autre méthode part d’un principe assez bête. Si toutes les architectures processeur ont bien des jeux d’instructions différents, la plupart des instructions ont des instructions (ou combinaisons d’instructions) équivalentes d’une architecture à l’autre.

Exemple

Le code binaire PowerPC “1C86000A” est décodé par l’instruction “mulli 4, 6, 10″, une simple opération de multiplication entre registres (r4 = r6 * 10).

Or, il existe en x86 une instruction équivalente, “imul 10″ (qui fait AX = AL * 10) et qui sera codée par le code binaire “F60A” (la longueur d’un code binaire n’a rien à voir avec sa performance; sa taille dépend de l’architecture).

Faire de la recompilation dynamique consiste alors à exécuter “F60A” dès lors que l’émulateur rencontrera “1C86000A”, comme décrit ci-après. La plupart du temps, on passe par un buffer d’instructions que l’on exécutera d’un seul tenant au moment opportun (par souci de performance et pour réserver la mémoire d’échange).

emu3

Concrètement, comment fait-on et où est la différence ?

Prenons un exemple d’itération d’un émulateur et comparons les deux approches. En entrée, notre émulateur reçoit le code PowerPC “1C86000A” (décrit précédemment). Le tableau ci-dessous détailles les différences.

dynarec

J’ai pris quelques raccourcis pour illustrer les différences (notamment masqué les overhead générés), mais je pense que l’essentiel est souligné: les performances. Parmi les raccourcis que j’ai pris, il y a toute une gymnastique de traitement des registres. En effet, l’architecture PowerPC possède quelques 64 registres alors que x86 en possède 2 à 4 fois moins suivant les versions. Il faut donc assurer la cohérence. Il faut aussi récupérer les résultats des opérations et les attribuer aux bons registres. Bref, la correspondance 1:1 est un cas idéal.

On peut aussi effectuer des optimisations. Parfois, une suite bien précise d’instructions peut être factorisée par une seule autre instruction (par exemple SSE 2/3/4). Un émulateur qui irait encore plus loin examinerait le code généré dans le buffer et chercherait des bouts à factoriser.

Il est aussi possible d’identifier des bouts de code récurrents issus des SDK. Par exemple, on peut connaître les instructions que génèrent certains SDK pour certaines fonctions et si on détecte ce pattern d’instructions, au lieu de chercher à le traduire, on exécute un code pré-fait optimisé. C’est très efficace sur les opérations fréquentes que font les SDK (typiquement, les opérations sur les string et autres lib helper).

Pour résumer, les avantages et inconvénients des deux approches

Le débat se situe essentiellement entre facilité-lenteur VS difficulté-performance.

Interpréteur

Avantages :

  • Indépendant de la cible, donc facilement portable;
  • Plus facile à débugger.

Inconvénient :

  • Lent.

Recompilation dynamique

Avantage :

  • Performant.

Inconvénients :

  • Devoir connaître sur le bout des doigts l’architecture source ET l’architecture cible;
  • Difficile à débugger;
  • Pas portable vers une autre cible sans créer un traducteur vers cette cible (et donc de connaître la nouvelle architecture cible sur le bout des doigts également);
  • Nécessite d’être programmé dans un langage qui permet de générer et d’exécuter des buffers d’instructions bas niveau.

Voilà pour la théorie / vulgarisation (en espérant que les puristes n’ai pas trop eu les poils qui se sont hérissés devant tant de raccourcis).

Si vous n’êtes pas membres wefrag, vos commentaires peuvent être recueillis par mail :

contact

4 commentaires pour “Théorie de l’émulation : la recompilation dynamique (dynarec)”

  1. crevetolog dit :

    Merci pour tes articles, très intéressants, suffisamment fournis et clairs, j’aime beaucoup.

  2. skaven dit :

    crevetolog a dit :
    Merci pour tes articles, très intéressants, suffisamment fournis et clairs, j’aime beaucoup.

    Pas mieux!

  3. tranxen dit :

    Même chose que crevetolog, c’est très instructif, merci.

  4. LeGreg dit :

    Tu pourrais aussi parler des problèmes liés au timing (dans un prochain article) pour l’émulation de vieilles machines.

Laisser un commentaire

Vous devez être connecté avec votre compte Wefrag pour publier un commentaire.