Stratégies d’amélioration des performances pour VM / interprète?

J’ai écrit une machine virtuelle simple en C, en utilisant un simple commutateur d’instructions, sans aucune instruction de décodage, mais les performances sont terribles.

Pour les opérations arithmétiques simples, la VM est environ 4000 fois plus lente que le code C natif pour les mêmes opérations. J’ai testé avec un groupe de masortingces de 10 millions de longueur, la première consistant en des instructions de programme, des opérations aléatoires + – * /, 2 masortingces contenant des entiers aléatoires et la troisième masortingce étant le stockage cible de l’opération.

Je m’attendais à voir la performance arithmétique chuter de 3 à 4 fois, si bien que «4000x m’a vraiment époustouflé. Même les langages interprétés les plus lents semblent offrir de meilleures performances. Donc, où je me trompe dans mon approche et comment puis-je améliorer les performances sans recourir à la compilation JIT pour coder en machine?

L’implémentation est … fondamentalement la plus simple que j’ai pu trouver:

begin: { switch (*(op+(c++))) { case 0: add(in1+c, in2+c, out+c); goto begin; case 1: sub(in1+c, in2+c, out+c); goto begin; case 2: mul(in1+c, in2+c, out+c); goto begin; case 3: div(in1+c, in2+c, out+c); goto begin; case 4: cout << "end of program" << endl; goto end; default: cout << "ERROR!!!" << endl; } } end: 

UPDATE: Je jouais avec la longueur du programme quand j’ai remarqué que le QElapsedTimer que j’utilisais pour profiler était en fait cassé. Maintenant, je me sers de la fonction clock () de et d’après lui, le goto calculé fonctionne à égalité avec le code natif, peut-être un peu moins. Ce résultat est-il légitime ??? Voici la source complète (c’est moche je sais, c’est juste pour tester après tout):

 #include  #include  #include  #include  using namespace std; #define LENGTH 70000000 void add(int & a, int & b, int & r) {r = a * b;} void sub(int & a, int & b, int & r) {r = a - b;} void mul(int & a, int & b, int & r) {r = a * b;} void div(int & a, int & b, int & r) {r = a / b;} int main() { char * op = new char[LENGTH]; int * in1 = new int[LENGTH]; int * in2 = new int[LENGTH]; int * out = new int[LENGTH]; for (int i = 0; i < LENGTH; ++i) { *(op+i) = i % 4; *(in1+i) = qrand(); *(in2+i) = qrand()+1; } *(op+LENGTH-1) = 4; // end of program long long sClock, fClock; unsigned int c = 0; sClock = clock(); cout << "Program begins" << endl; static void* table[] = { &&do_add, &&do_sub, &&do_mul, &&do_div, &&do_end, &&do_err, &&do_fin}; #define jump() goto *table[op[c++]] jump(); do_add: add(in1[c], in2[c], out[c]); jump(); do_sub: sub(in1[c], in2[c], out[c]); jump(); do_mul: mul(in1[c], in2[c], out[c]); jump(); do_div: div(in1[c], in2[c], out[c]); jump(); do_end: cout << "end of program" << endl; goto *table[6]; do_err: cout << "ERROR!!!" << endl; goto *table[6]; do_fin: fClock = clock(); cout << fClock - sClock << endl; delete [] op; delete [] in1; delete [] in2; delete [] out; in1 = new int[LENGTH]; in2 = new int[LENGTH]; out = new int[LENGTH]; for (int i = 0; i < LENGTH; ++i) { *(in1+i) = qrand(); *(in2+i) = qrand()+1; } cout << "Native begins" << endl; sClock = clock(); for (int i = 0; i < LENGTH; i += 4) { *(out+i) = *(in1+i) + *(in2+i); *(out+i+1) = *(in1+i+1) - *(in2+i+1); *(out+i+2) = *(in1+i+2) * *(in2+i+2); *(out+i+3) = *(in1+i+3) / *(in2+i+3); } fClock = clock(); cout << fClock - sClock << endl; delete [] in1; delete [] in2; delete [] out; return 0; } 

Darek Mihocka a rédigé de manière approfondie et détaillée la création d’interprètes rapides en C portable: http://www.emulators.com/docs/nx25_nostradamus.htm