OVH Cloud OVH Cloud

Eviter le coût de la résolution de "virtual"

29 réponses
Avatar
Vincent Richard
Bonsoir,

Soit le programme suivant :

class A
{
public:
virtual void f(const int p) = 0;
};

class B : public A
{
public:
void f(const int p) { /* ... */ }
};

int main()
{
A* a = new B;

for (int i = 0 ; i < 10000 ; ++i)
a->f(i);
}

Je crois savoir qu'à chaque fois que l'on fait un appel à 'f()', une
"recherche" est effectuée pour appeler la fonction correcte selon
l'objet sur lequel on l'appelle. Cette recherche a un coût.

Y'a-t-il un moyen de la limiter, par exemple, en obtenant l'adresse
une première fois (résolution), puis en l'appelant ensuite (simple
appel, sans résolution), ce qui fait que la résolution n'est pas
faite à chaque itération ?

Par exemple :

int main()
{
A* a = new B;
PointeurSurLaFonction pf = ???;

for (int i = 0 ; i < 10000 ; ++i)
(pf)(i);
}

Apparemment, cela ne peut pas être résolu avec les pointeurs sur
fonctions membres :

int main()
{
A* a = new B;

typedef void (A::*FuncPtr)(int);
FuncPtr pf = &A::f;

for (int i = 0 ; i < 10000 ; ++i)
(a->*pf)(i);
}

...puisque dans ce cas, la résolution est évidemment quand même faite.

Je ne sais pas si je suis très explicite...

Merci d'avance pour vos réponses.

Vincent

--
SL> Au fait elle est mieux ma signature maintenant ?
Oui. T'enlève encore les conneries que t'as écrit dedans et c'est bon.
-+- JB in <http://www.le-gnu.net> : Le neuneuttoyage par le vide -+-

10 réponses

1 2 3
Avatar
Fabien LE LEZ
On Wed, 03 Sep 2003 21:42:50 +0200, Vincent Richard
wrote:

Cette recherche a un coût.


Cette recherche a-t-elle un coût suffisant pour ralentir ton
application de façon significative ?

Avatar
Vincent Richard

On Wed, 03 Sep 2003 21:42:50 +0200, Vincent Richard
wrote:

Cette recherche a un coût.


Cette recherche a-t-elle un coût suffisant pour ralentir ton
application de façon significative ?


Cette fonction est appelée dans le cadre du tri d'une liste
de plusieurs dizaines de milliers d'éléments, donc je pense que
oui, c'est significatif.

Bon, et puis c'est aussi à titre d'information, pour ma culture
personnelle ! :-)

Vincent

--
SL> Au fait elle est mieux ma signature maintenant ?
Oui. T'enlève encore les conneries que t'as écrit dedans et c'est bon.
-+- JB in <http://www.le-gnu.net> : Le neuneuttoyage par le vide -+-


Avatar
Fabien LE LEZ
On Wed, 03 Sep 2003 22:36:12 +0200, Vincent Richard
wrote:

Cette recherche a-t-elle un coût suffisant pour ralentir ton
application de façon significative ?


Cette fonction est appelée dans le cadre du tri d'une liste
de plusieurs dizaines de milliers d'éléments, donc je pense que
oui, c'est significatif.


Tu ne peux pas le savoir sans faire de mesures du temps d'exécution.
On a souvent des surprises, surtout avec la capacité d'optimisation
des compilateurs et les architectures tordues des processeurs.


Avatar
Durand Richard
As-tu fait des tests de temps d'exécution avec une fonction membre non
virtuel ? Car il me semble que ce genre d'optimisation soit à la portée d'un
compilateur moderne.
Avatar
Frédéri MIAILLE
int main()
{
A* a = new B;

typedef void (A::*FuncPtr)(int);
FuncPtr pf = &A::f;

for (int i = 0 ; i < 10000 ; ++i)
(a->*pf)(i);
}




A mon avis, ce n'est pas possible et la recherche est obligatoire car ce
pointeur peut changer de type d'une itération à l'autre et dans cette
hypothèse la recherche est tout de même effectuée. Même s'il n'en est rien.

Première hypothèse :
Ce qui se produirait, et conformément à ce que tu disais, logiquement, c'est
que A::f() soit resolu lors de son appel ou encore, que pf devienne fou
entre
FuncPtr pf = &A::f; et f() qui changerai alors d'adresse.
Donc, comment veux-tu retenir l'adresse de quelque chose qui va changer lors
de son emploi ?

Seconde hypothèse :
A la rigueur, tu vas retenir un pointeur sur fonction qui va lorsque tu vas
l'utiliser, effectuer également le même type de recherche.

Et je ne sais pas si un pointeur sur fonction virtuelle pure est prévu dans
l'emploi du langage.

Troisième et dernière hypothèse :
Et puis, à ce moment là, ta fonction virtuelle ne sert à rien. Autant faire
un objet du type dérivé sans passer par la classe de base, donc redéfinir
ton objet, pour ensuite l'employer tranquillement, puisqu'il ne change pas.


--
Frédéri MIAILLE
fr.comp.lang.c
fr.comp.lang.c++
fr.comp.graphisme.programmation
fr.comp.os.ms-windows.programmation

Avatar
kanze
Vincent Richard wrote
in message news:<3f56452c$0$27055$...

Soit le programme suivant :

class A
{
public:
virtual void f(const int p) = 0;
};

class B : public A
{
public:
void f(const int p) { /* ... */ }
};

int main()
{
A* a = new B;

for (int i = 0 ; i < 10000 ; ++i)
a->f(i);
}

Je crois savoir qu'à chaque fois que l'on fait un appel à 'f()', une
"recherche" est effectuée pour appeler la fonction correcte selon
l'objet sur lequel on l'appelle. Cette recherche a un coût.


Un coût très faible, quand même. Il y a des cas où ça importe, mais ils
ne sont pas si fréquent que ça.

Y'a-t-il un moyen de la limiter, par exemple, en obtenant l'adresse
une première fois (résolution), puis en l'appelant ensuite (simple
appel, sans résolution), ce qui fait que la résolution n'est pas faite
à chaque itération ?


Utiliser un compilateur avec un bon optimisateur, un qui comprend le
C++. L'optimisation en question, d'enlever des calculs constants d'une
boucle, est une des plus classiques. Le seul problème pour le
compilateur, c'est de reconnaître que le calcul est constant, c-à-d en
fait, que le vptr de l'objet est une constante la vie durante de
l'objet. Si l'optimisateur connaît le C++, pas de problème. Sinon, c'est
quasiment impossible, puisqu'un pointeur à l'objet est passé à f, qui
pourrait en théorie bien modifier ce pointeur.

Par exemple :

int main()
{
A* a = new B;
PointeurSurLaFonction pf = ???;

for (int i = 0 ; i < 10000 ; ++i)
(pf)(i);
}

Apparemment, cela ne peut pas être résolu avec les pointeurs sur
fonctions membres :


Certainement, mais le resultat risque fort d'être moins rapide qu'un
appel à une fonction virtuelle.

int main()
{
A* a = new B;

typedef void (A::*FuncPtr)(int);
FuncPtr pf = &A::f;

for (int i = 0 ; i < 10000 ; ++i)
(a->*pf)(i);
}

...puisque dans ce cas, la résolution est évidemment quand même faite.



Je ne sais pas si je suis très explicite...


Pourquoi pas a->B::f(i) ? Si tu sais le type réel au moment que tu écris
la boucle. (Mais à ne faire que si le profiling le montre réelement
nécessaire. C'est une piège pour la maintenance.)

--
James Kanze GABI Software mailto:
Conseils en informatique orientée objet/ http://www.gabi-soft.fr
Beratung in objektorientierter Datenverarbeitung
11 rue de Rambouillet, 78460 Chevreuse, France, +33 (0)1 30 23 45 16

Avatar
kanze
"Frédéri MIAILLE" wrote in message
news:<bj5l5q$d9e$...

int main()
{
A* a = new B;

typedef void (A::*FuncPtr)(int);
FuncPtr pf = &A::f;

for (int i = 0 ; i < 10000 ; ++i)
(a->*pf)(i);
}


A mon avis, ce n'est pas possible et la recherche est obligatoire car
ce pointeur peut changer de type d'une itération à l'autre et dans
cette hypothèse la recherche est tout de même effectuée. Même s'il
n'en est rien.


Et comment le pointeur peut-il changer de type d'une itération à
l'autre ? Il ne change pas de valeur dans la boucle ici, et il n'est
accessible nul part ailleur. La seule possibilité, c'est que si la
fonction f fait quelque chose du genre :

this->~B() ;
new ( this ) C() ;

(où C est aussi dérivé de A) ; je ne me mettrais pas ma main au feu,
mais je crois que ça invoque un comportement indéfini (du fait que la
position de la partie A dans C n'est pas garantie être la même que la
position de la partie A dans B, si rien d'autre).

Mais qu'il utilise un pointeur à une fonction membre, ou qu'il fait
l'appel direct, ne change en principe rien, et j'imagine que l'analyse
dans le cas de l'appel direct serait plus simple pour le compilateur.

Première hypothèse :
Ce qui se produirait, et conformément à ce que tu disais, logiquement,
c'est que A::f() soit resolu lors de son appel ou encore, que pf
devienne fou entre FuncPtr pf = &A::f; et f() qui changerai alors
d'adresse. Donc, comment veux-tu retenir l'adresse de quelque chose
qui va changer lors de son emploi ?


C'est le problème du compilateur, et c'est la raison pourquoi les
pointeur à des fonctions membres sont souvent assez gros -- typiquement,
il contient l'information si la fonction est virtuelle ou non, et soit
l'adresse réele de la fonction non-virtuelle, soit la position de
l'entrée dans la vtable de la fonction virtuelle.

Seconde hypothèse :
A la rigueur, tu vas retenir un pointeur sur fonction qui va lorsque
tu vas l'utiliser, effectuer également le même type de recherche.


C'est une possibilité. Ça s'appelle une trampoline, et je sais que g++
s'en servais dans certains cas, au moins dans la passée. Je ne sais pas
si les pointeurs à des fonctions membres en étaient un des cas.

Et je ne sais pas si un pointeur sur fonction virtuelle pure est prévu
dans l'emploi du langage.


Je m'en sers tous le temps.

Troisième et dernière hypothèse :
Et puis, à ce moment là, ta fonction virtuelle ne sert à rien. Autant
faire un objet du type dérivé sans passer par la classe de base, donc
redéfinir ton objet, pour ensuite l'employer tranquillement, puisqu'il
ne change pas.


Il sait que dans la boucle en question, la virtualité ne sert à rien. Je
suppose qu'il l'a introduit pour d'autres parties de l'application,
qu'il ne nous a pas montrées.

--
James Kanze GABI Software mailto:
Conseils en informatique orientée objet/ http://www.gabi-soft.fr
Beratung in objektorientierter Datenverarbeitung
11 rue de Rambouillet, 78460 Chevreuse, France, +33 (0)1 30 23 45 16


Avatar
Michaël Cortex
Vincent Richard wrote:
Je crois savoir qu'à chaque fois que l'on fait un appel à 'f()', une
"recherche" est effectuée pour appeler la fonction correcte selon
l'objet sur lequel on l'appelle. Cette recherche a un coût.


Ce n'est *vraiment* une recherche (je ne sais pas dans quel sens tu
l'entends). La fonction est prise dans la vtable de l'objet pointé. Du coup,
on ne fait de truc du style "regarder de quel type est l'objet, puis appeler
la bonne fonction" (qui pourrait être en effet lent). Mais on fait plutôt
"trouve sa vtable et appelle la fonction 'f' dedans", et la vtable change
pour chaque type. C'est un peu le principe du polymorphisme (mais à une
autre échelle).

Stroustup, dans TCPL, dit que ce "overhead" devrait être ignoré, car presque
supprimé par les bons compilateurs... Mais bon, c'est de la théorie :) Ca
n'a jamais été critique à un ce point pour moi que je voulais optimiser sur
ce point.
--
<=- Michaël "Cortex" Monerau -=>

Avatar
Vincent Richard
Merci à tous pour vos réponses, je vais donc laisser comme cela et
laisser faire le compilateur.

Vincent

--
SL> Au fait elle est mieux ma signature maintenant ?
Oui. T'enlève encore les conneries que t'as écrit dedans et c'est bon.
-+- JB in <http://www.le-gnu.net> : Le neuneuttoyage par le vide -+-
Avatar
Christophe Lephay
a écrit dans le message de
news:
Vincent Richard wrote
in message news:<3f56452c$0$27055$...
int main()
{
A* a = new B;

for (int i = 0 ; i < 10000 ; ++i)
a->f(i);
}


Pourquoi pas a->B::f(i) ? Si tu sais le type réel au moment que tu écris
la boucle.


Je crois que ce qu'il avait en tête, c'est de ne pas connaitre le type de a
à la compilation...

Chris


1 2 3