J'ai essayé de comparer les performances du langage d'assemblage en ligne et du code C++, j'ai donc écrit une fonction qui additionne deux tableaux de taille 2000 pour 100000 fois. Voici le code :
#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
for(int i = 0; i < TIMES; i++)
{
for(int j = 0; j < length; j++)
x[j] += y[j];
}
}
void calcuAsm(int *x,int *y,int lengthOfArray)
{
__asm
{
mov edi,TIMES
start:
mov esi,0
mov ecx,lengthOfArray
label:
mov edx,x
push edx
mov eax,DWORD PTR [edx + esi*4]
mov edx,y
mov ebx,DWORD PTR [edx + esi*4]
add eax,ebx
pop edx
mov [edx + esi*4],eax
inc esi
loop label
dec edi
cmp edi,0
jnz start
};
}
Voici main()
:
int main() {
bool errorOccured = false;
setbuf(stdout,NULL);
int *xC,*xAsm,*yC,*yAsm;
xC = new int[2000];
xAsm = new int[2000];
yC = new int[2000];
yAsm = new int[2000];
for(int i = 0; i < 2000; i++)
{
xC[i] = 0;
xAsm[i] = 0;
yC[i] = i;
yAsm[i] = i;
}
time_t start = clock();
calcuC(xC,yC,2000);
// calcuAsm(xAsm,yAsm,2000);
// for(int i = 0; i < 2000; i++)
// {
// if(xC[i] != xAsm[i])
// {
// cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
// errorOccured = true;
// break;
// }
// }
// if(errorOccured)
// cout<<"Error occurs!"<<endl;
// else
// cout<<"Works fine!"<<endl;
time_t end = clock();
// cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";
cout<<"time = "<<end - start<<endl;
return 0;
}
Ensuite, j'exécute le programme cinq fois pour obtenir les cycles du processeur, qui peuvent être considérés comme du temps. A chaque fois, j'appelle une seule des fonctions mentionnées ci-dessus.
Et voilà le résultat.
Fonction de la version d'assemblage :
Debug Release
---------------
732 668
733 680
659 672
667 675
684 694
Average: 677
Fonction de la version C++ :
Debug Release
-----------------
1068 168
999 166
1072 231
1002 166
1114 183
Average: 182
Le code C++ en mode release est presque 3,7 fois plus rapide que le code assembleur. Pourquoi ?
Je suppose que le code assembleur que j'ai écrit n'est pas aussi efficace que ceux générés par GCC. Cela veut-il dire que je ne dois pas me fier aux performances du langage assembleur que j'ai écrit de mes mains, me concentrer sur le C++ et oublier le langage assembleur ?
30 votes
A peu près. L'assemblage codé à la main est approprié dans certaines circonstances, mais il faut s'assurer que la version assembleur est effectivement plus rapide que ce qui peut être réalisé avec un langage de plus haut niveau.
162 votes
Vous pourriez trouver instructif d'étudier le code généré par le compilateur, et essayer de comprendre pourquoi il est plus rapide que votre version assembleur.
35 votes
Ouais, on dirait que le compilateur est meilleur que toi pour écrire du asm. Les compilateurs modernes sont vraiment très bons.
20 votes
Avez-vous regardé l'assemblage produit par GCC ? Il est possible que GCC ait utilisé des instructions MMX. Votre fonction est très parallèle - vous pourriez potentiellement utiliser N processeurs pour calculer la somme en 1/Nème du temps. Essayez une fonction où il n'y a aucun espoir de parallélisation.
11 votes
Hm, j'aurais attendu d'un bon compilateur qu'il fasse cela ~100000 fois plus vite...
2 votes
Pas de surprise. Si tu fais ça, fais-le au moins bien.
1 votes
Cela n'aura pas trop d'importance dans cette application, mais pour votre information future, lorsque vous mesurez les cycles d'horloge pour un programme comme celui-ci où vous n'avez pas d'entrée utilisateur, vous devriez vraiment faire en sorte que votre processus se règle sur une priorité temps réel avant de commencer à mesurer les cycles d'horloge pour obtenir une mesure beaucoup plus précise (bien que cela ne change pas la conclusion de vos résultats ici ;).
3 votes
@PlasmaHH : en fait j'ai été assez surpris, mais Clang/LLVM n'optimise pas la boucle sur
TIMES
loin. Je m'attendais à ce que ce soit simplifié enx[j] += TIMES * y[j]
mais cela ne s'est pas produit. Même en intervertissant les boucles manuellement pour faire la boucle surTIMES
l'intérieur, il ne l'a toujours pas fait. en fumant0 votes
@MatthieuM. : peut-être que des règles de langage obscures empêchent cela ? Ou bien il est temps de rédiger un rapport de bug/amélioration...
1 votes
@PlasmaHH : En fait, cela devient possible avec le
restrict
mais Clang/LLVM ne parvient pas à interchanger les boucles automatiquement et n'optimise que si les boucles sont interchangées...2 votes
L'un des travaux universitaires classiques en matière de conception de processeurs consiste à prendre la sortie non optimisée du compilateur et à modifier manuellement l'assemblage jusqu'à ce qu'il tourne beaucoup plus vite (par exemple, au moins 100 fois plus vite). C'est un exercice amusant et instructif.
2 votes
Par curiosité, quels drapeaux de compilateur avez-vous utilisés dans les deux cas ?
2 votes
Bien sûr, vous devriez faire confiance aux performances de votre assemblage ; il sera exactement ce que vous avez spécifié ! En fait, vous ne devriez pas vous fier aux performances du code généré par le compilateur ; le résultat sera beaucoup plus rapide que ce que vous pensez.
1 votes
@DaxFohl : les performances de l'assemblage ne sont pas faciles à estimer lorsque le goulot d'étranglement d'aujourd'hui est la mémoire (dans la plupart des cas) et non le nombre d'instructions. Les modèles d'accès à la mémoire sont le plus souvent la pièce critique (jouer avec la préextraction, éviter les branches) et le fait d'utiliser le C ou l'assembleur n'est pas si important.
0 votes
Réponse épique d'une autre question : stackoverflow.com/a/2685541/372860
0 votes
Vous devez afficher le code d'assemblage que votre compilateur génère.
3 votes
La seule façon de comparer est de prendre l'assemblage du compilateur, de l'améliorer autant que possible, puis de le comparer. Si vous ne pouvez pas le faire, le compilateur est meilleur que vous et vous vivez dans le monde heureux de "l'inutile vérification de l'ASM". Si vous le pouvez, bienvenue en enfer où vous ne pouvez faire confiance à aucun de vos outils.