J'ai lu des informations sur les pointeurs de fonction en C. Et tout le monde a dit que cela ralentirait mon programme. Est-ce vrai?
J'ai fait un programme pour le vérifier. Et j'ai eu les mêmes résultats dans les deux cas. (mesure le temps.)
Alors, est-ce mauvais d'utiliser un pointeur de fonction? Merci d'avance.
Répondre pour certains gars. J'ai dit «cours lentement» pendant le temps que j'ai comparé sur une boucle. comme ça:
int end = 1000;
int i = 0;
while (i < end) {
fp = func;
fp ();
}
Lorsque vous exécutez ceci, j'ai le même temps si j'exécute ceci.
while (i < end) {
func ();
}
Je pense donc que le pointeur de fonction n’a aucune différence de temps et qu’il ne ralentit pas un programme, comme beaucoup de gens l’ont dit.
Vous voyez, dans les situations qui importent du point de vue des performances, comme appeler la fonction de manière répétée plusieurs fois au cours d'un cycle, les performances peuvent ne pas être différentes du tout.
Cela peut sembler étrange aux gens, qui sont habitués à penser que le code C est exécuté par une machine C abstraite dont le "langage machine" reflète étroitement le langage C lui-même. Dans un tel contexte, "par défaut" un appel indirect à une fonction est en effet plus lent que direct, car il implique formellement un accès mémoire supplémentaire afin de déterminer la cible de l'appel.
Cependant, dans la réalité, le code est exécuté par une machine réelle et compilé par un compilateur optimiseur qui possède une assez bonne connaissance de l'architecture de la machine sous-jacente, ce qui l'aide à générer le code le plus optimal pour cette machine spécifique. Et sur de nombreuses plates-formes, il peut s'avérer que le moyen le plus efficace d'effectuer un appel de fonction à partir d'un cycle résulte en un code identique / pour les appels directs et indirects, conduisant à des performances identiques pour les deux.
Considérons, par exemple, la plate-forme x86. Si nous traduisons "littéralement" un appel direct et indirect en code machine, nous pourrions nous retrouver avec quelque chose comme ceci
// Direct call
do-it-many-times
call 0x12345678
// Indirect call
do-it-many-times
call dword ptr [0x67890ABC]
Le premier utilise un opérande immédiat dans l’instruction machine et est en effet normalement plus rapide que le second, qui doit lire les données à partir d’un emplacement mémoire indépendant.
À ce stade, n’oublions pas que l’architecture x86 offre en réalité un moyen supplémentaire de fournir un opérande à l’instruction call
. Il fournit l'adresse cible dans un registre . Et une chose très importante à propos de ce format est qu’il est normalement plus rapide que les deux précédents . Qu'est ce que cela veut dire pour nous? Cela signifie qu'un bon compilateur optimiseur doit et va en tirer parti. Afin de mettre en œuvre le cycle ci-dessus, le compilateur essaiera d'utiliser un appel via un registre dans les deux cas. Si cela réussit, le code final pourrait ressembler à ceci
// Direct call
mov eax, 0x12345678
do-it-many-times
call eax
// Indirect call
mov eax, dword ptr [0x67890ABC]
do-it-many-times
call eax
Notez que maintenant la partie qui compte - l'appel réel dans le corps du cycle - est exactement et exactement la même dans les deux cas. Inutile de dire que la performance va être pratiquement identique .
Aussi étrange que cela puisse paraître, on pourrait même dire que sur cette plate-forme, un appel direct (appel avec un opérande immédiat dans call
) est plus lent qu'un appel indirect, tant que l'opérande de l'appel indirect est fourni en un registre (par opposition à être stocké en mémoire).
Bien sûr, l’ensemble n’est pas aussi simple en général. Le compilateur doit faire face à une disponibilité limitée de registres, à des problèmes d'aliasing, etc. Mais il s'agit de cas aussi simples que celui de votre exemple (et même dans des cas beaucoup plus compliqués), l'optimisation ci-dessus sera effectuée par un bon compilateur et éliminera complètement toute différence de performance entre un appel direct cyclique et un appel indirect cyclique. Cette optimisation fonctionne particulièrement bien en C++, lors de l’appel d’une fonction virtuelle, car dans une implémentation typique, les pointeurs impliqués sont entièrement contrôlés par le compilateur, ce qui lui donne une parfaite connaissance de l’image de repliement du spectre et d’autres éléments pertinents.
Bien sûr, il y a toujours une question de savoir si votre compilateur est assez intelligent pour optimiser des choses comme ça ...
Je pense que lorsque les gens disent cela, ils font référence au fait que l'utilisation de pointeurs de fonction peut empêcher les optimisations du compilateur (en ligne) et du processeur (prédiction de branche). Cependant, si les pointeurs de fonction sont un moyen efficace d’accomplir quelque chose que vous essayez d’exécuter, il est probable que toute autre méthode de le faire présenterait les mêmes inconvénients.
Et à moins que vos pointeurs de fonction ne soient utilisés en boucles serrées dans une application critique en termes de performances ou sur un système embarqué très lent, il est toutefois probable que la différence soit négligeable.
Beaucoup de gens ont fourni de bonnes réponses, mais je pense toujours qu'il manque un point. Les pointeurs de fonction ajoutent une déréférence supplémentaire qui les ralentit de plusieurs cycles. Ce nombre peut augmenter en fonction d'une mauvaise prédiction de branche (ce qui n'a d'ailleurs rien à voir avec le pointeur de fonction lui-même). De plus, les fonctions appelées via un pointeur ne peuvent pas être en ligne. Mais ce qui manque aux gens, c'est que la plupart des gens utilisent les pointeurs de fonction comme une optimisation.
Les API de c/c ++ se trouvent généralement dans les fonctions de rappel. Cela s'explique par le fait que l'écriture d'un système qui appelle un pointeur de fonction chaque fois qu'un événement se produit est beaucoup plus efficace que d'autres méthodes telles que le transfert de messages. Personnellement, j'ai également utilisé des pointeurs de fonction dans le cadre d'un système de traitement d'entrée plus complexe, dans lequel chaque touche du clavier est associée à un pointeur de fonction via une table de saut. Cela m'a permis de supprimer toute ramification ou logique du système de saisie et de gérer simplement la pression de touche entrant.
Et tout le monde a dit que cela ralentirait mon programme . Est-ce vrai?
Très probablement, cette affirmation est fausse. D'une part, si l'alternative à l'utilisation de pointeurs de fonction est quelque chose comme
if (condition1) {
func1();
} else if (condition2)
func2();
} else if (condition3)
func3();
} else {
func4();
}
c'est très probablement relativement beaucoup plus lent que d'utiliser un seul pointeur de fonction. Bien que l'appel d'une fonction par un pointeur entraîne une surcharge (généralement négligeable), il ne s'agit normalement pas de la différence entre appel direct à fonction et appel direct à travers pointeur qu'il convient de comparer.
Et deuxièmement, ne jamais optimiser les performances sans aucune mesure. Il est très difficile de savoir où se trouvent les goulots d'étranglement (read impossible ) et cela peut parfois être assez peu intuitif (par exemple, les développeurs du noyau Linux ont commencé à supprimer le mot clé inline
des fonctions car cela nuisait vraiment aux performances).
L'appel d'une fonction via un pointeur de fonction est un peu lent par rapport à un appel de fonction statique, car l'appel précédent comprend un déréférencement supplémentaire du pointeur. Pour autant que je sache, cette différence est négligeable sur la plupart des machines modernes (sauf peut-être certaines plates-formes spéciales aux ressources très limitées).
Les pointeurs de fonction sont utilisés car ils peuvent rendre le programme beaucoup plus simple, plus propre et plus facile à gérer (bien utilisé, bien sûr). Ceci compense largement la possible différence de vitesse mineure.
L'utilisation d'un pointeur de fonction est plus lente que d'appeler une fonction car il s'agit d'une autre couche d'indirection. (Le pointeur doit être déréférencé pour obtenir l'adresse mémoire de la fonction). Bien que ce soit plus lent, comparé à tout ce que votre programme peut faire (lire un fichier, écrire sur la console), il est négligeable.
Si vous devez utiliser des pointeurs de fonction, utilisez-les, car tout ce qui essaie de faire la même chose mais en évitant de les utiliser sera plus lent et moins facile à gérer que d'utiliser des pointeurs de fonction.
Beaucoup de bons points dans les réponses précédentes.
Cependant, jetez un oeil à la fonction de comparaison C qsort. Étant donné que la fonction de comparaison ne peut pas être insérée en ligne et doit respecter les conventions d'appels standard basées sur la pile, le temps total d'exécution du tri peut être un ordre de grandeur (plus exactement 3-10x) plus lent pour les clés entières, sinon même code avec un appel direct, en ligne.
Une comparaison en ligne typique serait une séquence d'instructions CMP simples et éventuellement CMOV/SET. Un appel de fonction entraîne également les frais généraux d'un appel, en configurant le cadre de pile, en effectuant la comparaison, en supprimant le cadre de pile et en renvoyant le résultat. Notez que les opérations de pile peuvent provoquer des blocages de pipeline en raison de la longueur du pipeline de la CPU et des registres virtuels. Par exemple, si la valeur de say eax est nécessaire avant que l'instruction qui a été modifiée en dernier ne soit terminée (ce qui prend généralement environ 12 cycles d'horloge sur les processeurs les plus récents). Si le processeur ne peut pas exécuter d'autres instructions dans l'attente, un blocage du pipeline se produira.
Peut-être.
La réponse dépend de l'utilisation du pointeur de la fonction et donc des alternatives. Comparer les appels de pointeur de fonction à des appels de fonction directs est trompeur si un pointeur de fonction est utilisé pour implémenter un choix qui fait partie de la logique de notre programme et qui ne peut pas être simplement supprimé. Je vais continuer et néanmoins montrer cette comparaison et revenir à cette pensée après.
Les appels de pointeur de fonction ont le plus de chances de dégrader les performances par rapport aux appels de fonction directs lorsqu'ils inhibent l'inline. Parce que l'inline est une optimisation de passerelle, nous pouvons créer des cas extrêmement pathologiques dans lesquels les pointeurs de fonction sont créés arbitrairement plus lentement que l'appel de fonction direct équivalent:
void foo(int* x) {
*x = 0;
}
void (*foo_ptr)(int*) = foo;
int call_foo(int *p, int size) {
int r = 0;
for (int i = 0; i != size; ++i)
r += p[i];
foo(&r);
return r;
}
int call_foo_ptr(int *p, int size) {
int r = 0;
for (int i = 0; i != size; ++i)
r += p[i];
foo_ptr(&r);
return r;
}
Code généré pour call_foo()
:
call_foo(int*, int):
xor eax, eax
ret
Agréable. foo()
a non seulement été inséré, mais a également permis au compilateur d’éliminer toute la boucle précédente! Le code généré met simplement à zéro le registre de retour en XORing le registre avec lui-même, puis retourne. D'autre part, les compilateurs devront générer du code pour la boucle dans call_foo_ptr()
(plus de 100 lignes avec gcc 7.3) et la plupart de ce code ne fait rien (tant que foo_ptr
pointe toujours sur foo()
). (Dans des scénarios plus typiques, vous pouvez vous attendre à ce que le fait d'inclure une petite fonction dans une boucle interne à chaud permette de réduire le temps d'exécution jusqu'à environ un ordre de grandeur.)
Donc, dans le pire des cas, un appel de pointeur de fonction est arbitrairement plus lent qu'un appel de fonction direct, mais cela est trompeur. Il s'avère que si foo_ptr
avait été const
, alors call_foo()
et call_foo_ptr()
auraient généré le même code. Cependant, cela nous obligerait à renoncer à la possibilité d'indirection fournie par foo_ptr
. Est-il "juste" que foo_ptr
soit const
? Si nous sommes intéressés par l'indirection fournie par foo_ptr
, alors non, mais si tel est le cas, un appel de fonction direct n'est pas non plus une option valide.
Si un pointeur de fonction est utilisé pour fournir une indirection utile, nous pouvons alors déplacer l'indirection ou, dans certains cas, permuter les pointeurs de fonction pour des conditions ou même des macros, mais nous ne pouvons pas simplement le supprimer. Si nous avons décidé que les pointeurs de fonction constituaient une bonne approche mais que les performances posaient problème, nous souhaitons généralement extraire l'indirection vers le haut de la pile d'appels afin de payer le coût de l'indirection dans une boucle externe. Par exemple, dans le cas courant où une fonction prend un rappel et l'appelle dans une boucle, nous pouvons essayer de déplacer la boucle la plus interne dans le rappel (et de modifier la responsabilité de chaque invocation de rappel en conséquence).