Le langage d’assemblage en ligne est-il plus lent que le code C ++ natif?

J’ai essayé de comparer les performances du langage d’assemblage en ligne et du code C ++, j’ai donc écrit une fonction qui ajoute deux tableaux de taille 2000 pour 100 000 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, je lance le programme cinq fois pour obtenir les cycles du processeur, qui peuvent être considérés comme du temps. Chaque fois que j’appelle l’une des fonctions mentionnées ci-dessus uniquement.

Et voici 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 près de 3,7 fois plus rapide que le code assembleur. Pourquoi?

Je suppose que le code d’assemblage que j’ai écrit n’est pas aussi efficace que ceux générés par GCC. Il est difficile pour un programmeur commun comme moi d’écrire du code plus rapidement que son adversaire généré par un compilateur. Cela signifie-t-il que je ne devrais pas faire confiance au langage d’assemblage écrit par mes mains, me concentrer sur le langage d’assemblage?

Oui, la plupart du temps

Tout d’abord, vous commencez par supposer qu’un langage de bas niveau (l’assemblage dans ce cas) produira toujours un code plus rapide que le langage de haut niveau (C ++ et C dans ce cas). Ce n’est pas vrai. Le code C est-il toujours plus rapide que le code Java? Non car il existe une autre variable: le programmeur. La manière dont vous écrivez le code et la connaissance des détails de l’architecture ont une grande influence sur les performances (comme vous l’avez vu dans ce cas).

Vous pouvez toujours produire un exemple où le code d’assemblage fait à la main est meilleur que le code compilé, mais c’est généralement un exemple fictif ou une routine unique, pas un vrai programme de plus de 500 000 lignes de code C ++. Je pense que les compilateurs produiront un meilleur code d’assemblage 95% de fois et parfois, seulement quelques fois, vous devrez peut-être écrire du code d’assemblage pour peu de routines critiques , performantes ou courtes, ou accéder à des fonctionnalités de haut niveau. ne pas exposer Voulez-vous une touche de cette complexité? Lisez cette réponse géniale ici sur SO.

Pourquoi ça?

Tout d’abord parce que les compilateurs peuvent faire des optimisations que nous ne pouvons même pas imaginer (voir cette courte liste ) et ils les feront en quelques secondes (lorsque nous aurons besoin de jours ).

Lorsque vous codez en assembleur, vous devez créer des fonctions bien définies avec une interface d’appel bien définie. Cependant, ils peuvent prendre en compte l’optimisation de l’ensemble du programme et l’ optimisation inter-procédures telles que l’ allocation des registres , la propagation constante , l’élimination des sous-expressions communes , la planification des instructions et autres optimisations complexes non évidentes ( modèle Polytope par exemple). Sur RISC , les gars de l’architecture ont cessé de s’inquiéter de cela il ya plusieurs années (la programmation des instructions, par exemple, est très difficile à régler manuellement ) et les processeurs CISC modernes ont également de très longs pipelines .

Pour certains microcontrôleurs complexes, même les bibliothèques système sont écrites en C au lieu d’assemblage car leurs compilateurs produisent un code final meilleur (et facile à gérer).

Les compilateurs peuvent parfois utiliser eux-mêmes certaines instructions MMX / SIMDx , et si vous ne les utilisez pas, vous ne pouvez tout simplement pas comparer (les autres réponses ont déjà très bien examiné votre code d’assemblage). Juste pour les boucles, voici une courte liste des optimisations de boucle de ce qui est généralement vérifié par un compilateur (pensez-vous que vous pourriez le faire vous-même lorsque votre emploi du temps a été décidé pour un programme C #?) pense que vous devez considérer au moins quelques optimisations simples . L’exemple de livre d’école pour les tableaux consiste à dérouler le cycle (sa taille est connue au moment de la compilation). Faites-le et relancez votre test.

De nos jours, il est également très rare d’utiliser un langage d’assemblage pour une autre raison: la pléthore de processeurs différents . Voulez-vous les soutenir tous? Chacun a une microarchitecture spécifique et des jeux d’instructions spécifiques . Ils ont un nombre différent d’unités fonctionnelles et des instructions d’assemblage doivent être organisées pour qu’elles restnt toutes occupées . Si vous écrivez en C, vous pouvez utiliser PGO, mais dans l’assemblage, vous aurez besoin d’une grande connaissance de cette architecture spécifique (et repensez et refaire tout pour une autre architecture ). Pour les petites tâches, le compilateur le fait généralement mieux, et pour les tâches complexes, le travail n’est généralement pas remboursé (et le compilateur peut quand même faire mieux ).

Si vous vous asseyez et que vous examinez votre code, vous verrez probablement que vous gagnerez plus à redéfinir votre algorithme que de le convertir en assembleur (lisez cet excellent article ici sur SO ), il y a des optimisations de haut niveau (et conseils au compilateur), vous pouvez effectivement appliquer avant d’avoir besoin de recourir au langage d’assemblage. Il vaut probablement la peine de mentionner que, souvent, en utilisant des composants insortingnsèques, vous obtiendrez des gains de performances que vous recherchez et que le compilateur pourra toujours effectuer la plupart de ses optimisations.

Cela dit, même si vous pouvez produire un code d’assemblage 5 à 10 fois plus rapide, vous devriez demander à vos clients s’ils préfèrent payer une semaine de votre temps ou acheter un processeur plus rapide de 50 $ . L’optimisation extrême le plus souvent (et particulièrement dans les applications métier) n’est tout simplement pas nécessaire pour la plupart d’entre nous.

Votre code d’assemblage est exceptionnellement médiocre et peut être amélioré:

  • Vous poussez et ouvrez un registre ( EDX ) dans votre boucle interne. Cela devrait être retiré de la boucle.
  • Vous rechargez les pointeurs de tableau à chaque itération de la boucle. Cela devrait sortir de la boucle.
  • Vous utilisez l’instruction de loop , qui est connue pour être lente sur la plupart des processeurs modernes (peut-être en raison de l’utilisation d’un ancien livre d’assemblage *)
  • Vous ne tirez aucun avantage du déroulement en boucle manuel.
  • Vous n’utilisez pas les instructions SIMD disponibles.

Donc, à moins d’améliorer considérablement votre ensemble de compétences en matière d’assemblage, vous n’avez pas besoin d’écrire du code assembleur pour les performances.

* Bien sûr, je ne sais pas si vous avez vraiment reçu l’instruction en loop d’un ancien livre d’assemblage. Mais vous ne le voyez presque jamais dans le code du monde réel, car chaque compilateur est suffisamment intelligent pour ne pas émettre de loop , vous ne le voyez que dans des livres mauvais et obsolètes.

Avant même de se lancer dans l’assemblage, il existe des transformations de code à un niveau supérieur.

 static int const 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]; } } } 

peut être transformé en via boucle de rotation :

 static int const TIMES = 100000; void calcuC(int *x, int *y, int length) { for (int j = 0; j < length; ++j) { for (int i = 0; i < TIMES; ++i) { x[j] += y[j]; } } } 

ce qui est beaucoup mieux en ce qui concerne la localité de la mémoire.

Cela pourrait être optimisé, faire a += b x fois équivaut à faire a += X * b pour obtenir:

 static int const TIMES = 100000; void calcuC(int *x, int *y, int length) { for (int j = 0; j < length; ++j) { x[j] += TIMES * y[j]; } } 

Cependant, il semble que mon optimiseur préféré (LLVM) n'effectue pas cette transformation.

[edit] J'ai trouvé que la transformation était effectuée si nous avions le qualificatif ressortingct à x et y . En effet, sans cette ressortingction, x[j] et y[j] pourraient être alias au même endroit, ce qui rend cette transformation erronée. [fin éditer]

Quoi qu’il en soit, c’est, je pense, la version C optimisée. C'est déjà beaucoup plus simple. Sur cette base, voici ma fissure à ASM (je laisse Clang le générer, je suis inutile):

 calcuAsm: # @calcuAsm .Ltmp0: .cfi_startproc # BB#0: testl %edx, %edx jle .LBB0_2 .align 16, 0x90 .LBB0_1: # %.lr.ph # =>This Inner Loop Header: Depth=1 imull $100000, (%rsi), %eax # imm = 0x186A0 addl %eax, (%rdi) addq $4, %rsi addq $4, %rdi decl %edx jne .LBB0_1 .LBB0_2: # %._crit_edge ret .Ltmp1: .size calcuAsm, .Ltmp1-calcuAsm .Ltmp2: .cfi_endproc 

Je crains de ne pas comprendre d'où viennent toutes ces instructions, mais vous pouvez toujours vous amuser et essayer de voir comment il se compare ... mais j'utiliserais toujours la version C optimisée plutôt que celle d'assemblage, dans le code. beaucoup plus portable.

Réponse courte: oui.

Réponse longue: oui, sauf si vous savez vraiment ce que vous faites et si vous avez une raison de le faire.

J’ai corrigé mon code asm:

  __asm { mov ebx,TIMES start: mov ecx,lengthOfArray mov esi,x shr ecx,1 mov edi,y label: movq mm0,QWORD PTR[esi] paddd mm0,QWORD PTR[edi] add edi,8 movq QWORD PTR[esi],mm0 add esi,8 dec ecx jnz label dec ebx jnz start }; 

Résultats pour la version Release:

  Function of assembly version: 81 Function of C++ version: 161 

Le code d’assemblage en mode release est presque 2 fois plus rapide que le C ++.

Est-ce que cela signifie que je ne devrais pas faire confiance à la performance du langage d’assemblage écrit par mes mains

Oui, c’est exactement ce que cela signifie, et c’est vrai pour toutes les langues. Si vous ne savez pas comment écrire du code efficace dans le langage X, vous ne devriez pas faire confiance à votre capacité à écrire du code efficace dans X. Si vous voulez un code efficace, vous devez utiliser un autre langage.

L’assemblée est particulièrement sensible à cela, car, bien, ce que vous voyez est ce que vous obtenez. Vous écrivez les instructions spécifiques que vous souhaitez que le processeur exécute. Avec les langages de haut niveau, il existe un compilateur qui peut transformer votre code et supprimer de nombreuses inefficacités. Avec l’assemblage, vous êtes seul.

La seule raison d’utiliser le langage d’assemblage de nos jours est d’utiliser certaines fonctionnalités non accessibles par le langage.

Ceci s’applique à:

  • Programmation du kernel qui doit accéder à certaines fonctionnalités matérielles telles que le MMU
  • Programmation haute performance utilisant des instructions vectorielles ou multimédia très spécifiques non sockets en charge par votre compilateur.

Mais les compilateurs actuels sont assez intelligents, ils peuvent même remplacer deux instructions séparées comme d = a / b; r = a % b; d = a / b; r = a % b; avec une seule instruction qui calcule la division et le rest en une seule fois si elle est disponible, même si C n’a pas cet opérateur.

Il est vrai qu’un compilateur moderne fait un travail incroyable en matière d’optimisation du code, mais je vous encourage tout de même à continuer à apprendre l’assemblage.

Tout d’abord, vous n’êtes pas intimidé , c’est un grand avantage. Vous êtes sur la bonne voie en établissant un profil afin de valider ou d’écarter vos hypothèses de vitesse , vous demandez l’ avis de personnes expérimentées et vous avoir le plus grand outil d’optimisation connu de l’humanité: un cerveau .

À mesure que votre expérience augmente, vous apprendrez quand et où l’utiliser (généralement les boucles les plus serrées et les plus profondes de votre code, après vous être profondément optimisé au niveau algorithmique).

Pour vous inspirer, je vous recommande de consulter les articles de Michael Aarmh (si vous n’avez pas eu de ses nouvelles, il est un gourou de l’optimisation; il a même collaboré avec John Carmack dans l’optimisation du moteur de rendu Quake!)

“Le code le plus rapide n’existe pas” – Michael Aarmh

J’ai changé le code asm:

  __asm { mov ebx,TIMES start: mov ecx,lengthOfArray mov esi,x shr ecx,2 mov edi,y label: mov eax,DWORD PTR [esi] add eax,DWORD PTR [edi] add edi,4 dec ecx mov DWORD PTR [esi],eax add esi,4 test ecx,ecx jnz label dec ebx test ebx,ebx jnz start }; 

Résultats pour la version Release:

  Function of assembly version: 41 Function of C++ version: 161 

Le code d’assemblage en mode release est presque 4 fois plus rapide que le C ++. IMHo, la vitesse du code d’assemblage dépend du programmeur

La plupart des compilateurs de langages de haut niveau sont très optimisés et savent ce qu’ils font. Vous pouvez essayer de vider le code de désassemblage et le comparer avec votre assembly natif. Je pense que vous verrez quelques astuces que votre compilateur utilise.

Juste par exemple, même si je ne suis pas sûr que ce soit plus bien :):

Faire:

 mov eax,0 

coûte plus de cycles que

 xor eax,eax 

qui fait la même chose.

Le compilateur connaît toutes ces astuces et les utilise.

Le compilateur vous a battu. Je vais essayer, mais je ne ferai aucune garantie. Je supposerai que la “multiplication” par TIMES est destinée à rendre le test de performance plus pertinent, que y et x sont alignés sur 16, et que cette length est un multiple non nul de 4. C’est tout à fait vrai de toute façon.

  mov ecx,length lea esi,[y+4*ecx] lea edi,[x+4*ecx] neg ecx loop: movdqa xmm0,[esi+4*ecx] paddd xmm0,[edi+4*ecx] movdqa [edi+4*ecx],xmm0 add ecx,4 jnz loop 

Comme je l’ai dit, je ne fais aucune garantie. Mais je serai surpris si cela peut être fait beaucoup plus rapidement – le goulot d’étranglement ici est le débit de mémoire même si tout est un succès L1.

c’est un sujet très intéressant!
J’ai changé le MMX par SSE dans le code de Sasha
Voici mes résultats:

 Function of C++ version: 315 Function of assembly(simply): 312 Function of assembly (MMX): 136 Function of assembly (SSE): 62 

Le code assembleur avec SSE est 5 fois plus rapide que le C ++

Implémenter aveuglément exactement le même algorithme, instruction par instruction, dans l’assembly est garanti plus lent que ce que le compilateur peut faire.

C’est parce que même la plus petite optimisation du compilateur est meilleure que votre code rigide sans aucune optimisation.

Bien sûr, il est possible de battre le compilateur, surtout si c’est une petite partie localisée du code, j’ai même dû le faire moi-même pour obtenir un env. 4x accélérer, mais dans ce cas, nous devons compter sur une bonne connaissance du matériel et de nombreuses astuces apparemment contre-intuitives.

C’est exactement ce que cela signifie. Laissez les micro-optimisations au compilateur.

J’aime cet exemple car il montre une leçon importante sur le code de bas niveau. Oui, vous pouvez écrire un assemblage aussi rapide que votre code C. Ceci est tautologiquement vrai, mais ne signifie pas nécessairement quelque chose. Clairement, quelqu’un peut le faire, sinon l’assembleur ne connaîtrait pas les optimisations appropriées.

De même, le même principe s’applique lorsque vous montez la hiérarchie de l’abstraction linguistique. Oui, vous pouvez écrire un parsingur en C aussi rapide qu’un script Perl rapide et sale. Mais cela ne signifie pas que parce que vous avez utilisé C, votre code sera rapide. Dans de nombreux cas, les langages de niveau supérieur effectuent des optimisations que vous n’avez peut-être jamais envisagées.

En tant que compilateur, je remplacerais une boucle de taille fixe par de nombreuses tâches d’exécution.

 int a = 10; for (int i = 0; i < 3; i += 1) { a = a + i; } 

produira

 int a = 10; a = a + 0; a = a + 1; a = a + 2; 

et finalement il saura que "a = a + 0;" est inutile donc il va supprimer cette ligne. J'espère que quelque chose dans votre tête maintenant prêt à joindre des options d'optimisation en tant que commentaire. Toutes ces optimisations très efficaces rendront le langage compilé plus rapide.

Dans de nombreux cas, la manière optimale d’effectuer certaines tâches peut dépendre du contexte dans lequel la tâche est exécutée. Si une routine est écrite en langage d’assemblage, il ne sera généralement pas possible de modifier la séquence d’instructions en fonction du contexte. À titre d’exemple simple, considérons la méthode simple suivante:

 inline void set_port_high(void) { (*((volatile unsigned char*)0x40001204) = 0xFF); } 

Un compilateur pour le code ARM 32 bits, étant donné ce qui précède, le rendrait probablement comme quelque chose comme:

 ldr r0,=0x40001204 mov r1,#0 strb r1,[r0] [a fourth word somewhere holding the constant 0x40001204] 

ou peut-être

 ldr r0,=0x40001000 ; Some assemblers like to round pointer loads to multiples of 4096 mov r1,#0 strb r1,[r0+0x204] [a fourth word somewhere holding the constant 0x40001000] 

Cela pourrait être légèrement optimisé en code assemblé à la main, soit:

 ldr r0,=0x400011FF strb r0,[r0+5] [a third word somewhere holding the constant 0x400011FF] 

ou

 mvn r0,#0xC0 ; Load with 0x3FFFFFFF add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF strb r0,[r0+5] 

Les deux approches assemblées à la main nécessiteraient 12 octets d’espace de code plutôt que 16; ce dernier remplacerait un “load” par un “add”, qui exécuterait deux cycles plus rapidement sur une ARM7-TDMI. Si le code devait être exécuté dans un contexte où r0 était ne pas savoir / ne pas faire attention, les versions du langage d’assemblage seraient donc un peu meilleures que la version compilée. D’un autre côté, supposons que le compilateur savait qu’un registre [par exemple r5] allait contenir une valeur située à 2047 octets de l’adresse désirée 0x40001204 [par exemple 0x40001000], et savait qu’un autre registre [par exemple r7] allait pour contenir une valeur dont les bits bas étaient 0xFF. Dans ce cas, un compilateur pourrait optimiser la version C du code pour simplement:

 strb r7,[r5+0x204] 

Beaucoup plus court et plus rapide que le code d’assemblage optimisé à la main. En outre, supposons que set_port_high s’est produit dans le contexte:

 int temp = function1(); set_port_high(); function2(temp); // Assume temp is not used after this 

Pas du tout invraisemblable lors du codage d’un système embarqué. Si set_port_high est écrit en code assembleur, le compilateur devra déplacer r0 (qui contient la valeur de retour de function1 ) ailleurs avant d’appeler le code assembleur, puis déplacer cette valeur sur r0 (puisque function2 attendra son premier paramètre dans r0), le code d’assemblage “optimisé” aurait donc besoin de cinq instructions. Même si le compilateur ne connaissait aucun registre contenant l’adresse ou la valeur à stocker, sa version à quatre instructions (qu’il pourrait adapter pour utiliser tous les registres disponibles, pas nécessairement r0 et r1) battrait l’assembly “optimisé” -language version. Si le compilateur avait l’adresse et les données nécessaires dans r5 et r7 comme décrit précédemment, function1 ne modifierait pas ces registres et pourrait donc remplacer set_port_high avec une seule instruction strb quatre instructions plus petites et plus rapides que l’assemblage “optimisé à la main” code.

Notez que le code assembleur optimisé à la main peut souvent surpasser un compilateur dans les cas où le programmeur connaît le stream de programme précis, mais les compilateurs brillent dans les cas où un morceau de code est écrit avant que son contexte soit connu. invoqué à partir de plusieurs contextes [si set_port_high est utilisé dans cinquante endroits différents du code, le compilateur pourrait décider indépendamment pour chacun de ceux-ci comment le développer].

En général, je dirais que le langage d’assemblage est susceptible de produire les plus grandes améliorations de performances dans les cas où chaque élément de code peut être abordé dans un nombre très limité de contextes et est susceptible de nuire aux performances le code peut être abordé dans de nombreux contextes différents. Interestingly (and conveniently) the cases where assembly is most beneficial to performance are often those where the code is most straightforward and easy to read. The places that assembly language code would turn into a gooey mess are often those where writing in assembly would offer the smallest performance benefit.

[Minor note: there are some places where assembly code can be used to yield a hyper-optimized gooey mess; for example, one piece of code I did for the ARM needed to fetch a word from RAM and execute one of about twelve routines based upon the upper six bits of the value (many values mapped to the same routine). I think I optimized that code to something like:

 ldrh r0,[r1],#2! ; Fetch with post-increment ldrb r1,[r8,r0 asr #10] sub pc,r8,r1,asl #2 

The register r8 always held the address of the main dispatch table (within the loop where the code spend 98% of its time, nothing ever used it for any other purpose); all 64 ensortinges referred to addresses in the 256 bytes preceding it. Since the primary loop had in most cases a hard execution-time limit of about 60 cycles, the nine-cycle fetch and dispatch was very instrumental toward meeting that goal. Using a table of 256 32-bit addresses would have been one cycle faster, but would have gobbled up 1KB of very precious RAM [flash would have added more than one wait state]. Using 64 32-bit addresses would have required adding an instruction to mask off some bits from the fetched word, and would still have gobbled up 192 more bytes than the table I actually used. Using the table of 8-bit offsets yielded very compact and fast code, but not something I would expect a comstackr would ever come up with; I also would not expect a comstackr to dedicate a register “full time” to holding the table address.

The above code was designed to run as a self-contained system; it could periodically call C code, but only at certain times when the hardware with which it was communicating could safely be put into an “idle” state for two roughly-one-millisecond intervals every 16ms.

In recent times, all the speed optimisations that I have done were replacing brain damaged slow code with just reasonable code. But for things were speed was really critical and I put serious effort into making something fast, the result was always an iterative process, where each iteration gave more insight into the problem, finding ways how to solve the problem with fewer operations. The final speed always depended on how much insight I got into the problem. If at any stage I used assembly code, or C code that was over-optimised, the process of finding a better solution would have suffered and the end result would be slower.

C++ is faster unless you are using assembly language with deeper knowledge with the correct way.

When I code in ASM, I reorganize the instructions manually so the CPU can execute more of them in parallel when logically possible. I barely use RAM when I code in ASM for example: There could be 20000+ lines of code in ASM and I not ever once used push/pop.

You could potentially jump in the middle of the opcode to self-modify the code and the behavior without the possible penalty of self-modifying code. Accessing registers takes 1 tick(sometimes takes .25 ticks) of the CPU.Accessing the RAM could take hundreds.

For my last ASM adventure, I never once used the RAM to store a variable(for thousands of lines of ASM). ASM could be potentially unimaginably faster than C++. But it depends on a lot of variable factors such as:

 1. I was writing my apps to run on the bare metal. 2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle. 

I am now learning C# and C++ because i realized productivity matters!! You could try to do the fastest imaginable programs using pure ASM alone in the free time. But in order to produce something, use some high level language.

For example, the last program I coded was using JS and GLSL and I never noticed any performance issue, even speaking about JS which is slow. This is because the mere concept of programming the GPU for 3D makes the speed of the language that sends the commands to the GPU almost irrelevant.

The speed of assembler alone on the bare metal is irrefutable. Could it be even slower inside C++? – It could be because you are writing assembly code with a comstackr not using an assembler to start with.

My personal council is to never write assembly code if you can avoid it, even though I love assembly.

All the answers here seem to exclude one aspect: sometimes we don’t write code to achieve a specific aim, but for the sheer fun of it. It may not be economical to invest the time to do so, but arguably there is no greater satisfaction than beating the fastest comstackr optimized code snippet in speed with a manually rolled asm alternative.

A c++ comstackr would, after optimization at the organizational level, produce code that would utilize the built in functions of the targeted cpu. HLL will never outrun or out-perform assembler for several reasons; 1.) HLL will be comstackd and output with Accessor code, boundary checking and possibly built in garbage collection (formerly addressing scope in the OOP mannerism) all requiring cycles (flips and flops). HLL does an excellent job these days (including newer C++ and others like GO), but if they outperform assembler (namely your code) you need to consult the CPU Documentation -comparisons with sloppy code are most certainly inconclusive and comstackd langs like assembler all resolve down to op-code HLL abstracts the details and does not eliminate them else you app isn’t going to run if it’s even recognize by the host OS.

Most assembler code (primarily objects) are output as “headless” for inclusion into other executable formats with far less processing required hence it will be much faster, but far more unsecure; if an executable is output by the assembler (NAsm, YAsm; etc.) it will still run faster until it completely matches the HLL code in functionality then results may be accurately weighed.

Calling an assembler based code object from HLL in any format will inherently add processing overhead as well in addition to memory space calls using globally allocated memory for variable/constant data types (this applies to both LLL and HLL). Remember that the final output is using the CPU ultimately as its api and abi relative to the hardware (opcode) and both, assemblers and “HLL comstackrs” are essentially/fundamentally identical with the only true exception being readability (grammatical).

Hello world console application in assembler using FAsm is 1.5 KB (and this is in Windows even smaller in FreeBSD and Linux) and outperforms anything GCC can throw out on its best day; reasons are implicit padding with nops, access validation and boundary checking to name a few. The real goal is clean HLL libs and an optimizable comstackr that targets a cpu in a “hardcore” manner and most do these days (finally). GCC is not better than YAsm -it is the coding practices and understanding of the developer that are in question and “optimization” comes after novice exploration and interim training & experience.

Comstackrs have to link and assemble for output in the same opcode as an assembler because those codes are all that a CPU will except (CISC or RISC [PIC too]). YAsm optimized and cleaned up a great deal on early NAsm ultimately speeding up all output from that assembler, but even then YAsm still, like NAsm, produce executables with external dependencies targeting OS libraries on behalf of the developer so mileage may vary. In closing C++ is at a point that is incredible and far more safe than assembler for 80+ percent especially in the commercial sector…

Assembly could be faster if your comstackr generates a lot of OO support code.

Modifier:

To downvoters: the OP wrote “should I … focus on C++ and forget about assembly language?” and I stand by my answer. You always need to keep an eye on the code OO generates, particularly when using methods. Not forgetting about assembly language means that you will periodically review the assembly your OO code generates which I believe is a must for writing well-performing software.

Actually, this pertains to all comstackable code, not just OO.