OVH Cloud OVH Cloud

C++ embarqué

29 réponses
Avatar
Stan
http://www.research.att.com/~bs/esc99.html


Même si le domaine visé est l'embarqué,
le côté didactique de BS est toujours
agréable à lire...


--
-Stan

10 réponses

1 2 3
Avatar
Jean-Marc Bourguet
writes:

Parce que je constate qu'elles varient d'une exécution à
l'autre. Et que du fait que mon compteur de boucle est surn un
entier 32 bits, je ne peux pas augmenter la durée de façon
illimitée. (Normallement, je considère quelque chose comme cinq
minutes pour chaque mésure comme un minimum. Mais ici, j'en suis
loin.)


Le probleme avec une telle duree, c'est que c'est difficile d'avoir
une machine qui ne fait que ca. Le SE va prendre la main a un moment
ou a un autre, ce ui en plus va vider les caches, etc. On mesure donc
d'office plus que ce qu'on veut mesure. J'ai pas la tete a faire une
estimation.

Je n'ai jamais vu un facteur 5. J'ai vu -10%/+50% au maximum.
Et c'est coherent avec un appel indirect de fonction via un
pointeur.


On est déjà à plus de quatre ci-dessus, sur un Sparc plutôt
ancien. Des personnes de chez HP (à l'époque) m'ont assuré que
sur l'architecture PA Risc, c'était pire.


Mais sur des machines recentes les couts sont diminues.

Petit rappel de l'histoire des architectures.

Si on considere l'architecture visible par le programmeur: les
registres et le jeu d'instructions.

La premiere etape a ete une interpretation de ce jeu d'instruction par
des ordinateurs ayant une architecture interne relativement differente
(et plus ou moins specialisee). Cela peut se faire soit par
interpreteur ecrit dans le langage machine de l'architecture
specialisee, soit par une traduction plus simple (telle instruction se
traduit par telle liste de micro-instructions). Dans les deux cas, on
parle de micro-programmation et c'est parfois modifiable par
l'utilisateur. A noter qu'il peut y avoir une reelle disparite entre
l'architecture visible et l'interne. L'IBM 360 par exemple a ete
lance avec une architecture visible et 5 ou 6 architectures internes
avec des performances et couts differents.

Si on simplifie le jeu d'instructions (et on complexifie les
micro-instructions), on peut arriver a une correspondance 1-1 entre
instruction et micro-instruction.

Si on regarde l'execution d'une instruction, il y a plusieurs etapes:
on la lis, on la decode, on recupere les operandes, on l'execute et on
ecrit le resultat. Ces etapes etant relativement independantes, il
est possible de commencer (et bien) une instruction sans que la
precedante soit terminee. Le seul probleme alors c'est les
dependances de donnees (si on a besoin du resultat de l'instruction
precedante, c'est difficile de commencer a executer la suivante).
C'est le pipeline.

On a remarque que les instructions sont souvent independantes. Si on
arrive a en lire plusieurs en meme temps, on peut en executer
plusieurs si on dispose de plusieurs unites d'execution. Ca se fait,
c'est ce qu'on appelle du multi-scalaire. Au debut les unites etaient
simplement l'ALU entier et l'ALU flottant mais maintenant on peut
avoir plusieurs unites de meme type.

Quand on peut executer plusieurs instructions en meme temps, la
difficulte commence a etre de les trouver... La premiere chose, c'est
d'aller voir plus loin si apres une instruction ayant des dependances
ne se trouve pas une instruction independante. Par exemple dans
LD R1,[R2]
MUL R1,R1,R3
ADD R4,R1
ADD R2,#1
on peut faire la deuxieme addition avant d'avoir fait la premiere.
C'est l'execution dans le desordre.

L'etape suivante, c'est de renommer des registres. Si on a
LD R1,[R2]
MUL R1,R1,R3
ADD R4,R1
ADD R2,#1
LD R1,[R2]
MUL R1,R1,R3
ADD R4,R1
ADD R2,#1
Apparemment on est bloque apres la deuxieme addition. Mais si on
avait
LD R1,[R2]
MUL R1,R1,R3
ADD R4,R1
ADD R2,#1
LD R5,[R2]
MUL R5,R5,R3
ADD R4,R5
ADD R2,#1
on pourrait rapidement commencer le second load. Ce renommage est
maintenant fait par les processeurs. (Et c'est pas recent. Un des
IBM 360 avait une telle possibilite).

Ok, on arrive a extraire le parallelisme dans un bloc, mais on reste
bloque aux sauts. La premiere idee, c'est de predire s'ils seront
pris ou pas pour commencer la charger la destination. Et de predire
la destination pour des sauts indirects. Si la prediction est bonne,
on a gagne du temps (d'initialisation du cache).

On peut meme commencer a executer tant que les resultats ne sont pas
ecrits en memoire. C'est de l'execution speculative. Il y a deux ou
trois ans (j'ai pas regarde les choses depuis) on envisageait de faire
de l'execution speculative a travers plusieurs branchements
conditionnels ou meme en suivant les deux branches.

Le problème, c'est que c'est un branchement indirect. Ce qui pose
des problèmes pour la prédiction des branchements. Dans le cas du
HP, d'après ce que j'ai compris, il en résulte un flush du pipeline
à chaque coup.


Normalement on ne vide plus les pipelines que si le branchement n'a
pas ete correctement predit. Et les taux de prediction sont de
l'ordre de 90%.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Jean-Marc Bourguet
Laurent Deniau writes:

Jean-Marc Bourguet wrote:
Mon idee est que maintenant essayer de separer les choses de leur
contexte n'a plus guere de sens parce que le cout depend fortement du
contexte; ca ne m'etonnerait pas du tout qu'on se retrouve meme
parfois avec des couts negatifs.
Dans le cas present, la premiere chose a noter, c'est que quand on
compile pour des objets partages (code PIC), on se retrouve souvent
(au moins sous Unix, je suppose pour Windows aussi) a ce que des
appels "directs" soient aussi en fait des appels indirects (je suppose
que B.S et toi savez ca et en avez tenu compte).


Oui, mais dans ce cas, on a un appel indirect. On peut donc tester
la rapidite d'un appel indirect hors PIC et en deduire le coup d'un
appel direct avec PIC.


Je voulais dire que vous aviez utiliser les bonnes options pour ne pas
avoir du code PIC sans vous en douter.

Deuxieme chose, ensuite, quels sont les facteurs qui comptent dans le
temps d'execution d'un appel?
1/ est-ce que la page est en memoire
independant de direct-indirect


2/ est-ce que l'entree de la page est dans le TLB
independant de direct-indirect


3/ est-ce que la fonction est dans le cache
independant de direct-indirect


4/ est-ce que l'appel est predictible (est-ce qu'il est pris ou non,
est-ce que sa destination est predictible ou non).
la d'accord, et c'est justement en partie ce que l'on mesure.



Non. Le cas est trop simple, dans une boucle comme cela a partir du
moment ou il y a un predicteur d'adresse il predira correctement (ces
predicteurs sont en gros des caches).

A mon avis ces quatres facteurs vont en pratique dominer la
difference entre un appel direct et un appel indirect et ces
quatres facteurs sont dependants du contexte.


Je pense que les trois premiers ne sont pas representatif du coup
d'un appel direct - indirect.


C'est independant mais ca va changer le rapport direct/indirect et le
ramener vers un. Ce que tu mesures est une borne superieure de ce
rapport (note que ca va dans le sens de ce que tu veux montrer, je ne
fais que te donner un argument supplementaire).

Mon point de vue est de dire que les appels virtuels sont en general
aussi rapides que les appels direct. L'objectif est d'eviter que des
programmeurs partent dans des delires algorithmiques pour ne pas
utiliser les fonctions virtuelles pour cause d'efficacite.


But louable et je suis d'accord avec le reste.

A+
--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
kanze
Laurent Deniau wrote:
wrote:


[...]
Les temps absolus sont peut parlant (depends des
architectures et des compilateurs). Je prefere de loin un
ratio virtuel/non-virtuel.


Mais ça aussi dépend de l'architectures et des compilateurs.
Et


Oui mais ce que l'on cherche, ce n'est pas de savoir combien
de temps met une operation, mais son efficacite par rapport a
une autre operation. C'est cela qui conditionne le
comportement des programmeurs.


Oui, mais ça dépend étroitement des architectures et des
complateurs. Si je donne des mésures pour une machine donnée,
c'est assez facile à obtenir des rapports. Mais ils ne valent
que pour cette machine, dans cet environement.

Et si ça conditionne le comportement des programmeurs, il est
temps de changer les programmeurs. Considérer de telles
questions lorqu'on écrit le code, c'est vraiment de
l'optimisation prématurée.

beaucoup. Disons que sur une machine généraliste moderne, je
m'attendrais à un rapport entre 1:2 et 1:10. Sur des
machines plus primitives (embarquées ou anciennes), j'ai
déjà vu nettement moins de 1:2.


Jamais vu de tel rapport.


Tu ne t'es jamais servi d'un Sparc moderne ? Où d'une machine HP
Risc ?

Heureusement parce que OOC n'a que des methodes virtuelles. Je
n'aurais jamais fait ca si je n'etais pas convaincu que
virtuel = direct +/- 10% (note que j'ai mesure des appels
virtuel plus rapide que des appels directs).


Je n'ai jamais vu une architecture où le rapport est seulement
de 10%.

Est-ce que tu es sûr que tu ne mésures que la différence de
l'appel même, et pas la différence totale de performance ? Parce
que dans les applications que je connais (même sans utiliser
inline du tout), le temps passés dans les appels n'en fait
qu'une très faible partie du temps d'exécution.

En fait, c'est quasiment impossible d'isoler l'appel
complétement -- il y a toujours au moins un retour un plus. Si
on admet que l'appel non-virtuel se fait dans un clock, mettons
2 nanosécondes, et que l'appel virtuel, à cause de la purge du
pipeline, exige en fait 15 nanosécondes, on a un rapport à 7,5.
Si on considère que par quelle miracle, le processeur arrive à
prédire le branchement correct dans le rétour, c'est 2
nanasécondes de plus : l'appel plus le rétour font alors 4
(non-virtuel) vs. 17 (virtuel) -- un rapport d'un peu plus que
4. Si le rétour purge aussi le pipeline (le cas sur les
PA-Risc), on se rétrouve avec 17 vs. 30 -- une facteur de moins
de 2 (et l'inline commence à être vraiment intéressant). Plus tu
ajoutes dans ce que tu mésures réelement, plus tu atténues la
différence.

Ce qui n'est pas forcément une mauvaise chose. En fin de compte,
ce qui compte, ce n'est pas le coût d'un appel, c'est le temps
d'exécution totale du programme. Et le fait qu'un appel
virtuelle coûte 10 fois plus qu'un appel non-virtuelle n'est pas
forcément significatif. D'abord, ils font des choses
différentes, et aussi, quel pourcentage du temps de l'exécution
se passe réelement dans les appels -- si ce n'est qu'un ou deux
pour cent, une facteur dix dans le temps d'exécution n'a pas
d'impact sur le temps d'exécution du programme. C'est le genre
de benchmark que j'ai fait par curiosité, pour m'amuser, mais
qui n'a pas le moindre impact sur la façon que je code.

[...]
Je dirais que la différence entre virtuel et non-virtuel
(facteur 5), c'est à peu près ce à laquelle je m'attendrais
sur


Je n'ai jamais vu un facteur 5. J'ai vu -10%/+50% au
maximum. Et c'est coherent avec un appel indirect de
fonction via un pointeur.


On est déjà à plus de quatre ci-dessus, sur un Sparc plutôt
ancien. Des personnes de chez HP (à l'époque) m'ont assuré
que sur l'architecture PA Risc, c'était pire.


Je continue de penser que tu mesures inline vs virtual (ou
direct).


Régarde bien les résultats avec le compilateur Sun. J'ai mésuré
inline, non-virtuel, et virtuel. Et il y a bien une différence
importante entre l'inline et le non-virtuel aussi.

En fait, je viens de régarder l'exécutable généré par g++ avec
adb ; il n'a pas générer les appels inline, et en fait, il
génère exactement la même chose que Sun CC. Mais les résultats
sont bien différents. En fin de compte, je crois que Jean-Marc a
raison quand il dit que « essayer de mesurer quelque chose de ce
genre n'a plus de sens. » Mais je suis têtu, et il s'avère qu'il
y a un certain nombre de machines qui ne font pas grand chose
aujourd'hui...

Le problème, c'est que c'est un branchement indirect. Ce qui
pose des problèmes pour la prédiction des branchements. Dans
le cas du HP, d'après ce que j'ai compris, il en résulte un
flush du pipeline à chaque coup. > > Personnellement je ne
crois pas. Cela ecroulerait les performances de > facon
mesurable et en ferait une architecture tres inefficace
(mais je > n'ai pas HP sous la main...). La plupart des
langages+projets > aujourd'hui utilisent des appels
indirects (interfaces). > Note que je ne mésure que le coup
de l'appel d'une fonction triviale (qui renvoie un int
constante) : « i = f() ; ». Il y a donc non seulement
l'appel de la fonction proprement dit, mais aussi chargement
du pointeur this, chargement et affectation de la valeur de
retour, et le retour lui-même. La différence dans le temps
de l'appel lui-même est donc encore plus grande.


Quand je fais ce type de tests, je fais avec:
- 0 arg + 0 ret
- 1 arg + 0 ret
- 0 arg + 1 ret
- 1 arg + 1 ret
- 2 args

Ca me donne une idee des performances plus realiste.


Quand j'ai fait mes mésures, c'était par pûr curiosité. Pour
faire des mesures « réalistes », il faudrait que je fasse une
statistique de ce qui se trouve dans nos fonctions, pondérée par
la fréquence que chaque fonction est appelée.

De même, j'ai utilisé une charpente de benchmark qui n'a pas été
conçu pour mesurer des choses aussi petites. Il donne de très
bons résultats quand la durée de ce qu'on mesure dépasse la
durée d'un appel virtuel par une facteur de deux ou de trois,
mais quand la durée devient plus petite, le code qui enlève le
coût du au charpente introduit trop de jitter -- pour des choses
vraiment rapides, il m'arrive à avoir des résultats négatifs.
(Mais comment faire avec précision pour de telles durées ? Parce
que c'est précisement avec de telles durées que le coût de la
gestion de la boucle, etc., pèse le plus.)

De l'autre côté, il faut le comparer à ce qu'on fait dans la
fonction. Dès qu'il y a une multiplication ou une division
entière, avec des termes non constantes, le temps de l'appel
devient négligeable. Sur ma machine... on ne peut pas faire
de généralités.


On ne parle que du cout de l'appel, le reste est speculatif.


Mais comment mesures-tu le coût de l'appel, sans mesurer aussi
le coût du retour, le coût de la gestion de la boucle, etc. ?
Sans parler du coût des indirections supplémentaires nécessaire
pour tromper l'optimisateur.

C'est l'argument des utilisateurs d'Objective-C qui clame que
le message dispatch (bien que tres lent, 800% pour gcc) est
suffisament rapide en raison du code de la fonction. Et quand
tu demandes comment faire une classe Complex efficace (je te
rappelle que les data membres ne sont accessible que par des
methodes), ils te repondent que Objective-C n'est pas fait
pour ca...


Et si tu leur démandes pourquoi Objective-C n'est pas fait pour
ça... ils répondent que c'est parce que les appels de fonction
sont trop lents ? :-)

Au passage j'ai une implementation de message dispatch en C
(type Objective-C) qui a un overhead de 40% a 80% par
rapport a un appel direct.


Sur quelle machine ? Mésuré comment ?


Linux IA32, gcc 3.3.5
clock_t, duree > 10 sec, elimination des couts "parasites", ratio.


La dernière fois que j'ai fait des mesures sur un Intel, c'était
un 80386, qui tournait en mode 16 bits. Alors, mes informations
ne sont pas des plus récentes. Mais c'est effectivement le
processeur où j'ai vu le moins de différence.

C'est une implemantation qui ne supporte pas le message
forwarding avec autre chose que [jusqu'a] deux objects (idem
Stepstone objc). Cette remarque est importante car elle
"cristalise" le choix de l'implementation du dispatcher:

- soit le message dispatch recherche le pointeur de fonction a
partir du selecteur et le renvoi a l'appelant qui l'invoque
(POC, mon dispatcher version rapide). Cette techique a
l'avantage de permettre d'inliner le code de recherche dans le
cache qui avec mes tests arrive a 99,7% de hits sur 1000000 de
messages generes.

- soit tout est transfere (argument inclus), au dispatcher qui
forward le tout a la methode une fois trouvee (GCC, mon
dispatcher version lente). Cette solution permet de forwarder
n'importe quel message, au prix systematique d'un double appel
de fonction, donc performance > 200% (240-300% dans mon cas).

Ensuite tu as le choix de l'implementation du cache: GCC
utilise un sparse array a 2 (ou 3 niveaux configurable a la
compilation), le mien utilise une hash table.

Et c'est forcement plus lent qu'un method lookup. Alors j'ai
du mal a imaginer un facteur 5 pour les methodes virtuelles.


C'est cependant le cas, au moins sur les machines modernes.


Est-ce que les appels directs sont dans une TU sparee avec une
compilation separee?


Évidemment. Aussi, dans la boucle de base, il y a un appel
virtuel, pour tromper l'optimisateur. Avant de faire les
mesures, je mesure la boucle avec une fonction virtuelle vide,
et je soustrais cette mesure de chacune des mesures qui suivent.

Parce que un facteur 5, c'est ce que je mesure entre inline et
direct (ou virtual).


Sur un Intel. C-à-d une architecture ancienne.

Tu ne comparais pas avec de l'inline par harsard?



D'ou ma question ;-)

Est-ce que tu as au moins lu ce que j'ai posté ? J'ai fait


Oui mais je n'ai ni le code, ni la methode de compilation.


Tout à fait. Mais dans les mésures, il y avait de inline, et de
non inline. Et avec Sun CC, ils était bien différents.

plusieurs mésures, dans des conditions différentes. J'ai
bien l'impression que g++ a inliné les fonctions, parce
qu'il n'y a


C'est ce que je pense.

pas de différences entre les temps pour les fonctions
inlinées, et les temps pour les fonctions non-virtuelles. Et
que ces temps


Bingo...

pour les fonctions inlinées ressemblent à peu près à celles
de Sun CC. Dans le cas de Sun CC, en revanche, il y a bien
une différence.



Tout à fait. Du coup, pour g++, il y a une facteur de 45 entre
les virtuels et les non-virtuels (ce que je trouve aussi
suspect). Avec Sun CC, en revanche, cette différence s'approche
à 5, ce qui est dans l'ordre de grandeur auquel je m'attends.

Pendant que j'écrivais ceci, j'ai repété les tests sur plusieurs
machines. Ce que je constate, c'est que les rapports entre les
résultats tiennent, sur une machine donnée. Et que curieusement,
avec g++, l'appel de la fonction non-virtuelle non-inlinée est
systèmatiquement plus rapide que l'appel inliné. Ce qui n'est
pas le cas avec Sun CC (où non-inline prend bien 7 ou 8 fois
plus de temps que inline). J'ai aussi vérifié le code de
l'exécutable avec adb -- g++ et Sun CC génère exactement les
mêmes instructions. Alors, il y a quelque chose dans la
contexte... Je ne l'explique pas, mais j'ai déjà rémarqué que
les temps sur un Sparc peuvent varier énormement selon les
alignements, etc.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34




Avatar
kanze
Jean-Marc Bourguet wrote:
writes:

Parce que je constate qu'elles varient d'une exécution à
l'autre. Et que du fait que mon compteur de boucle est surn
un entier 32 bits, je ne peux pas augmenter la durée de
façon illimitée. (Normallement, je considère quelque chose
comme cinq minutes pour chaque mésure comme un minimum. Mais
ici, j'en suis loin.)


Le probleme avec une telle duree, c'est que c'est difficile
d'avoir une machine qui ne fait que ca. Le SE va prendre la
main a un moment ou a un autre, ce ui en plus va vider les
caches, etc. On mesure donc d'office plus que ce qu'on veut
mesure. J'ai pas la tete a faire une estimation.


Tout à fait. Mais si on fait tourner tous les tests assez
longtemps, on peut espérer que sur le moyen, ces effets
affectent chacun des mesures de la même façon.

[...]
La premiere etape a ete une interpretation de ce jeu
d'instruction par des ordinateurs ayant une architecture
interne relativement differente (et plus ou moins
specialisee). Cela peut se faire soit par interpreteur ecrit
dans le langage machine de l'architecture specialisee, soit
par une traduction plus simple (telle instruction se traduit
par telle liste de micro-instructions). Dans les deux cas, on
parle de micro-programmation et c'est parfois modifiable par
l'utilisateur. A noter qu'il peut y avoir une reelle disparite
entre l'architecture visible et l'interne. L'IBM 360 par
exemple a ete lance avec une architecture visible et 5 ou 6
architectures internes avec des performances et couts
differents.


Juste un détail, mais le microcode est venu assez tardivement,
dans les années 1960. En fait, l'IBM 360 était la première
architecture de s'en servir. À cet égard, les architectures RISC
sont un rétour en arrière.

[...]
Le problème, c'est que c'est un branchement indirect. Ce qui
pose des problèmes pour la prédiction des branchements. Dans
le cas du HP, d'après ce que j'ai compris, il en résulte un
flush du pipeline à chaque coup.


Normalement on ne vide plus les pipelines que si le
branchement n'a pas ete correctement predit. Et les taux de
prediction sont de l'ordre de 90%.


C'est bien d'un employé de HP (à l'époque) que j'ai mes
informations. Que les PA Risc (de l'époque) n'essayait même pas
à faire de la prédiction sur un saut indirect.

Ce qui se conçoit. La prédiction n'est utile que si tu connais
les adresses du code dans la branche prédite. A priori, les PA
Risc de l'époque ne savait pas rétrouver les adresses du code
dans le cas d'un saut indirect -- y compris les retours d'une
fonction. (Note bien que dans les cas qui nous intéresse, la «
prédiction » est correcte 100% des fois, puisqu'il s'agit des
sauts inconditionnels.) D'après mes derniers mesures, sur un
Sparc, le hardware arrive à trouver l'adresse dans le cas d'un
retour d'une fonction, mais non dans le cas d'un appel virtuel.
Probablement parce que dans le premier cas (sur un Sparc),
l'adresse cible est dans un registre, et que dans le deuxième,
il faut la chercher en mémoire.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Gabriel Dos Reis
writes:

[...]

| > > beaucoup. Disons que sur une machine généraliste moderne, je
| > > m'attendrais à un rapport entre 1:2 et 1:10. Sur des
| > > machines plus primitives (embarquées ou anciennes), j'ai
| > > déjà vu nettement moins de 1:2.
|
| > Jamais vu de tel rapport.
|
| Tu ne t'es jamais servi d'un Sparc moderne ?

Quel est le rapport ?

[...]

| Est-ce que tu es sûr que tu ne mésures que la différence de
| l'appel même, et pas la différence totale de performance ? Parce
| que dans les applications que je connais (même sans utiliser
| inline du tout), le temps passés dans les appels n'en fait
| qu'une très faible partie du temps d'exécution.

Peut-être qu'il parle d'applications que tu ne connais pas.

[...]

| En fait, je viens de régarder l'exécutable généré par g++ avec
| adb ; il n'a pas générer les appels inline, et en fait, il
| génère exactement la même chose que Sun CC. Mais les résultats
| sont bien différents. En fin de compte, je crois que Jean-Marc a
| raison quand il dit que « essayer de mesurer quelque chose de ce
| genre n'a plus de sens. » Mais je suis têtu, et il s'avère qu'il
| y a un certain nombre de machines qui ne font pas grand chose
| aujourd'hui...

de fait, « mésurer » en regardant juste le code généré n'a plus grand
sens.

[...]

| > > C'est cependant le cas, au moins sur les machines modernes.
|
| > Est-ce que les appels directs sont dans une TU sparee avec une
| > compilation separee?
|
| Évidemment. Aussi, dans la boucle de base, il y a un appel
| virtuel, pour tromper l'optimisateur.

Lequel ?

[...]

| Pendant que j'écrivais ceci, j'ai repété les tests sur plusieurs
| machines. Ce que je constate, c'est que les rapports entre les
| résultats tiennent, sur une machine donnée. Et que curieusement,
| avec g++, l'appel de la fonction non-virtuelle non-inlinée est
| systèmatiquement plus rapide que l'appel inliné.

Quelle version de GCC ?

| Ce qui n'est
| pas le cas avec Sun CC (où non-inline prend bien 7 ou 8 fois
| plus de temps que inline). J'ai aussi vérifié le code de
| l'exécutable avec adb -- g++ et Sun CC génère exactement les
| mêmes instructions. Alors, il y a quelque chose dans la
| contexte...

il faut aussi voir si ta version de GCC support les "hot section"
ou non.

| Je ne l'explique pas, mais j'ai déjà rémarqué que
| les temps sur un Sparc peuvent varier énormement selon les
| alignements, etc.

Bah oui. depuis un certain temps, GCC utilise les "hot section" et
"cold section", qui peuvent selon les applications avoir des effets
intéressants sur la performance. Je ne sais pas si tu as une de ces
versions.

-- Gaby
Avatar
Gabriel Dos Reis
writes:

[...]

| sauts inconditionnels.) D'après mes derniers mesures, sur un
| Sparc, le hardware arrive à trouver l'adresse dans le cas d'un
| retour d'une fonction, mais non dans le cas d'un appel virtuel.
| Probablement parce que dans le premier cas (sur un Sparc),
| l'adresse cible est dans un registre, et que dans le deuxième,
| il faut la chercher en mémoire.

Il y a beaucoup de facteurs qui entrent en jeu, y compris l'ABI et ce
que tu « mets » dans tes tests.

-- Gaby
Avatar
Laurent Deniau
wrote:
Laurent Deniau wrote:

wrote:



[...]

Les temps absolus sont peut parlant (depends des
architectures et des compilateurs). Je prefere de loin un
ratio virtuel/non-virtuel.





Mais ça aussi dépend de l'architectures et des compilateurs.
Et




Oui mais ce que l'on cherche, ce n'est pas de savoir combien
de temps met une operation, mais son efficacite par rapport a
une autre operation. C'est cela qui conditionne le
comportement des programmeurs.



Oui, mais ça dépend étroitement des architectures et des
complateurs. Si je donne des mésures pour une machine donnée,
c'est assez facile à obtenir des rapports. Mais ils ne valent
que pour cette machine, dans cet environement.


Tout a fait. Ce que je dis, c'est que meme si ca change d'une
architecture a l'autre, cela reste plus facile a comparer.

Et si ça conditionne le comportement des programmeurs, il est
temps de changer les programmeurs. Considérer de telles
questions lorqu'on écrit le code, c'est vraiment de
l'optimisation prématurée.


Et pourtant on trouve plus que des templates et les fonctions inline
partout avec des patterns de plus en plus compliquees.

beaucoup. Disons que sur une machine généraliste moderne, je
m'attendrais à un rapport entre 1:2 et 1:10. Sur des
machines plus primitives (embarquées ou anciennes), j'ai
déjà vu nettement moins de 1:2.




Jamais vu de tel rapport.



Tu ne t'es jamais servi d'un Sparc moderne ? Où d'une machine HP
Risc ?


uname -a
SunOS sunmta6 5.6 Generic_105181-12 sun4u sparc SUNW,Ultra-1

c'est pas tres moderne, je te l'accorde.

Il y a qque semaines, j'ai du me contenter d'un gcc 2.2 (!) et 2.7.2 sur
des Sun. Mon code ne passait pas sur 2.2 (pre-c89?) mais heureusement
les deux architectures etaient identiques, j'ai donc pu utiliser 2.7.2
pour les deux machines...

Heureusement parce que OOC n'a que des methodes virtuelles. Je
n'aurais jamais fait ca si je n'etais pas convaincu que
virtuel = direct +/- 10% (note que j'ai mesure des appels
virtuel plus rapide que des appels directs).



Je n'ai jamais vu une architecture où le rapport est seulement
de 10%.

Est-ce que tu es sûr que tu ne mésures que la différence de
l'appel même, et pas la différence totale de performance ? Parce
que dans les applications que je connais (même sans utiliser
inline du tout), le temps passés dans les appels n'en fait
qu'une très faible partie du temps d'exécution.


Je ne comprends pas ce que tu dis. De quel temps d'execution tu parles,
si ce n'est pas celui de l'appel? Comme un peu de code peut-etre plus
explicite qu'un long discours, je mets un test en fin de post.

En fait, c'est quasiment impossible d'isoler l'appel
complétement -- il y a toujours au moins un retour un plus. Si
on admet que l'appel non-virtuel se fait dans un clock, mettons
2 nanosécondes, et que l'appel virtuel, à cause de la purge du
pipeline, exige en fait 15 nanosécondes, on a un rapport à 7,5.
Si on considère que par quelle miracle, le processeur arrive à
prédire le branchement correct dans le rétour, c'est 2
nanasécondes de plus : l'appel plus le rétour font alors 4
(non-virtuel) vs. 17 (virtuel) -- un rapport d'un peu plus que
4. Si le rétour purge aussi le pipeline (le cas sur les
PA-Risc), on se rétrouve avec 17 vs. 30 -- une facteur de moins
de 2 (et l'inline commence à être vraiment intéressant). Plus tu
ajoutes dans ce que tu mésures réelement, plus tu atténues la
différence.

Ce qui n'est pas forcément une mauvaise chose. En fin de compte,
ce qui compte, ce n'est pas le coût d'un appel, c'est le temps
d'exécution totale du programme. Et le fait qu'un appel
virtuelle coûte 10 fois plus qu'un appel non-virtuelle n'est pas
forcément significatif. D'abord, ils font des choses
différentes, et aussi, quel pourcentage du temps de l'exécution
se passe réelement dans les appels -- si ce n'est qu'un ou deux
pour cent, une facteur dix dans le temps d'exécution n'a pas
d'impact sur le temps d'exécution du programme. C'est le genre
de benchmark que j'ai fait par curiosité, pour m'amuser, mais
qui n'a pas le moindre impact sur la façon que je code.


Par ce que tu as de l'experience...

Mais il ne faut pas oublier les decisions qui ont ete prises pour C++.
Le but etait d'avoir le meme degre d'efficacite que le C (probablement
pour facilite la transition psychologique des programmeur C vers le C++).

Je vois encore pas mal d'articles et de propositions (tout plus complexe
les unes que les autres) sur les multi-methodes en C++ alors qu'un
double (ou N) appel virtuel resout le probleme. Je ne connais pas la
raison, mais cela semble etroitement lie au coup des appels virtuels.

Je dirais que la différence entre virtuel et non-virtuel
(facteur 5), c'est à peu près ce à laquelle je m'attendrais
sur






Je n'ai jamais vu un facteur 5. J'ai vu -10%/+50% au
maximum. Et c'est coherent avec un appel indirect de
fonction via un pointeur.





On est déjà à plus de quatre ci-dessus, sur un Sparc plutôt
ancien. Des personnes de chez HP (à l'époque) m'ont assuré
que sur l'architecture PA Risc, c'était pire.




Je continue de penser que tu mesures inline vs virtual (ou
direct).



Régarde bien les résultats avec le compilateur Sun. J'ai mésuré
inline, non-virtuel, et virtuel. Et il y a bien une différence
importante entre l'inline et le non-virtuel aussi.

En fait, je viens de régarder l'exécutable généré par g++ avec
adb ; il n'a pas générer les appels inline, et en fait, il
génère exactement la même chose que Sun CC. Mais les résultats
sont bien différents. En fin de compte, je crois que Jean-Marc a
raison quand il dit que « essayer de mesurer quelque chose de ce
genre n'a plus de sens. » Mais je suis têtu, et il s'avère qu'il
y a un certain nombre de machines qui ne font pas grand chose
aujourd'hui...


Le problème, c'est que c'est un branchement indirect. Ce qui
pose des problèmes pour la prédiction des branchements. Dans
le cas du HP, d'après ce que j'ai compris, il en résulte un
flush du pipeline à chaque coup. > > Personnellement je ne
crois pas. Cela ecroulerait les performances de > facon
mesurable et en ferait une architecture tres inefficace
(mais je > n'ai pas HP sous la main...). La plupart des
langages+projets > aujourd'hui utilisent des appels
indirects (interfaces). > Note que je ne mésure que le coup
de l'appel d'une fonction triviale (qui renvoie un int
constante) : « i = f() ; ». Il y a donc non seulement
l'appel de la fonction proprement dit, mais aussi chargement
du pointeur this, chargement et affectation de la valeur de
retour, et le retour lui-même. La différence dans le temps
de l'appel lui-même est donc encore plus grande.




Quand je fais ce type de tests, je fais avec:
- 0 arg + 0 ret
- 1 arg + 0 ret
- 0 arg + 1 ret
- 1 arg + 1 ret
- 2 args



Ca me donne une idee des performances plus realiste.



Quand j'ai fait mes mésures, c'était par pûr curiosité. Pour
faire des mesures « réalistes », il faudrait que je fasse une
statistique de ce qui se trouve dans nos fonctions, pondérée par
la fréquence que chaque fonction est appelée.

De même, j'ai utilisé une charpente de benchmark qui n'a pas été
conçu pour mesurer des choses aussi petites. Il donne de très
bons résultats quand la durée de ce qu'on mesure dépasse la
durée d'un appel virtuel par une facteur de deux ou de trois,
mais quand la durée devient plus petite, le code qui enlève le
coût du au charpente introduit trop de jitter -- pour des choses
vraiment rapides, il m'arrive à avoir des résultats négatifs.
(Mais comment faire avec précision pour de telles durées ? Parce
que c'est précisement avec de telles durées que le coût de la
gestion de la boucle, etc., pèse le plus.)


Il suffit de les eliminer.

De l'autre côté, il faut le comparer à ce qu'on fait dans la
fonction. Dès qu'il y a une multiplication ou une division
entière, avec des termes non constantes, le temps de l'appel
devient négligeable. Sur ma machine... on ne peut pas faire
de généralités.




On ne parle que du cout de l'appel, le reste est speculatif.



Mais comment mesures-tu le coût de l'appel, sans mesurer aussi
le coût du retour, le coût de la gestion de la boucle, etc. ?


Le cout du retour est inclu biensur. Pour moi un appel c'est un
aller-retour. Le cout de la boucle peut se retirer, cf code ci-dessous.

Sans parler du coût des indirections supplémentaires nécessaire
pour tromper l'optimisateur.


Compilation separee.

C'est l'argument des utilisateurs d'Objective-C qui clame que
le message dispatch (bien que tres lent, 800% pour gcc) est
suffisament rapide en raison du code de la fonction. Et quand
tu demandes comment faire une classe Complex efficace (je te
rappelle que les data membres ne sont accessible que par des
methodes), ils te repondent que Objective-C n'est pas fait
pour ca...



Et si tu leur démandes pourquoi Objective-C n'est pas fait pour
ça... ils répondent que c'est parce que les appels de fonction
sont trop lents ? :-)


Les appels de method, oui. Facteur 8 (dans le meilleur des cas) quand
meme. Pour un getter sur un Array, ca peut etre critique.

Au passage j'ai une implementation de message dispatch en C
(type Objective-C) qui a un overhead de 40% a 80% par
rapport a un appel direct.





Sur quelle machine ? Mésuré comment ?




Linux IA32, gcc 3.3.5
clock_t, duree > 10 sec, elimination des couts "parasites", ratio.



La dernière fois que j'ai fait des mesures sur un Intel, c'était
un 80386, qui tournait en mode 16 bits. Alors, mes informations
ne sont pas des plus récentes. Mais c'est effectivement le
processeur où j'ai vu le moins de différence.


C'est une implemantation qui ne supporte pas le message
forwarding avec autre chose que [jusqu'a] deux objects (idem
Stepstone objc). Cette remarque est importante car elle
"cristalise" le choix de l'implementation du dispatcher:



- soit le message dispatch recherche le pointeur de fonction a
partir du selecteur et le renvoi a l'appelant qui l'invoque
(POC, mon dispatcher version rapide). Cette techique a
l'avantage de permettre d'inliner le code de recherche dans le
cache qui avec mes tests arrive a 99,7% de hits sur 1000000 de
messages generes.



- soit tout est transfere (argument inclus), au dispatcher qui
forward le tout a la methode une fois trouvee (GCC, mon
dispatcher version lente). Cette solution permet de forwarder
n'importe quel message, au prix systematique d'un double appel
de fonction, donc performance > 200% (240-300% dans mon cas).



Ensuite tu as le choix de l'implementation du cache: GCC
utilise un sparse array a 2 (ou 3 niveaux configurable a la
compilation), le mien utilise une hash table.



Et c'est forcement plus lent qu'un method lookup. Alors j'ai
du mal a imaginer un facteur 5 pour les methodes virtuelles.





C'est cependant le cas, au moins sur les machines modernes.




Est-ce que les appels directs sont dans une TU sparee avec une
compilation separee?



Évidemment. Aussi, dans la boucle de base, il y a un appel
virtuel, pour tromper l'optimisateur. Avant de faire les
mesures, je mesure la boucle avec une fonction virtuelle vide,
et je soustrais cette mesure de chacune des mesures qui suivent.


Parce que un facteur 5, c'est ce que je mesure entre inline et
direct (ou virtual).



Sur un Intel. C-à-d une architecture ancienne.


Tu ne comparais pas avec de l'inline par harsard?





D'ou ma question ;-)



Est-ce que tu as au moins lu ce que j'ai posté ? J'ai fait




Oui mais je n'ai ni le code, ni la methode de compilation.



Tout à fait. Mais dans les mésures, il y avait de inline, et de
non inline. Et avec Sun CC, ils était bien différents.


plusieurs mésures, dans des conditions différentes. J'ai
bien l'impression que g++ a inliné les fonctions, parce
qu'il n'y a




C'est ce que je pense.



pas de différences entre les temps pour les fonctions
inlinées, et les temps pour les fonctions non-virtuelles. Et
que ces temps




Bingo...



pour les fonctions inlinées ressemblent à peu près à celles
de Sun CC. Dans le cas de Sun CC, en revanche, il y a bien
une différence.




Tout à fait. Du coup, pour g++, il y a une facteur de 45 entre
les virtuels et les non-virtuels (ce que je trouve aussi
suspect). Avec Sun CC, en revanche, cette différence s'approche
à 5, ce qui est dans l'ordre de grandeur auquel je m'attends.

Pendant que j'écrivais ceci, j'ai repété les tests sur plusieurs
machines. Ce que je constate, c'est que les rapports entre les
résultats tiennent, sur une machine donnée. Et que curieusement,
avec g++, l'appel de la fonction non-virtuelle non-inlinée est
systèmatiquement plus rapide que l'appel inliné. Ce qui n'est


Je ne me preoccupe pas de l'inline. C'est trop "instable" sur le plan
des performances pour en tirer des conclusions.

pas le cas avec Sun CC (où non-inline prend bien 7 ou 8 fois
plus de temps que inline). J'ai aussi vérifié le code de
l'exécutable avec adb -- g++ et Sun CC génère exactement les
mêmes instructions. Alors, il y a quelque chose dans la
contexte... Je ne l'explique pas, mais j'ai déjà rémarqué que
les temps sur un Sparc peuvent varier énormement selon les
alignements, etc.


Ca oui!

Il semble que mes ratios on change et sont moins stable avec gcc 3.3.5
(ceux que j'ai cite date de gcc 2.95). Sur pentium-M 1.5Ghz, j'ai

gcc (direct, indirect) 0 = no arg

call0 : time = 3.03e-09 sec, iter = 330033003/sec)
call1 : time = 3.04e-09 sec, iter = 328947368/sec)
call2 : time = 4.70e-09 sec, iter = 212765957/sec)
callR : time = 3.72e-09 sec, iter = 268817204/sec)
callR1 : time = 3.38e-09 sec, iter = 295857988/sec)
icall0 : time = 4.05e-09 sec, iter = 246913580/sec)
icall1 : time = 6.22e-09 sec, iter = 160771704/sec)
icall2 : time = 4.73e-09 sec, iter = 211416490/sec)
icallR : time = 4.72e-09 sec, iter = 211864406/sec)
icallR1: time = 5.48e-09 sec, iter = 182481751/sec)

Les appels directs sont plus rapide.

g++ (direct, virtual) 0 = only this.

call0 : time = 3.36e-09 sec, iter = 297619047/sec)
call1 : time = 4.72e-09 sec, iter = 211864406/sec)
call2 : time = 4.42e-09 sec, iter = 226244343/sec)
callR : time = 3.36e-09 sec, iter = 297619047/sec)
callR1 : time = 4.63e-09 sec, iter = 215982721/sec)
icall0 : time = 3.04e-09 sec, iter = 328947368/sec)
icall1 : time = 4.39e-09 sec, iter = 227790432/sec)
icall2 : time = 4.39e-09 sec, iter = 227790432/sec)
icallR : time = 3.36e-09 sec, iter = 297619047/sec)
icallR1: time = 5.00e-09 sec, iter = 200000000/sec)

Les appels virtuels sont plus rapide (sauf pour R1)...

Si tu as le temps, tu peux executer le code ci-dessous sur les
architectures que tu as. La version C++ est facile deduire du code
ci-dessous.

//-------------------speed_f.h
#ifndef SPEED_H
#define SPEED_H

void do_nothing0(void);
void do_nothing1(unsigned a);
void do_nothing2(unsigned a, unsigned b);
unsigned do_nothingR(void);
unsigned do_nothingR1(unsigned a);

extern void (*p_do_nothing0) (void);
extern void (*p_do_nothing1) (unsigned);
extern void (*p_do_nothing2) (unsigned, unsigned);
extern unsigned (*p_do_nothingR) (void);
extern unsigned (*p_do_nothingR1)(unsigned);

#endif

//-------------------speed_f.c
#include "speed_f.h"

void do_nothing0(void) {}
void do_nothing1(unsigned a) { (void)a; }
void do_nothing2(unsigned a, unsigned b) { (void)a; (void)b; }
unsigned do_nothingR(void) { return 0; }
unsigned do_nothingR1(unsigned a) { return a; }

void (*p_do_nothing0) (void) = do_nothing0;
void (*p_do_nothing1) (unsigned) = do_nothing1;
void (*p_do_nothing2) (unsigned, unsigned) = do_nothing2;
unsigned (*p_do_nothingR) (void) = do_nothingR;
unsigned (*p_do_nothingR1)(unsigned) = do_nothingR1;

//-------------------call_speed.c
#include <time.h>
#include <stdio.h>
#include <stdlib.h>

#include "speed_f.h"

#define RUN_TEST(expr)
{
clock_t t[4];
unsigned i;

t[0] = clock();
for (i = 0; i < loop; i++) {
expr;
}
t[1] = clock();

t[2] = clock();
for (i = 0; i < loop; i++) {
expr;
expr;
expr;
}
t[3] = clock();

return test_unit_time(t, loop);
}

static double
test_unit_time(clock_t t[4], unsigned loop)
{
double dt0 = t[1]-t[0];
double dt1 = t[3]-t[2];

return (dt1-dt0)/CLOCKS_PER_SEC/2/loop;
}

static double
call0_speed(unsigned loop)
{
RUN_TEST(do_nothing0());
}

static double
call1_speed(unsigned loop)
{
RUN_TEST(do_nothing1(i));
}

static double
call2_speed(unsigned loop)
{
RUN_TEST(do_nothing2(i,i));
}

static double
callR_speed(unsigned loop)
{
RUN_TEST(do_nothingR());
}

static double
callR1_speed(unsigned loop)
{
RUN_TEST(do_nothingR1(i));
}

static double
icall0_speed(unsigned loop)
{
RUN_TEST(p_do_nothing0());
}

static double
icall1_speed(unsigned loop)
{
RUN_TEST(p_do_nothing1(i));
}

static double
icall2_speed(unsigned loop)
{
RUN_TEST(p_do_nothing2(i,i));
}

static double
icallR_speed(unsigned loop)
{
RUN_TEST(p_do_nothingR());
}

static double
icallR1_speed(unsigned loop)
{
RUN_TEST(p_do_nothingR1(i));
}

static void
print(const char *func, double val)
{
printf("%s: time = %.2e sec, iter = %9u/sec)n",
func, val, (unsigned)(1.0/val));
fflush(stdout);
}

int
main(int argc, char *argv[])
{
unsigned loop = argc > 1 ? strtoul(argv[1],0,0) : 500000000ul;

print("call0 ", call0_speed (loop));
print("call1 ", call1_speed (loop));
print("call2 ", call2_speed (loop));
print("callR ", callR_speed (loop));
print("callR1 ", callR1_speed (loop));

print("icall0 ", icall0_speed (loop));
print("icall1 ", icall1_speed (loop));
print("icall2 ", icall2_speed (loop));
print("icallR ", icallR_speed (loop));
print("icallR1", icallR1_speed(loop));

return 0;
}





Avatar
Jean-Marc Bourguet
Laurent Deniau writes:

gcc (direct, indirect) 0 = no arg

call0 : time = 3.03e-09 sec, iter = 330033003/sec)
call1 : time = 3.04e-09 sec, iter = 328947368/sec)
call2 : time = 4.70e-09 sec, iter = 212765957/sec)
callR : time = 3.72e-09 sec, iter = 268817204/sec)
callR1 : time = 3.38e-09 sec, iter = 295857988/sec)
icall0 : time = 4.05e-09 sec, iter = 246913580/sec)
icall1 : time = 6.22e-09 sec, iter = 160771704/sec)
icall2 : time = 4.73e-09 sec, iter = 211416490/sec)
icallR : time = 4.72e-09 sec, iter = 211864406/sec)
icallR1: time = 5.48e-09 sec, iter = 182481751/sec)

Les appels directs sont plus rapide.


Pour info:

Sun-Blade-1500
call0 : time = 5.24e-09 sec, iter = 190839694/sec)
call1 : time = 6.17e-09 sec, iter = 162074554/sec)
call2 : time = 6.20e-09 sec, iter = 161290322/sec)
callR : time = 5.28e-09 sec, iter = 189393939/sec)
callR1 : time = 6.20e-09 sec, iter = 161290322/sec)
icall0 : time = 1.38e-08 sec, iter = 72202166/sec)
icall1 : time = 1.38e-08 sec, iter = 72254335/sec)
icall2 : time = 1.34e-08 sec, iter = 74571215/sec)
icallR : time = 1.38e-08 sec, iter = 72411296/sec)
icallR1: time = 1.34e-08 sec, iter = 74794315/sec)

Sun-Blade-2500
call0 : time = 4.39e-09 sec, iter = 227790432/sec)
call1 : time = 5.12e-09 sec, iter = 195312499/sec)
call2 : time = 5.09e-09 sec, iter = 196463654/sec)
callR : time = 4.33e-09 sec, iter = 230946882/sec)
callR1 : time = 5.15e-09 sec, iter = 194174757/sec)
icall0 : time = 1.14e-08 sec, iter = 87950747/sec)
icall1 : time = 1.14e-08 sec, iter = 87719298/sec)
icall2 : time = 1.10e-08 sec, iter = 90991810/sec)
icallR : time = 1.14e-08 sec, iter = 87873462/sec)
icallR1: time = 1.10e-08 sec, iter = 90661831/sec)

(Dans les deux cas c'est des Sparc IIIi; donc si j'ai bonne memoire
multi-scalaire mais execution dans l'ordre; il me semble que les
Sparc IV ont de l'execution dans le desordre et speculative.)

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

Avatar
Jean-Marc Bourguet
Jean-Marc Bourguet writes:

Laurent Deniau writes:

gcc (direct, indirect) 0 = no arg

call0 : time = 3.03e-09 sec, iter = 330033003/sec)
call1 : time = 3.04e-09 sec, iter = 328947368/sec)
call2 : time = 4.70e-09 sec, iter = 212765957/sec)
callR : time = 3.72e-09 sec, iter = 268817204/sec)
callR1 : time = 3.38e-09 sec, iter = 295857988/sec)
icall0 : time = 4.05e-09 sec, iter = 246913580/sec)
icall1 : time = 6.22e-09 sec, iter = 160771704/sec)
icall2 : time = 4.73e-09 sec, iter = 211416490/sec)
icallR : time = 4.72e-09 sec, iter = 211864406/sec)
icallR1: time = 5.48e-09 sec, iter = 182481751/sec)

Les appels directs sont plus rapide.


Pour info:

Sun-Blade-1500
Avec sparcWorks, -xO3 si j'ai bonne mémoire mais rien de

spécifique au processeur.
call0 : time = 5.24e-09 sec, iter = 190839694/sec)
call1 : time = 6.17e-09 sec, iter = 162074554/sec)
call2 : time = 6.20e-09 sec, iter = 161290322/sec)
callR : time = 5.28e-09 sec, iter = 189393939/sec)
callR1 : time = 6.20e-09 sec, iter = 161290322/sec)
icall0 : time = 1.38e-08 sec, iter = 72202166/sec)
icall1 : time = 1.38e-08 sec, iter = 72254335/sec)
icall2 : time = 1.34e-08 sec, iter = 74571215/sec)
icallR : time = 1.38e-08 sec, iter = 72411296/sec)
icallR1: time = 1.34e-08 sec, iter = 74794315/sec)

Sun-Blade-2500
call0 : time = 4.39e-09 sec, iter = 227790432/sec)
call1 : time = 5.12e-09 sec, iter = 195312499/sec)
call2 : time = 5.09e-09 sec, iter = 196463654/sec)
callR : time = 4.33e-09 sec, iter = 230946882/sec)
callR1 : time = 5.15e-09 sec, iter = 194174757/sec)
icall0 : time = 1.14e-08 sec, iter = 87950747/sec)
icall1 : time = 1.14e-08 sec, iter = 87719298/sec)
icall2 : time = 1.10e-08 sec, iter = 90991810/sec)
icallR : time = 1.14e-08 sec, iter = 87873462/sec)
icallR1: time = 1.10e-08 sec, iter = 90661831/sec)

(Dans les deux cas c'est des Sparc IIIi; donc si j'ai bonne memoire
multi-scalaire mais execution dans l'ordre; il me semble que les
Sparc IV ont de l'execution dans le desordre et speculative.)


AMD Athlon(TM) XP1700+ gcc -O3

call0 : time = 9.79e-09 sec, iter = 102145045/sec)
call1 : time = 3.79e-09 sec, iter = 263852242/sec)
call2 : time = 3.37e-09 sec, iter = 296735905/sec)
callR : time = 9.74e-09 sec, iter = 102669404/sec)
callR1 : time = 3.79e-09 sec, iter = 263852242/sec)
icall0 : time = 1.22e-08 sec, iter = 82169268/sec)
icall1 : time = 1.25e-08 sec, iter = 79936051/sec)
icall2 : time = 4.34e-09 sec, iter = 230414746/sec)
icallR : time = 1.19e-08 sec, iter = 83963056/sec)
icallR1: time = 1.24e-08 sec, iter = 80906148/sec)

et en gcc -O3 -mcpu=athlon-xp

call0 : time = 3.78e-09 sec, iter = 264550264/sec)
call1 : time = 4.08e-09 sec, iter = 245098039/sec)
call2 : time = 3.43e-09 sec, iter = 291545189/sec)
callR : time = 3.78e-09 sec, iter = 264550264/sec)
callR1 : time = 4.12e-09 sec, iter = 242718446/sec)
icall0 : time = 4.80e-09 sec, iter = 208333333/sec)
icall1 : time = 5.40e-09 sec, iter = 185185185/sec)
icall2 : time = 4.34e-09 sec, iter = 230414746/sec)
icallR : time = 1.30e-08 sec, iter = 76804915/sec)
icallR1: time = 1.29e-08 sec, iter = 77339520/sec)

Je ne me risquerai pas à une interprétation quelconque au
dela de remarquer que les écarts se sont resserés.

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Jean-Marc Bourguet
writes:

Juste un détail, mais le microcode est venu assez
tardivement, dans les années 1960. En fait, l'IBM 360
était la première architecture de s'en servir. À cet
égard, les architectures RISC sont un rétour en arrière.


SI j'en crois Blaauw et Brooks (dans Computer Architecture,
Concepts and Evolution, p 71), l'idée (sous le nom de
micro-coding, mais il emploie aussi micro-programmation dans
le même chapitre sans introduire de nuance) provient de
Wilkes en 1951. Toujours d'après le même auteur, Van der
Poel a été influencé par cette pratique pour son ordinateur
(architecture datant de 1956 mais disponible en 1959) qui
lui n'est vraissemblablement pas micro-codé mais dont le
langage a une influence assez visible des micro-codes.

A lire les architectures visibles des IBM 705 et 7030
(stretch), j'ai l'impression que ce sont aussi des machines
micro-codées même si ce n'est pas dit explicitement (mais
bon le terme n'est pas employé non plus dans la description
de l'IBM 360 dont les modèles bas de gamme l'était).

[...]
Le problème, c'est que c'est un branchement indirect. Ce qui
pose des problèmes pour la prédiction des branchements. Dans
le cas du HP, d'après ce que j'ai compris, il en résulte un
flush du pipeline à chaque coup.


Normalement on ne vide plus les pipelines que si le
branchement n'a pas ete correctement predit. Et les taux de
prediction sont de l'ordre de 90%.


C'est bien d'un employé de HP (à l'époque) que j'ai mes
informations. Que les PA Risc (de l'époque) n'essayait même pas
à faire de la prédiction sur un saut indirect.


Si j'en crois Hennesy et Patterson (Computer Architecture, A
Quantitatice Approach, 3ieme edition de 2001),
l'architecture P6 (Pentium Pro, Pentium II, Pentium III)
utilise un buffer de 512 entrées pour ça (avec un taux de
réussite de 80%). Netburst (Pentium IV) a un buffer 8 fois
plus gros et un autre algo.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org



1 2 3