Puisque le but de ce projet est d'apprendre, l'utilisation d'un outil ou d'une solution de remplacement ne répond clairement pas à la question.
Tout d'abord, un avertissement : je ne suis pas un programmeur x86 - j'ai fait une quantité décente de travail dans les environnements embarqués et maintenant (avec les téléphones mobiles) les puces ARM. Passons aux choses sérieuses...
Il y a deux façons de rendre votre interpréteur plus rapide : Faire en sorte qu'il optimise le code BF lui-même, et faire en sorte que l'interpréteur lui-même soit optimisé. Je recommanderai un peu des deux en une seule étape ci-dessous.
Pour autant que je sache, x86 consacre beaucoup d'espace de teinture à fournir des propriétés de branchement rapide relativement impressionnantes. Probablement à cause de cela, j'ai vu plusieurs compilateurs (y compris gcc) produire des branches imbriquées en faveur de véritables tables de saut pour x86. Les tables de saut semblent attrayantes en théorie, mais en réalité, x86 optimise si bien les techniques traditionnelles que la pensée conventionnelle big-O ne s'applique pas dans la plupart des cas. C'est pourquoi les développeurs x86 de longue date vous diront que si vous voulez calculer la rapidité d'un code, vous devez l'écrire, l'exécuter et le chronométrer.
Malgré la vitesse à laquelle les branchements peuvent se produire sur x86, il vaut toujours la peine de dépenser un peu d'overhead pour ne pas brancher. Les instructions BF étant de toute façon très simples, cela peut prendre la forme de "faire la plupart des instructions en une seule fois puisque c'est plus rapide qu'un autre branchement". Certaines de ces opérations peuvent même être effectuées en parallèle par le processeur là où le branchement ne peut pas l'être. (x86 a suffisamment d'unités de décodage et d'exécution dans un seul noyau pour que cela soit probablement réalisable).
Le contrôle des erreurs (comme le wrapping) est un autre élément qui va réduire vos performances. Le fait de le conserver vous cause des problèmes de performance (pas majeurs pour l'instant) mais surtout vous empêche d'optimiser.
De plus, votre programme est très générique. Il vous permettra d'utiliser n'importe quelle valeur maximale pour l'enveloppement. S'il s'agissait d'une puissance de deux, il suffirait d'effectuer un ET par bit (très rapide puisqu'il représente un cycle CPU sur pratiquement toutes les plates-formes modernes) équivalent au wrapping au lieu de comparer et de bifurquer. Le diable se cache dans les détails de l'écriture d'interprètes vraiment rapides - chaque petit détail que vous ajoutez rend le tout beaucoup plus complexe.
À cette fin, je recommande de rationaliser ce que fait votre interpréteur BF (faire en sorte qu'il s'enroule à une puissance de deux pour les valeurs, par exemple). L'astuce du AND binaire seul et le fait de forcer le wrap à être l'option (comme c'est le cas dans la plupart des langages de programmation modernes pour les overflows/underflows) supprime déjà au moins deux branches.
Une fois qu'on s'en est occupé, cela permet une optimisation intéressante : on peut se débarrasser des instructions BF et faire en sorte que la machine exécute des instructions différentes qui conviennent mieux à un interprète.
Considérez Java : Java n'interprète pas. Il effectue un JIT dans un langage entièrement différent.
Je recommande d'appliquer la même logique. Vous l'avez déjà fait un peu en donnant à vos instructions une valeur qui leur est associée.
Je recommande de créer une instruction contenant les informations suivantes :
- le montant à ajouter à la valeur sur le pointeur de données (ou soustraire -- la soustraction consiste simplement à ajouter un nombre négatif)
- le montant à ajouter à la valeur de le pointeur de données (à nouveau, ou soustraire)
- un pointeur vers l'instruction suivante à exécuter
- une valeur qui est comparée à la valeur du pointeur de données avant il est changé
La boucle de l'interpréteur se transforme alors en la logique suivante : ajouter la valeur des données dans l'instruction à la valeur du pointeur de données ajouter les données adresse la valeur dans l'instruction au pointeur de données lui-même si la valeur du pointeur de données correspond à notre valeur de comparaison place le pointeur d'instruction sur le nouveau pointeur d'instruction continuer (vers la boucle d'interprétation suivante) si la valeur de comparaison est une valeur spéciale (par exemple, vous pourriez choisir 0x80000000) gérer les trucs d'entrée/sortie augmenter l'adresse de l'instruction d'une unité
L'interprétation initiale devient maintenant plus délicate : Les instructions peuvent maintenant combiner +, -, <, > et parfois même [, et généralement ] dans la même instruction unique. La première branche de la boucle peut être représentée sans branchement si vous êtes malin à ce sujet (et encore plus efficacement avec un peu d'assembleur coincé ou quelques intrinsèques du compilateur). Vous pouvez informer le compilateur qu'il est peu probable que la deuxième branche soit atteinte (l'entrée/sortie est le goulot d'étranglement dans ce cas, pas la vitesse d'interprétation, donc même si vous faites beaucoup d'entrée/sortie, une petite branche non optimisée ne fera pas la différence).
Il faut faire attention à l'état de fin de course. La dernière instruction de votre programme BF interprété doit maintenant TOUJOURS rendre le pointeur d'instruction NULL pour que la boucle se termine.
L'ordre dans lequel l'interprétation a lieu est important car -/+ est fait avant <> est fait avant [ est fait avant ] est fait avant I/O. Donc, >+ est deux instructions interprétées alors que +> est une seule.
Au-delà de l'ingénierie d'un interpréteur rapide à boucles serrées comme celui-ci, vous vous intéressez à l'analyse de code plus complexe, auquel cas vous vous orientez vers la conception d'un compilateur et vous vous éloignez moins d'un simple interpréteur. Ce n'est pas quelque chose que je fais tous les jours, mais le livre "Compiler Construction" de Louden a été une très bonne lecture pour moi, mais ce ne sera pas un petit projet de cette façon. A moins que vous ne vouliez sérieusement rendre cette chose ridiculement rapide et au final probablement compiler du code, je resterais à l'écart des grosses optimisations percutantes.
J'espère vous avoir donné une idée à essayer et à tester qui vous conduira à d'autres étapes d'optimisation. Je n'ai pas essayé tout cela moi-même, donc ce ne sont que des conjectures basées sur l'expérience passée. Cependant, même si cela ne s'avère pas plus rapide, vous aurez acquis une certaine expérience dans la réécriture de code BF sur une architecture relativement différente de celle d'un interpréteur BF classique.
P.S. Superbe question !