On peut voir Brainfuck comme un jeu d'instructions d'un processeur. Et comme je n'ai plus de quoi programmer en VHDL sous la main (et que de toutes façons je ne sais plus faire), j'ai décidé de faire autrement.
J'ai décidé de simuler un processeur. Objet. En Java.
Eh bien croyez-le ou non, ça marche. C'est complètement inefficace, mais ça marche… J'arrive même à sortir la fractale !
On crée un processeur, avec un jeu d'instructions défini (chacune d'entre elles est une classe)
On crée 2 mémoires (Brainfuck est une machine de Harvard)
On charge le programme dans la mémoire programme, qui est donc représenté par une suite d'instructions
On crée deux registres : le pointeur-mémoire et le pointeur-programme, tous deux initialisés à 0
On exécute l'instruction courante, puis on incrémente le pointeur-mémoire, jusqu'à arriver au bout de la mémoire-programme
Chaque instruction peut modifier l'état des registres ou des mémoires
Le programme est fonctionnel mais crade : programme en dur dans la classe CLI, aucun commentaire (cela dit, ils ne devraient pas être très importants pour commencer, il faut juste savoir qu'on part de la classe nommée CLI.java), classes limitées aux concepts réellement utilisés, etc.
J'avais une version super-optimisée en Java aussi, que j'avais fait pour ce même topic sur le SdZ. Si je la retrouve, je vous la poste et on verra la différence de perfs
Autrement, quelqu'un a déjà tenté le compilateur inverse ? Je veux dire, générer le BrainFuck correspondant à un code écrit en Pascal (ou autre langage relativement simple) ?
Ça ne correspond pas exactement à ce que tu décris, mais j'ai une fois utilisé un algorithme génétique pour générer du BF, c'était foireux, mais j'arrivais à générer un code qui affichait "Salut !" en assez peu de temps. Ma plus grande réussite, générer le code suivant en BF :
1
2
scanf("%d %d",&a,&b);printf("%d %d",a,b);
Ce qui n'est pas si évident que ça en fin de compte.
Je me garde de vous montrer mon code (en rust), cette chose étant une abommination truffée de multiples et obscurs hacks. Si j'ai la motivation de 1. mettre à jour le code avec la dernière version de Rust 2. le nettoyer…
Voici mon code complet, gérant les boucles. Comme annoncé, j’ai dû reprendre en profondeur la structure du programme. L’interpréteur utilise son ruban de données en entrelaçant, dans cet ordre :
un ruban contenant le code,
un ruban de « circulation » permettant de passer du code aux données et vice-versa (un va-et-vient incessant qui plombe sérieusement les performances),
un ruban temporaire,
et le ruban de données du programme interprété.
Comme les spécifications du Brainfuck sont très flottantes, les miennes figurent dans le code source.
L’interpréteur fonctionne sur des exemples comme le « Hello world », mais pas avec ceux de ctcp et Alex qui affichent des listes de carrés. Je ne sais vraiment pas d’où vient le problème, ayant vérifié tout le code et celui-ci marchant pour d’autre exemples avec boucles (y-compris imbriquées).
#!/chemin/vers/un/interpréteur/brainfuck________________________________________________________________________________This is a Brainfuck interpreter written in Brainfuck⋅Thus it requires a Brainfuck implementation to be run correctly⋅The following assumptions are made on the Brainfuck implementation usedto run this interpreter: — the tape does not wrap and is initialized with zeros; — either negative numbers or integer wrapping is supported; — EOF is traduced by either −1 or the “no change” policy⋅The specifications of the interpreted language are drawn from those of theimplementation running this interpreter; with the following alterations: — the tape memory is a quarter of that of this interpreter; — there shall be less instructions than a quarter of the number of cells in the interpreter’s tape; — there shall be less nested loops than the maximal value of a cell; — EOF is traduced by −1; — the source code follows an ASCII‐compatible charset; — each opening bracket must match a single closing bracket; otherwise an error “out of tape” will be issued if the implementation running this interpreter produces this kind of errors⋅________________________________________________________________________________structure of the tape: ( code | ptr | tmp | data )*codes: −1 = “marked” rbracket (ie⋅ which has to be translated back to code 2) 0 = end or “active” lbracket (ie⋅ beginning of a loop currently being executed) 1 = lbracket 2 = rbracket 3 = rmove 4 = lmove 5 = dot 6 = minus 7 = comma 8 = plusLEXING>>>>>-,+[-<+>>+++++++++++++[-<------->]< (91 decrementations)[<+> :not a lbracket--[<+> :not a rbracket>++++++[-<+++++>]<+ (31 incrementations)[<+> :not a rmove++[<+> :not a lmove++++++++++++++[<+> :not a dot+[<+> :not a minus+[<+> :not a comma+[<[-]> :not a plus (ie⋅ nothing)[-]<<<<]]]]]]]]>>>>-,+]rewind:<<<<<[<<<<]init first cells:+> 0 > 0 > 0 >_ >+PARSING<[switch instruction code:“marked” rbracket?+[>-]>[>]< (test code=0 in ptr with tmp=0)[<+++> :set code 2 (accounting for the current parsing state)-]+<lbracket?--[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)->+>[<-]<[<]> (test data=0 in tmp with ptr=0)<+ {>[->>>>+<<<<]<}>>>>[[{>[->>>>+<<<<]<}>>>>]{>[->>>>+<<<<]<}>>>>] {>[-<<<<+>>>>]<}<<<<[{>[-<<<<+>>>>]<}<<<<]{>[->>>>+<<<<]<}>>>>-[+{>[-<<<<+>>>>]<}<<<<{>[-<<<<+>>>>]<}<<<<-]+ (switch position & transfer tmp) if data was =0:>[-<<+>> :restore code 1 for the lbracket+ :init the nested loops counter (stored in tmp) to 1 jump to the matching rbracket:[[->>>>+<<<<] :transfer counter<>>>>[<<<<->>>>-]+ (move right)>+<-< :ptr=0 & counter augmented by 1 if code≠1 → decrement counter:-[>>-<]>[<]<>>-<< :counter restored if code≠2 → increment counter:-[>>+<]>[<]<++>+> :ptr=1]<<---->> :set code −1 for a marked rbracket (accounting for the current parsing state); this prevents the rbracket from being evaluated during this very parsing loop <->]< else:[<-> :mark the lbracket as active-]+-]+<rbracket?-[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)->+>[<-]<[<]> (test data=0 in tmp with ptr=0)<+ {>[->>>>+<<<<]<}>>>>[[{>[->>>>+<<<<]<}>>>>]{>[->>>>+<<<<]<}>>>>] {>[-<<<<+>>>>]<}<<<<[{>[-<<<<+>>>>]<}<<<<]{>[->>>>+<<<<]<}>>>>-[+{>[-<<<<+>>>>]<}<<<<{>[-<<<<+>>>>]<}<<<<-]+ (switch position & transfer tmp) if data was =0:>[-<<- :mark the current rbracket to jump back to it jump to the last active lbracket:[><<<<[>>>>-<<<<-]+ (move left)<]+ :set code 1 for a normal lbracket jump back to the rbracket:+[->>>>>[<<<<->>>>-]+ (move right)<+]>><->]< else:[<++ :restore code 2 for the rbracket jump to the last active lbracket:[><<<<[>>>>-<<<<-]+ (move left)<]-- :account for the current parsing state in the lbracket’s code>-]+-]+<rmove?-[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)>>>>[<<<<->>>>-]+ (move right)>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)-]+<lmove?-[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)<<<<[>>>>-<<<<-]+ (move left)>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)-]+<dot?-[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)>>.<<>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)-]+<minus?-[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)>>-<<>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)-]+<comma?-[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)>>[-]-,<< :enforce −1 for EOF>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)-]+<plus?-[>-]>[>]< (test code=0 in ptr with tmp=0)[>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)>>+<<>>>>[[>>>>]>>>>]<<<<[<<<<]>>>>-[+<<<<<<<<-]+ (switch position between code/data)-]+<++++++++>>>>>[<<<<->>>>-]+ (move right)<]
C’est une excellente idée. Interpréter du Brainfuck avec du TI-Basic (soit un langage presque aussi pourri), j’aime. En fait, en oubliant les langages ésotériques, le TI-Basic est le plus proche du Brainfuck que je voie (même en assembleur, on peut appeler des fonctions…).
Étant donné que les caractères en nombre, ça n'existe pas vraiment sur une calculatrice, à la place, la liste L1 fait office d'entrée standard.
Il est possible de convertir un caractère en nombre, et réciproquement. C’est même toi qui choisit les codes des caractères, du coup tu peux prendre l’ASCII au lieu de la page de code bizarre de la calculatrice !
D'ailleurs, si vous avez d'autres micro-optimisations, je suis preneur.
Les lignes 7-8, 9-10, 26-27 et 35-36 se condensent avantageusement en une seule à chaque fois.
La variable Ans rend souvent des services appréciables, par exemple lignes 25-27 et 34-36.
J'ai codé ma petite implémentation rapide (non optimisée et code fait à la va-vite). Il se peut qu'il y ait des bugs… (si vous pouviez me les signaler, merci).
J'ai défini un petit langage (appelé LBL (langage Brainfuck lisible)) où chaque instruction Brainfuck est traduit par un mot. C'est plus simple à lire ; ce langage a aussi quelques autres améliorations. Il peut ensuite être compilé en Brainfuck, qui est ensuite lui-même compilé par mon compilateur Brainfuck écrit en C++. Il est assez performant et peut afficher un ensemble de Mandelbrot en une dizaine de secondes…
Ceci est en quelque sorte une version bêta. Je pense après l'améliorer, donc si vous trouvez des bugs ou avez des idées d'améliorations, je suis preneur…
packageinfo.kisai.brainfuck;importjava.io.IOException;importjava.io.InputStream;importjava.io.InputStreamReader;/** * Interpréteur BrainFuck * @author SpaceFox */publicclassBrainFuck{// Paramètres de la machineprivateinttailleMemoire;privateinttailleValeur;// Variablesprivatechar[]programme;privateStringBuildersortie=newStringBuilder();privatechar[]memoireTab;privateintpointeurMaxMemoire;privatechar[]programmeCompile;privateint[]parametresCompile;/** * Constructeur unique.<br/> * Initialise la mémoire et les tailles maximales. * @param tailleMemoire la taille max de la mémoire, en cases. Aucune taille max si <code>null</code>. * @param tailleValeur la taille max d'une case. Aucune taille max si <code>null</code>. */publicBrainFuck(IntegertailleMemoire,IntegertailleValeur){if(tailleMemoire==null){this.tailleMemoire=Integer.MAX_VALUE;initMemoire(30000);// Purement arbitraire, sera augmenté si besoin}else{this.tailleMemoire=tailleMemoire;initMemoire(tailleMemoire);}if(tailleValeur==null){this.tailleValeur=Character.MAX_VALUE;}else{this.tailleValeur=tailleValeur;}}publicvoidsetProgramme(char[]programme){this.programme=programme;}publicvoidsetProgramme(Stringprogramme){setProgramme(programme.toCharArray());}/** * Pour debug. Utile pour les versions étendues (verif de la transformation). * @return le programme en brainfuck. */publicStringgetProgramme(){returnString.valueOf(programme);}/** * Exécute le programme et renvoie le résultat sous forme de chaîne. Les entrées sont lues sur l'entrée passée en * paramètre. * @param entreeStream un flux d'entrée quelconque. * @return le résultat du programme. */publicStringexecute(InputStreamentreeStream){compile();intlongueurProgramme=programmeCompile.length;intiInstruction=0;intpointeur=0;do{switch(programmeCompile[iInstruction]){case'+':case'-':memoireTab[pointeur]=incrementeMemoire(pointeur,parametresCompile[iInstruction]);break;case'>':case'<':pointeur=incrementePointeur(pointeur,parametresCompile[iInstruction]);break;case'0':resetMemoire(pointeur);break;case'a':addition(pointeur);break;case'm':copie(pointeur);break;case'.':// sortie.append(getMemoireAt(pointeur));System.out.print(getMemoireAt(pointeur));break;case',':memoireTab[pointeur]=litEntree(entreeStream);break;case'[':iInstruction=sautConditionnelComp(iInstruction,pointeur);break;case']':iInstruction=retourConditionnelComp(iInstruction,pointeur);break;default:// Autres cas --> ignorés (commentaires)break;}// Pour debug// System.out.println(// " instruction n°" + iInstruction// + " " + programmeCompile[iInstruction]// + " (" + parametresCompile[iInstruction]// + ") ==> pointeur " + pointeur// + " --> valeur " + Integer.valueOf(memoireTab[pointeur]));iInstruction++;}while(iInstruction<longueurProgramme);returnsortie.toString();}privatevoidcompile(){char[]progSansComm=preProcesseur(supprimeCommentaires());finalinttailleProg=progSansComm.length;char[]progTmp=newchar[tailleProg];int[]paramTmp=newint[tailleProg];// CompilationintiProg=0;inttailleProgComp=0;intcompteur=0;charinstr;charinstrPrec='\u0000';while(iProg<tailleProg){instr=progSansComm[iProg];// Instructions cumulablesif((instr!=instrPrec)&&(instrPrec=='+'||instrPrec=='-'||instrPrec=='>'||instrPrec=='<')){progTmp[tailleProgComp]=instrPrec;paramTmp[tailleProgComp]=compteur;tailleProgComp++;compteur=0;}// Instructions à ajouter directif(instr=='['||instr==']'||instr=='.'||instr==','||instr=='0'||instr=='a'||instr=='m'){progTmp[tailleProgComp]=instr;tailleProgComp++;compteur=0;}// Modif du compteur si besoinif(instr=='+'||instr=='>'){compteur++;}elseif(instr=='-'||instr=='<'){compteur--;}instrPrec=instr;iProg++;}// Correspondancesfor(intiCrochetOuvrant=0;iCrochetOuvrant<tailleProgComp;iCrochetOuvrant++){if(progTmp[iCrochetOuvrant]=='['){intnbCrochets=1;intiCrochetFermant=iCrochetOuvrant;while(nbCrochets>0){iCrochetFermant++;if(progTmp[iCrochetFermant]=='['){nbCrochets++;}elseif(progTmp[iCrochetFermant]==']'){nbCrochets--;}}// Correspondance [ --> ]paramTmp[iCrochetOuvrant]=iCrochetFermant;// Correspondance ] --> [paramTmp[iCrochetFermant]=iCrochetOuvrant;}}// Copie dans les tableaux générauxprogrammeCompile=newchar[tailleProgComp];parametresCompile=newint[tailleProgComp];System.arraycopy(progTmp,0,programmeCompile,0,tailleProgComp);System.arraycopy(paramTmp,0,parametresCompile,0,tailleProgComp);// System.out.println("Taille compilée : " + tailleProgComp);}/** * Récupère le caractère dans la mémoire. Initialise la mémoire jusqu'à ce pointeur si besoin. * @param pointeur l'adresse de la case à récupérer. * @return le contenu de cette case. */privatechargetMemoireAt(intpointeur){if(pointeur>pointeurMaxMemoire){initMemoire(pointeur);}returnmemoireTab[pointeur];}/** * Incrémente le pointeur de manière sécurisée. * @param pointeur la valeur actuelle du pointeur. * @return <code>pointeur + 1</code> ou <code>0</code> en cas de dépassement de la taille max de la mémoire. */privateintincrementePointeur(intpointeur,inti){return((pointeur+i)%tailleMemoire);}/** * Incrémente la valeur de la case mémoire de manière sécurisée. * @param pointeur l'adresse de la case à incrémenter. * @return <code>valeur + 1</code> ou <code>0</code> en cas de dépassement de la taille max d'une case. */privatecharincrementeMemoire(intpointeur,inti){return(char)((getMemoireAt(pointeur)+i)%tailleValeur);}/** * Lit un caractère sur l'entrée.<br/> * Un problème de lecture ne provoque pas d'erreur mais le caractère <code>0x0000</code> * @param entreeStream le flux d'entrée sur lequel lire le caractère. * @return le premier caractère de l'entrée, ou <code>0x0000</code> en cas d'erreur. */privatecharlitEntree(InputStreamentreeStream){InputStreamReaderentreeReader=newInputStreamReader(entreeStream);char[]entree=newchar[1];try{entreeReader.read(entree);}catch(IOExceptione){entree[0]=0;}returnentree[0];// La seule qui existe}/** * Donne l'index de l'instruction suivante, selon le résultat du saut conditionnel. * @param iInstruction l'index de l'instruction courante. * @param pointeur le pointeur pour la condition * @return l'index de l'instruction courante si la valeur à la case indiquée par le pointeur est différente de * <code>0x0000</code>.<br/> * L'index de l'instruction suivant le crochet fermant correspondant sinon. */privateintsautConditionnelComp(intiInstruction,intpointeur){if(getMemoireAt(pointeur)==0){returnparametresCompile[iInstruction];}returniInstruction;}/** * Donne l'index de l'instruction suivante, selon le résultat du saut conditionnel. * @param iInstruction l'index de l'instruction courante. * @param pointeur le pointeur pour la condition * @return l'index de l'instruction courante si la valeur à la case indiquée par le pointeur est égale à * <code>0x0000</code>.<br/> * L'index de l'instruction suivant le crochet ouvrant correspondant sinon. */privateintretourConditionnelComp(intiInstruction,intpointeur){if(getMemoireAt(pointeur)!=0){returnparametresCompile[iInstruction];}returniInstruction;}/** * Initialise la mémoire (remplissage par des 0) jusqu'à la taille indiquée.<br/> * Seule la partie entre la taille courante et la taille spécifiée est initialisée, pour pouvoir agrandir la mémoire * en cas de "mémoire infinie".<br/> * Cette méthode ne fait rien si la taille voulue est plus petite que la taille actuelle. * @param taille la taille voulue pour la mémoire. */privatevoidinitMemoire(inttaille){char[]newMemoireTab=newchar[taille];if(memoireTab==null){memoireTab=newchar[taille];}System.arraycopy(memoireTab,0,newMemoireTab,0,memoireTab.length);// Initialisation du restefor(inti=memoireTab.length;i<taille;i++){newMemoireTab[i]=0;}memoireTab=newMemoireTab;pointeurMaxMemoire=memoireTab.length-1;}privatechar[]supprimeCommentaires(){// System.out.println("Taille avec commentaires : " + programme.length);char[]tmp=newchar[programme.length];intprogTailleSansComm=0;for(charc:programme){if(c=='+'||c=='-'||c=='>'||c=='<'||c=='['||c==']'||c=='.'||c==','){tmp[progTailleSansComm++]=c;}}char[]retour=newchar[progTailleSansComm];System.arraycopy(tmp,0,retour,0,progTailleSansComm);// System.out.println("Taille sans commentaires : " + progTailleSansComm);returnretour;}privatechar[]preProcesseur(char[]supprimeCommentaires){Stringstr=newString(supprimeCommentaires);// System.out.println("Avant préprocesseur : " + str);// RAZ cellulestr=str.replaceAll("\\[-\\]","0");// Additionstr=str.replaceAll("\\[->\\+<\\]","a");// Déplacementstr=str.replaceAll(">\\[-\\]<\\[->\\+<\\]","m");// System.out.println("Après préprocesseur : " + str);returnstr.toCharArray();}privatevoidresetMemoire(intpointeur){memoireTab[pointeur]=0;}privatevoidaddition(intpointeur){memoireTab[pointeur+1]+=memoireTab[pointeur];memoireTab[pointeur]=0;}privatevoidcopie(intpointeur){memoireTab[pointeur+1]=memoireTab[pointeur];memoireTab[pointeur]=0;}}
Et puis parce que c'est vous, un bout de code pour lancer le programme. Comme d'hab, la flemme, vous collez directement le code Brainfuck dans le code :
packageinfo.kisai.brainfuck;/** * * @author SpaceFox */publicclassCLI{/** * @param args the command line arguments */publicstaticvoidmain(String[]args){longdebut=System.currentTimeMillis();/* * Test BrainFuck */BrainFuckbf=newBrainFuck(30000,65536);bf.setProgramme("++++++++++[ >+++++++>+++++++ +++>+++>+<<<<-]>++.>+.+++ ++++..+++.>++.<<+++++++++++++++.> .+++.------ .- -------.>+.>.");Stringsortie=bf.execute(System.in);System.out.println(sortie);System.out.println("Temps : "+(System.currentTimeMillis()-debut)+" ms");}}
Alors on commence par remarquer que c'est programmé en français… passons.
Que fait ce programme ?
Bonne question, dont voici la réponse (honteusement repiquée et recompilée de ce que j'avais écris il y a environ 4 ans sur le Site du Zéro) :
Où perds-on du temps ?
Toutes ces optimisations sont validées à grand coups de profiler !
Des optimisations efficaces :
On perds un temps monstrueux à chercher le crochet correspondant à chaque fois qu'on en trouve un. C'est ridicule, puisque brainfuck n'est pas auto-modifiant (vu que programme et données sont dans deux espaces mémoire séparés) ; donc la correspondance entre crochets est fixe. Y'a qu'à la calculer avant de lancer le programme, et hop ! ça va beaucoup plus vite !
Les listes et maps pour gérer la mémoire et le programme, c'est très "objet", mais quand on passe son temps à ne se servir que de ça (des millions d'appels, j'exagère pas) c'est pas très rapide. Donc, on remplace la gestion par des tableaux, et hop c'est plus rapide ! En plus c'est pas vraiment plus compliqué, sauf peut-être la fonction d'initialisation de la mémoire (et encore).
Là on arrive un peu à la limite de la chose : le profiler me dit que je passe surtout du temps (environ la moitié) dans la méthode principale, qui contient surtout un switch…
Comment améliorer ça ?
En tirant partie des propriétés du langage.On va transformer un peu le truc : au lieu d'avoir une série d'instructions, on va avoir une série d'instructions avec un paramètre chacune.
Au lieu d'écrire + n fois de suite, on va avoir une instruction + avec comme paramètre n : au lieu d'ajouter 1 n fois, on ajoute n une fois, ce qui prends approximativement n fois moins de temps
Seules les instructions +-> et < peuvent se chaîner.
Par contre on profite aussi des paramètres pour les crochets [ et ] : quand on est dans ce cas, le paramètre n'est plus le nombre de fois où il faut répéter l'opération, mais l'emplacement du crochet correspondant.
On évite une structure en plus (mais ça ne change pas grand-chose en fait). On remarque que du coup, on a plus besoin que de 6 symboles au lieu de 8 :
-(n) revient à faire +(-n)
<(n) revient à faire >(-n)
On gagne pratiquement rien en performances, mais un peu en simplicité dans le code du "compilateur".
La perte de temps à compiler est très largement compensée par le gain en rapidité : sur un programme très lourd comme celui qui affiche la GPL (plus d'1Mo de code BF…) on gagne encore du temps.
Plus d'optimisations ?
Là, je pense qu'on arrive au bout de ce qu'on peut faire en restant sur un interpréteur Java.
On peut encore gagner quelques pourcents en supprimant la gestion de la mémoire circulaire (environ 10 %) et des cases de mémoire circulaires (environ 10% aussi)
Des optimisations qui n'en sont pas
On peut sans doute faire mieux avec le "compilateur", mais en même temps on s'en fout : il n'est appelé qu'une fois, et est déjà très très rapide : ~ 3 ms sur Mandelbrot, et 35 ms pour les Mo de code source du programme qui affiche la GPL.
Les astuces à la con comme coller des "final" partout, ça sert strictement à rien (dans ce genre de programme en tous cas)
Les "inline" en Java non plus (c'est pour ça que j'ai laissé les méthodes dans le switch de la version optimisée : on ne gagne rien à déplacer le corps des méthodes dans le switch) [Avec la JVM Java 8, il semblerait même qu'on perde quelques pourcentages en performances].
Les "modulo" au lieu des "if" pour gérer la mémoire circulaire. J'ai mis des modulo parce que avec +n c'est plus simple à gérer qu'avec un if.
Et partant de là, est-ce qu'on peut encore plus optimiser ?
Oui ! En implémentant ceci :
Nettoyage du code avant compilation (pour éviter les interférences avec les caractères de commentaires).
Les tableaux programmes "compilés" font maintenant la taille réelle, et pas la taille du programme d'origine avec plein de cases vides.
Détection de la remise à 0 d'une case, pattern [-]
Détection de l'addition : m[1] = m[0] + m[1] et m[0] = 0 ; pattern [->+<]
Détection de la copie d'octet : m[1] = m[0] et m[0] = 0 ; pattern >[-]<[->+<]
Et les performances ?
J'ai testé sur deux programmes calculatoires :
La fractale, qui passe de 6 minutes 30 à 17.5 secondes, soit un facteur 22.
Les tours de Hanoï, qui passe de 142 à 1.7 secondes, soit un facteur 83 !
Ce dernier programme utilise massivement des structures qui sont fortement optimisées à la compilation (la réinitialisation d'une case, en particulier, est appelée plusieurs dizaines de millions de fois) ; d'où le résultat.
Bon alors après avoir laissé ça en suspens quelques jours, je reviens et il y a déjà plein de monde …
Je répète ce que j'ai dit dans le topic sur le tuto BF : je suis en train (mais doucement hein …) de créer un compilateur BF. Mais un "vrai" compilateur. C'est-à-dire que je ne passe pas par un langage intermédiaire type C/C++, mais je crée direct du code assembleur.
Pour accélérer un peu les choses (parce que sinon on ne gagne strictement rien par rapport à un BF->C->executable), j'utilise les registres comme cellule.
Du coup, je suis limité en cellule.
La taille des cellules en BF étant celle d'un octet, j'utilise donc les registres 8 bits. Donc pour l'instant, j'ai 8 cellules. Avec des rotations sur les registres, je peux monter à 16 (mais ça sera un peu plus tard, parce que ça va être un peu le boxon). Je suis donc très loin du "tableau infini".
Après ce long préambule viens ma question : suis-je dans le coup ?
Dit autrement : est-ce que vous allez me hurler dessus parce que je n'ai que 16 cases ?
En fait, théoriquement je peux monter au dessus de 16, comme en C, en utilisant de la mémoire. Sauf qu'à ce moment là les registres ne serviront que comme "zone tempon" pour accélérer le programme. Or, dire que je ne serais pas capable de rivaliser en terme d'optimisation avec un compilateur qui date d'avant ma naissance, ce n'est pas de la modestie, c'est du réalisme.
Donc, mon compilateur, une fois fini, est-ce que ça vous intéresse ?
@Maëlan : Tu as des '-' et des '.' dans tes commentaires, tu les as pris en compte?
Que nenni ! Unicode est fantastique !
J’avais eu des idées comme ça aussi. Un code bilingue, ce serait rigolo.
@ SpaceFox : Les premières optimisations que tu présentes sont déjà faites (de façon plus ou moins automatique) par un certain nombre d’implémentations, par exemple le code de ctcp. Par contre, la détection de motif est nettement plus intéressante.
Je pense que ça peut encore s’améliorer en ajoutant des motifs reconnus, en particulier celui de la duplication ([->+>+<<]), et en rendant tout ça plus générique (par exemple, si je déplace une valeur de deux cases vers la droite ou de trois cases vers la gauche, ça serait chouette que ça soit reconnu quand même — je me sers massivement de déplacements de quatre cases dans mon propre code).
Autre idée, on peut aussi faire de l’élimination de branches mortes (utile pour du code généré entre autres), mais je ne suis pas certain que le gain soit significatif. Par exemple, on sait qu’en sortant d’une boucle, la cellule actuelle est nulle (du coup […][Ce code ne sera jamais exécuté, ça fait un super-commentaire.]).
En tout cas, tes résultats sont assez impressionnants.
N'étant un développeur ASM expérimenté, j'ai modifié mon code précédent pour générer de l'assembleur (nasm). La seule optimisation qu'il y a, c'est la fusion lorsqu'il y a plusieurs +, -, > ou < à la suite. inc ecx devenant add ecx, 5
J'ai une solution rapide (et probablement pas super propre) en Haskell. Je précise que ça fait vraiment pas longtemps que j'ai commencé à apprendre le langage et je suis très loin d'avoir tout compris.
Par contre, j'arrive pas à faire tourner l'exemple de Mandelbrot. Pourtant, toutes mes fonctions sont récursives terminales, donc je suppose que c'est lié à la paresse du haskell. Si c'est bien le cas, j'aimerais bien savoir s'il y a une bonne solution pour résoudre le problème.
Je me suis posé une question débile : puisqu'on peut voir Brainfuck comme un processeur, combien d'opérations par secondes l'interpréteur construit peut-il traiter ?
Réponse : entre 60 et 90 millions d'opérations / seconde selon les instructions et l'OS sur mon PC (Core i5 3570 à 3.6/3.7 Ghz pendant le test). Soit entre 40 et 60 cycles par instruction Brainfuck (ça vous donne une idée de la quantité d'opérations traitées par les programmes de fractale ou de résolution des tours de Hanoï).
Le compteur d'instructions ne ralentit pas vraiment (environ 1/30ème de ralentissent, ce qui est cohérent avec les chiffres ci-dessus).
Plus surprenant, je voulais voir ce que ça donnait dans ma VM Linux (à la base pour voir le vrai rendu des tours de Hanoï)… eh bien c'est 10 à 15 % plus rapide !
typeoptimization_token=MoveAddofint*int(* AddTo n m ajoute m fois la valeur de la cellule courante à la cellule de distance n *)|AddToofint*inttypetoken=Addofint|Moveofint|Print|Read|Assignofint|Loopoftokenlist|Macroofoptimization_tokenletstring_of_t(ops:token):string=letopenPrintfinletrecto_stringindent=function|Movex->sprintf"%sMove(%d)"indentx|Addi->sprintf"%sAdd(%d)"indenti|Print->sprintf"%sPrint"indent|Read->sprintf"%sRead"indent|Assignx->sprintf"%sAssign(%d)"indentx|Loopnodes->sprintf"%sLoop <<\n%s\n%sLoop >>"indent(String.concat"\n"(List.map(to_string("| "^indent))nodes))indent|Macro(MoveAdd(x,y))->sprintf"%sMoveAdd(%d, %d)"indentxy|Macro(AddTo(x,y))->sprintf"%sAddTo (%d, %d)"indentxyinto_string""ops(* Afin d’implémenter une mémoire circulaire (faute d’avoir une mémoire infinie :-() *)letrecnew_pos(key:int)(p:int)(len:int):int=if(p+key)>=0then(p+key)modlenelselen+(p+key)modlen(* optimiser les boucles dans lesquelles la somme des mouvements égale zéro, où la cellule de départ de boucle est décrémentée de -1 et qui ne contiennent que des instructions Move et Add. *)letoptimize_simple_loop(instruction:token):tokenlist=letrecaux(content:tokenlist)(count_move:int):tokenlist=matchcontentwith|[]->Assign0::[](* On ignore les incrémentations sur la cellule d’entrée de la boucle, que l’on remet à zéro à la fin (voir deux lignes plus haut). *)|Macro(MoveAdd(x,y))::xswhenx+count_move=0->auxxs0|Addx::xs->auxxs0|Macro(MoveAdd(x,y))::xs->Macro(AddTo(count_move+x,y))::auxxs(count_move+x)|Movex::xs->auxxs(count_move+x)|other::xs->failwith(string_of_t(other)^"optimize_simple_loop isnt applied to a simple-loop statement")inmatchinstructionwith|Loopxs->auxxs0|_->failwith"optimize_simple_loop isnt applied to a loop statement"letis_simple_loop(instruction:token):bool=letrecaux(content:tokenlist)(count_move:int)(count_add:int)=matchcontentwith|[]->count_move=0&&count_add=(-1)(* count_add permet de compter le nombre le nombre d’incrémentations qui sont faites sur la cellule d’entrée de la boucle *)|Macro(MoveAdd(x,y))::xswhenx+count_move=0->auxxs0(count_add+y)(* Un Add x isolé est forcément devant toute instruction < ou >, on est donc sur la cellule d’entrée de la boucle *)|Addx::xs->auxxs0(count_add+x)|Macro(MoveAdd(x,y))::xs->auxxs(count_move+x)count_add|Movex::xs->auxxs(count_move+x)count_add|_->falseinmatchinstructionwith|Loopcontent->auxcontent00|_->falseletrecoptimize(instructions:tokenlist):tokenlist=matchinstructionswith|[]->[]|Movex::Addy::xs->Macro(MoveAdd(x,y))::optimizexs|Loopxs::ys->letmin_optimized=Loop(optimizexs)inifis_simple_loopmin_optimizedthen(optimize_simple_loopmin_optimized)@optimizeys(* pas joli joli :/ *)elsemin_optimized::optimizeys|Assign0::Addx::xs->Assignx::optimizexs|other::xs->other::optimizexsletlen=30000letmemory=Array.makelen0letcursor=ref0letreceval(program:tokenlist):unit=letaux'=function|Macro(MoveAdd(x,y))->cursor:=new_posx!cursorlen;memory.(!cursor)<-memory.(!cursor)+y;|Movex->cursor:=new_posx!cursorlen|Addx->memory.(!cursor)<-memory.(!cursor)+x|Print->print_char(Char.chrmemory.(!cursor))|Read->letc=Scanf.scanf" %c"(funx->x)inmemory.(!cursor)<-Char.codec|Assignx->memory.(!cursor)<-x;|Macro(AddTo(x,y))->leti=new_posx!cursorleninmemory.(i)<-memory.(i)+y*memory.(!cursor);|Loopys->whilememory.(!cursor)<>0doevalysdoneinList.iteraux'programletparse(channel:in_channel):tokenlist=letrecparse_increments(stream:charStream.t)(acc:int):charStream.t*int=matchstreamwithparser|[<''+';r>]->parse_incrementsr(acc+1)|[<''-';r>]->parse_incrementsr(acc-1)|[<'l;r>]->([<'l;r>],acc)|[<>]->([<>],acc)inletrecparse_movements(stream:charStream.t)(acc:int):charStream.t*int=matchstreamwithparser|[<''>';r>]->parse_movementsr(acc+1)|[<''<';r>]->parse_movementsr(acc-1)|[<'l;r>]->([<'l;r>],acc)|[<>]->([<>],acc)inletrecaux(stream:charStream.t):tokenlist=matchstreamwithparser|[<''+';r>]->let(s,n)=parse_incrementsr1inAddn::auxs|[<''-';r>]->let(s,n)=parse_incrementsr(-1)inAddn::auxs|[<''>';r>]->let(s,n)=parse_movementsr1inMoven::auxs|[<''<';r>]->let(s,n)=parse_movementsr(-1)inMoven::auxs|[<''.';r>]->Print::auxr|[<'',';r>]->Read::auxr|[<''[';r>]->letcontent=auxrinifcontent=[Add(-1)]thenAssign0::auxrelseLoopcontent::auxr|[<'']';r>]->[]|[<'l;r>]->auxr|[<>]->[]inaux(Stream.of_channelchannel)let_=lettokens=parse(open_inSys.argv.(1))inletoptimized=optimizetokensin(* List.iter (fun x -> print_endline(string_of_t x)) optimized; *)(*afficher l’ast*)evaloptimized
Un peu dans l'idée de ce qu'a fait Guiloo : le code suivant permet d'imiter au plus proche le comportement du Brainfuck en Haskell, sauf pour les crochets qui sont remplacés par la fonction loop. Pour utiliser le Brainskell (ie le sous ensemble de Haskell qui équivaut au BF) , il suffit de changer la définition de codeBF et de compiler. Celle donnée actuellement correspond à ,[-+-].>+<. J'écrirais probablement un petit convertisseur BF -> Brainskell pour voir un peu les performances.
Une petite version C++ d'un interpréteur très légèrement optimisé sur les branchements (mais vraiment très légèrement, d'autant que les maps, c'est pas l'idée du siècle). J'ai choisi une bande circulaire pour rendre la partie interprétation noexcept.
J'ai essayé de rendre l'ensemble le plus robuste possible aux erreurs d'exécution (c'est ce qui m'a pris le plus de temps au pifomètre) et de trouver les endroits qui peuvent encore poser problème (signalé en annotations dans le code), tout en écrivant des contrats basiques sur les fonctions (et en mettant des indices sur les raisons qui font que le contrat est valide). L'ensemble me semble const-correct.
(Petite note : un fichier qui contiendrait des caractères pas trop catholiques ne passerait pas forcément bien dans la moulinette, vous êtes prévenus ).
#include <cassert>#include <algorithm>#include <fstream>#include <iostream>#include <string>#include <iterator>#include <array>#include <stdexcept>#include <map>usingstr_citer=std::string::const_iterator;usingjumps=std::map<str_citer,str_citer>;/* Returns : true if c is a BF instruction false otherwise Exception : noexcept*/inlineboolis_an_inst(charc)noexcept{returnc=='+'||c=='-'||c=='>'||c=='<'||c=='.'||c==','||c=='['||c==']';}str_citercompute_jumps(std::stringconst&prg,str_citerstart,jumps&from_to,jumps&to_from);voidextract_program(std::ifstream&ifs,std::string&program);voidinterpret(std::stringconst&program,jumpsconst&from_to,jumpsconst&to_from)noexcept;intmain(intargc,char*argv[]){if(argc!=2)throwstd::invalid_argument{"use : ./exec file_name"};std::ifstreamifs{argv[1],std::ios::in};//warning ! remaining exception : std::string allocation (x2)if(!ifs)throwstd::runtime_error{std::string{argv[1]}+" does not exists"};//at this point :// - ifs is validstd::stringprg{};extract_program(ifs,prg);//at this point :// - ifs is not valid anymore// - prg is a pure BF programjumpsfrom_to,to_from;//exception : unmatched ]if(compute_jumps(prg,std::begin(prg),from_to,to_from)!=std::end(prg))throwstd::runtime_error{"This program is not valid (unbalanced braces)."};//at this point :// - prg is a pure BF program// - from_to contains jumps from [ to ] and no occurrence of std::end(prg)// - to_from contains jumps from ] to [ and no occurrence of std::end(prg)interpret(prg,from_to,to_from);return0;}/* Pre : - ifs valid Post : - prg is pure BF - ifs is not valid anymore Exceptions : - IO exceptions on getline (unlikely) - allocation exception on program += and line allocation - remove_if cannot raise exception here - clear is noexcept - resize should not allocate (size is inferior)*/voidextract_program(std::ifstream&ifs,std::string&program){usingstd::begin;usingstd::end;program.clear();for(std::stringline{};getline(ifs,line);)program+=line;//we only keep BF instructionsautoe=std::remove_if(begin(program),end(program),[](charc){return!is_an_inst(c);});//remove final moved charactersprogram.resize(e-begin(program));}/* Pre : - prg is pure BF - std::begin(prg) <= it <= std::end(prg) Post : - from_to contains jumps from [ to ] * and no occurrence of std::end(prg) - to_from contains jumps from ] to [ * and no occurrence of std::end(prg) * between Pre(it) and Return value Returns : - iterator to ] or std::end(prg) (if this last happens in a rec call it is an error) Exceptions : - runtime_error (parsing) - allocation error on inserting in the map - recursive call exceptions*/str_citercompute_jumps(std::stringconst&prg,str_citerit,jumps&from_to,jumps&to_from){//invariant : - std::begin(prg) <= it <= std::end(prg)// - from_to and to_from contains all visited jumpswhile(it!=std::end(prg)){if(*it=='['){str_citerbegin{it};it=compute_jumps(prg,it+1,from_to,to_from);//exception : unmatched [if(it==std::end(prg))throwstd::runtime_error{"This program is not valid (unbalanced braces)."};assert(begin!=std::end(prg));//obviousassert(it!=std::end(prg));//obvious//guarantee : from_to an to_from does not contain std::end(prg)from_to[begin]=it;to_from[it]=begin;}elseif(*it==']')returnit;++it;}returnstd::end(prg);}/* Pre : - prg is a pure BF program - from_to contains jumps from [ to ] and no occurrence of std::end(prg) - to_from contains jumps from ] to [ and no occurrence of std::end(prg) Post: - nothing special : standard output as been updated with output Exceptions : noexcept*/voidinterpret(std::stringconst&program,jumpsconst&from_to,jumpsconst&to_from)noexcept{usingstd::begin;usingstd::end;std::array<char,30000>data;data.fill(0);decltype(data)::iteratordp{begin(data)};str_citerpc{begin(program)};// Invariant : - std::begin(program) <= pc <= std::end(program) (== causes end loop)// - std::begin(data) <= dp < std::end(program)while(pc!=end(program)){charconstbyte{*dp};switch(*pc){case'+':++(*dp);break;case'-':--(*dp);break;case'>':++dp;break;case'<':--dp;break;case'.':std::cout<<byte;break;case',':*dp=static_cast<char>(getchar());break;case'[':if(!*dp)pc=(*from_to.find(pc)).second;break;case']':if(*dp)pc=(*to_from.find(pc)).second;break;default:assert(false&&"Something really bad happened");}if(dp==end(data))dp=begin(data);++pc;}}
Sur ma machine, ça affiche Mandelbrot en 34s (en même temps c'est de l'interprété pur à La RacheTM ). Et l'exécution en DEBUG marche aussi évidemment .
Connectez-vous pour pouvoir poster un message.
Connexion
Pas encore membre ?
Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte