IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

x86 32-bits / 64-bits Assembleur Discussion :

C++ "classique" plus rapide que l'Assembleur SSE ?


Sujet :

x86 32-bits / 64-bits Assembleur

  1. #1
    Membre averti
    Inscrit en
    Juillet 2006
    Messages
    34
    Détails du profil
    Informations forums :
    Inscription : Juillet 2006
    Messages : 34
    Par défaut C++ "classique" plus rapide que l'Assembleur SSE ?
    Bonjour,

    J'ai écrit une méthode C++ pour multiplier deux quaternions (machins mathématiques servant à représenter des rotations dans l'espace 3D), bref, le code donne quelque chose comme ça:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    void CQuaternion::mult2(const CQuaternion & P)
    	{
    	float x = m_Q[3] * P.m_Q[0] + m_Q[0] * P.m_Q[3] + m_Q[1] * P.m_Q[2] - m_Q[2] * P.m_Q[1];
    	float y = m_Q[3] * P.m_Q[1] - m_Q[0] * P.m_Q[2] + m_Q[1] * P.m_Q[3] + m_Q[2] * P.m_Q[0];
    	float z = m_Q[3] * P.m_Q[2] + m_Q[0] * P.m_Q[1] - m_Q[1] * P.m_Q[0] + m_Q[2] * P.m_Q[3];
    	float w = m_Q[3] * P.m_Q[3] - m_Q[0] * P.m_Q[0] - m_Q[1] * P.m_Q[1] - m_Q[2] * P.m_Q[2];
     
    	m_Q[0] = x ;
    	m_Q[1] = y ;
    	m_Q[2] = z ;
    	m_Q[3] = w ;
     
    	}

    Dans le but d'accélérer les calculs sur mon athlon xp 2200+, j'ai écrit un programme en assembleur, en utilisant nasm, que j'ai intégré dans mon programme C++

    Le tout donne quelque chose comme ça:

    Dans le code c++:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    extern "C" {  // routine de multiplication en assembleur sse
    		void mm_mult_quaternions ( CQuaternion &res, const CQuaternion &op1);
     
    	    }
     
     
    void CQuaternion::mult1(const CQuaternion & P)
    	{
    		mm_mult_quaternions(*this,P);
    	}
    Et l'assembleur:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
     
    segment .text
     
    	global mm_mult_quaternions
     
    %define Q_quaternion ebp+8
    %define P_quaternion ebp+12
     
     
    mm_mult_quaternions:
     
    		push            ebp
                    mov             ebp,esp
     
    		push            eax
                    push            ebx
     
     
    		mov             eax,[Q_quaternion]                    
                    mov             ebx,[P_quaternion]                    
     
     
    	movaps		xmm0,[eax] ; (Q3,Q2,Q1,Q0)
    	movaps		xmm1,[ebx] ; (P3,P2,P1,P0)
    	movaps		xmm5,[sign] ; (-1,1,-1,1)
     
    	movaps xmm4,xmm0
    	shufps xmm4,xmm4, 0xFF ; (Q3,Q3,Q3,Q3)
     
    	mulps xmm4,xmm1		; (Q3P3,Q3P2,Q3P1,Q3,P0) => res1
     
    	movaps xmm3,xmm0
    	shufps xmm3,xmm3,0		; (Q0,Q0,Q0,Q0)
     
    	shufps xmm1,xmm1,0x1B	; (P0,P1,P2,P3)
     
    	mulps xmm3,xmm1		; (P0Q0,P1Q0,P2Q0,P3Q0)
    	xorps xmm3,xmm5			; (-Q0P0,Q0P1,-Q0P2,Q0P3)
     
    	addps xmm4,xmm3			; res2
     
    	movaps xmm3,xmm0
    	shufps xmm3,xmm3,0x55	; (Q1,Q1,Q1,Q1)
     
    	shufps xmm1,xmm1,0xB1	; (P1,P0,P3,P2)
     
    	mulps xmm3,xmm1		; (Q1P1,Q1P0,Q1P3,Q1P2)
     
    	shufps xmm5,xmm5,0xD8	; (-1,-1,1,1)
    	xorps xmm3,xmm5			; (-Q1P1,-Q1P0,Q1P3,Q1P2)
     
    	addps xmm4,xmm3			; => res3
     
    	shufps xmm0,xmm0, 0xAA	; (Q2,Q2,Q2,Q2)
     
    	shufps xmm1,xmm1,0x1B	; (P2,P3,P0,P1)
     
    	mulps xmm0,xmm1		; (Q2P2,Q2P3,Q2P0,Q2P1)
     
    	shufps xmm5,xmm5, 0xD2	; (-1,1,1,-1)
     
    	xorps xmm0,xmm5			;  (-Q2P2,Q2P3,Q2P0,-Q2P1)
     
    	addps xmm4,xmm0			; => res
     
    	movaps [eax],xmm4
     
            pop     ebx
            pop     eax
     
    	mov     esp,ebp
            pop     ebp
     
    	ret
     
     
    		align		16
    sign:	dd		0x00000000
    		dd		0x80000000
    		dd		0x00000000
    		dd		0x80000000
    Les deux donnent un bon résultat.
    L' ennui c'est que la fonction "mult2" donc en C++, est plus rapide que l'autre "mult1" en assembleur utilisant les instruction SSE du processeur.

    Je fais faire 100000 multiplications et je compare le temps entre deux instructions "gettimeofday". La différence est de l'ordre par exemple:

    4634 microsecondes pour mult1 (assembleur SSE)
    3038 microsecondes pour mult2 (pur c++)

    J'utilise gcc 4.1 sur LINUX-Debian etch

    Si quelqu'un peut me dire qu'est-ce qui cloche ?

    Merci d'avance.

  2. #2
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 565
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 565
    Par défaut
    Utilise la commande time, plutôt.

    C'est difficile de te donner une réponse sans se pencher sérieusement sur ton code, mais il faudrait d'une part vérifier tout ton environnement lors de l'exécution. Voir ce qui est déjà chargé dans le cache, estimer la précision de gettimeofday() qui ne doit pas être fameuse en dessous de la seconde non plus, etc.

    Cela dit, il est aujourd'hui connu que les compilateurs actuels savent faire des merveilles en matière d'optimisation, ce qui est souvent difficile à admettre pour les puristes de l'assembleur dont je suis le premier à faire partie :-) En particulier, ton compilo est capable de choisir la meilleure voie en fonction de ta plate-forme et du processeur que tu utilises, car il y a beaucoup de subtilités pas forcément triviales.

    Par exemple, l'instruction LOOPZ qui sert à décrémenter ECX et à boucler vers une étiquette donnée tant que ECX n'est pas nul a été remplacée depuis longtemps par les instructions « DEC ECX - JNZ label », plus fondamentales. Pourquoi ce choix, parce qu'avec l'évolution des CPU, les instructions les plus fréquemment utilisées ont été avantagées au dépit de toutes les autres. Du coup, ces dernières instructions sont toujours plus rapide à elles deux que LOOPZ toute seule.

    Si tu utilises gcc, précise l'option -S pour voir quel genre de code assembleur le compilo produit.

  3. #3
    Membre Expert Avatar de KiLVaiDeN
    Profil pro
    Inscrit en
    Octobre 2003
    Messages
    2 890
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Octobre 2003
    Messages : 2 890
    Par défaut
    Salut,

    Il se peut aussi que ton code C++ ne soit pas compilé pour utiliser les processeurs SSE, et du coup qu'il soit plus rapide de ce fait ?

    Il me semble ( en étant sûr à 80% ) que les calculs sur des registres de type 64bit sont forcément plus lents que des calculs sur les registres normaux, et donc si ton programme C++ compilé n'utilise pas les registres SSE, il peut être plus rapide. Les registres de type SSE sont à utiliser pour le transfert de données larges, mais pour les calculs je ne suis pas certain que ce soit une bonne idée.

    Comme le dit Obsidian, je pense qu'au final la réponse de toutes manières résidera dans le code assembleur généré par ton compilateur, tu auras une piste de comparaison pour comprendre ce qui peut rendre ton code C++ plus rapide.

    A+

  4. #4
    Membre averti
    Inscrit en
    Juillet 2006
    Messages
    34
    Détails du profil
    Informations forums :
    Inscription : Juillet 2006
    Messages : 34
    Par défaut
    Merci pour vos réponses,

    D'abord j'ai essayé de remesurer les temps d'execution des 2 procedures avec "time" et même "times" le rapport est toujours le même avec "mult2" toujours plus rapide.

    J'ai compilé avec gcc et l'option -S pour voir:
    Je ne suis pas trés fort en assembleur mais le code de mult2 ressemble bien à de l'assembleur avec instructions FPU (ce qui semble normal en fin de compte)
    avec un peu plus d'une centaine de lignes.

    Mon code SSE comprend environ une cinquantaine de lignes soit environ la moitié.
    Mais là je rejoins KiLVaiDeN qui dit que les instructions SSE sur registres de 128 bits ( et non 64 bits ) sont forcement plus lentes que sur registres normaux (32 bits si je ne m'abuse à moins que ce ne soit 64 bits pour le FPU, les spécialistes me corrigeront!)

    Et que pour ce type de calcul, celà ne valait certainement pas la peine de recourir aux instructions SSE du processeur.

    il faudrait peut-être faire les sommes des cycles machines de toutes les instructions de chacune des procedures et les comparer pour se rendre compte.

    J'ai lu un document sur la comparaison entre instructions SSE et assembleur FPU au sujet de la multiplication de matrices et en fait, SSE commence à devenir interressant à partir de la multiplication de 2 matrices 4x4, passant à 2 matrices 6x6 on est environ 2x plus rapide.

    J'ai été séduit par la technologie SSE pour optimiser mes calculs mais apparemment c'était pas la peine de s'emm..der avec çà.

    A moins qu'un partisant me prouve le contraire

    A+

  5. #5
    Membre expérimenté

    Inscrit en
    Février 2009
    Messages
    200
    Détails du profil
    Informations forums :
    Inscription : Février 2009
    Messages : 200
    Par défaut Ah ! Sans beurre et sans re ploch...
    Désolé de te répondre dans le cadre d'un OS Microsoft mais la série de commentaires pourra ainsi servir à plusieurs désirant faire de l'optimisation SSE et de la mesure sous d'autres OS actuels.

    Plusieurs choses à savoir à propos d'SSE et du couple assembleur/HLL:

    - Tu ne peux pas mesurer du "temps" SSE car c'est un coprocesseur non synchronisé (contrairement à FPU et, donc, sa légendaire lenteur):
    Il travaille en // de ton CPU, il est donc "transparent" pour le temps et la charge CPU comme les commandes BIBTBL par exemple ou les utilisations directes WDDM des cartes graphiques (qui dans ton cas seraient nettement plus efficaces qu'une utilisation CPU/SSE -> regarde de ce côté là pour bénéficier du support hardware de Linux et les différents SDK de NVIDIA ou AMD/ATI ils ont fait quelques légers efforts dans le domaine).

    - Le SSE est très puissant, relativement simple d'emploi une fois que tu as vu où sont les réels problèmes (rarement dans "SSE" et la plupart du temps dans les outils de mesure ou la logique du codeur. Je te rassure, tu ne t'ennuies pas pour rien).

    - Ce que tu peux mesurer, sous certaines conditions très précises, c'est le temps écoulé entre deux mesures (temps de l'appel et de retour de l'unité de traitement SSE) ce qui ne veut pas dire que le travail est fait ET validé.


    - (Tu l'as fait donc ce n'est pas pour toi que je dis ça) Il te faut IMPERATIVEMENT travailler avec des données alignées sur 128 bits (16 bytes) sinon SSE ne sera pas très aimable avec toi.

    - Dans ton cas, il te faut utiliser SSE sans polluer le cache (la différence de perf est plus que très nette) le choix des instructions est donc limité et de type Movntdq

    - Il te faut mesurer une boucle interne à ton code assembleur et non pas une boucle C++ vers assembleur (ce qui serait ridicule) et faire la mesure à l'intérieur du code assembleur pour la raison suivante:
    Il te faut aussi limiter les changements de contextes au maximum (passer de C++ à assembleur sans arrêt) c'est horriblement coûteux au niveau du noyau (kernel), en temps partagé CPU ainsi qu'en perte de privilèges.

    - Tout ça veut dire: Tout dans la partie assembleur .

    - Il te faut penser en somme de travail // sérialisées (plusieurs traitements sériels en // des traitements CPU).

    - Evite les déplacements de données par une organisation des datas dédiées à ton algo assembleur ; le reste possède, j'imagine, un degré de répétition nettement moins important, donc peu pénalisant (sinon dans l'assembleur pareillement).

    - Utilise largement les 8 ou 16 registres à ta disposition selon que tu es en 32 ou 64 bits, cela te permet de stoker tes "constantes" et de ne pas les recharger à chaque fois puisque ce sont des registres "persistants".

    - Tu peux aussi éviter des déplacements avec des "doubles registres" –1 1 1 –1 / -1 –1 11 pour tes masques… etc.

    Enfin, pour la mesure:

    - Passer en time-critical et, si tu peux travailler sous Seven, augmenter tes privilèges via AvSetMmThreadCharacteristics et AvSetMmThreadPriority n'hésites pas à te créer une clé de registre perso avec des spécifications maximums, ce n'est pas pour offrir c'est pour manger sur place !

    Pour mémoire mais tu trouveras tout-ça dans le dernier SDK.

    GetCurrentProcess + SetPriorityClass + REALTIME_PRIORITY_CLASS

    GetCurrentThread + SetThreadPriority THREAD_PRIORITY_TIME_CRITICAL

    Vérifier que SetThreadAffinityMask via 0FF toutes tes unités de traitements (le µP c'est très bien gérer ses flux comme un grand !).

    - Pour la mesure du temps oublies les moyens antédiluviens:
    QueryPerformanceCounter est le seul moyen fiable de mesurer en nombre de cycles (pragmatiques dirons-nous, dans des systèmes à haut niveau de partage et de // ) écouler ou de tranches de 100 ns (qui est la résolution maximum des "nouveaux" timers hardware Intel. Oublies les CPUID qui ne peuvent plus suivre à l'heure actuelle.

    - Tu peux aussi faire de la // gigogne avec des events et des threads qui tournent sur déclenchement: Tu pourras ainsi travailler par blocs de données avec des files d'attentes et optimiser les temps inoccupés… etc. etc.

    La stratégie est toujours plus efficace que l'optimisation millimétrée dans un premier temps et Penser en assembleur pour faire du C++ sera toujours plus efficace que de penser C++ en assembleur: Les stratégies et les économies ne sont pas du tout les mêmes.

    (cf, code plus court = code plus rapide… c'est loin d'être toujours vérifié dans l'approche stratégique)

Discussions similaires

  1. Python plus rapide que l'Assembleur ?
    Par le pythonien dans le forum x86 16-bits
    Réponses: 8
    Dernier message: 17/08/2009, 22h57
  2. [VB6] timer plus rapide que 1 d'interval
    Par windob dans le forum VB 6 et antérieur
    Réponses: 12
    Dernier message: 24/02/2004, 01h16
  3. Réponses: 8
    Dernier message: 31/10/2003, 17h21

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo