177 votes

modèle vs std::function

Merci à C++11, nous avons reçu l' std::function famille de foncteur des wrappers. Malheureusement, je n'arrête pas d'entendre que de mauvaises choses à propos de ces nouveaux ajouts. Le plus populaire, c'est qu'ils sont horriblement lent. Je l'ai testé et ils ont vraiment sucer en comparaison avec les modèles.

#include <iostream>
#include <functional>
#include <string>
#include <chrono>

template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }

float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        calc1([](float arg){ return arg * 0.5f; });
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    return 0;
}

111 ms vs 1241 mme. Je suppose que c'est parce que les modèles peuvent être bien incorporé, tout en functions couvrir le fonctionnement interne via les appels virtuels.

Évidemment, les modèles ont leurs problèmes tels que je les vois:

  • ils doivent être fournis comme en-têtes, ce qui n'est pas quelque chose que vous pourriez ne pas souhaiter le faire lors de la libération de votre bibliothèque comme un code source fermé,
  • ils peuvent prendre le temps de compilation beaucoup plus longtemps, à moins extern template-comme la politique est introduit,
  • il n'existe pas (du moins à ma connaissance) propre manière de représenter les exigences (concepts, anyone?) d'un modèle, d'un bar un commentaire décrivant ce genre de foncteur est prévu.

Je peux donc supposer que functions peut être utilisé comme de facto standard de transmission de foncteurs, et dans les endroits où la haute performance attendue des modèles devraient être utilisés?


Edit:

Mon compilateur est le compilateur de Visual Studio 2012 sans CTP.

185voto

Andy Prowl Points 62121

En général, si vous êtes face à une conception de la situation qui vous donne un choix, utiliser des modèles. J'ai souligné le mot design parce que je pense que vous devez vous concentrer sur est la distinction entre les cas d'utilisation d' std::function et des modèles, qui sont assez différents.

En général, le choix de modèles est juste un exemple d'un principe plus large: essayez de spécifier autant de contraintes que possible au moment de la compilation. Le raisonnement est simple: si vous repérez une erreur, ou une incompatibilité de type, avant même que votre programme est généré, vous ne livrons pas un buggy programme à votre client.

En outre, comme vous l'avez justement souligné, les appels de modèle de fonctions sont résolus de manière statique (c'est à dire au moment de la compilation), de sorte que le compilateur a toutes les informations nécessaires pour optimiser et éventuellement d'insérer le code (qui ne serait pas possible si l'appel ont été effectuées par le biais d'un vtable).

Oui, c'est vrai que le modèle n'est pas parfait, et C++11 est toujours en manque d'un soutien pour des concepts; cependant, je ne vois pas comment std::function serait vous faire économiser à cet égard. std::function n'est pas une alternative à des modèles, mais plutôt un outil pour la conception des situations où les modèles ne peuvent pas être utilisés.

L'un de ces cas d'utilisation se pose lorsque vous avez besoin pour résoudre un appel au moment de l'exécution par l'invocation d'un objet appelable qui adhère à une signature spécifique, mais dont le type de béton est inconnu au moment de la compilation. C'est généralement le cas lorsque vous avez une collection de rappels de potentiellement différents types, mais dont vous avez besoin pour invoquer de manière uniforme; le type et le nombre de rappels enregistrés est déterminé au moment de l'exécution en fonction de l'état de votre programme et de l'application de la logique. Certains de ces rappels pourrait être foncteurs, certains pourraient être des fonctions ordinaires, certains pourraient être le résultat de la liaison à d'autres fonctions à certains arguments.

std::function et std::bind offrons également un idiome naturel pour l'activation de la fonctionnelle de la programmation en C++, où les fonctions sont traitées comme des objets et obtenir naturellement au curry et combinés pour générer d'autres fonctions. Bien que ce type de combinaison peuvent être obtenus avec des modèles ainsi, une conception similaire de la situation vient normalement avec les cas d'utilisation qui nécessitent de déterminer le type de combiné appelable objets au moment de l'exécution.

Enfin, il existe d'autres situations où l' std::function est inévitable, par exemple, si vous voulez écrire récursive lambdas; toutefois, ces restrictions ne sont plus dictés par les limites technologiques que par des distinctions conceptuelles je crois.

Pour résumer, l'accent sur la conception et essayer de comprendre ce que sont les conceptuelle des cas d'utilisation pour ces deux constructions. Si vous les mettez dans la comparaison de la façon dont vous l'avez fait, vous forçant dans une arène, il est probable qu'ils n'appartiennent pas.

93voto

Cassio Neri Points 6095

Andy Prowl a bien couvert les problèmes de conception. C'est, bien sûr, très important, mais je crois que la question initiale concerne plus les problèmes de performance liés à l' std::function.

Tout d'abord, une petite remarque sur la technique de mesure: Le 11 ms obtenus pour calc1 n'a pas de sens du tout. En effet, l'assembly généré (ou de déboguer le code assembleur), on peut voir que VS2012 de l'optimiseur est suffisamment intelligent pour réaliser que le résultat de l'appel d' calc1 indépendant de l'itération et se déplace à l'appel de la boucle:

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

En outre, elle se rend compte que l'appelant calc1 n'a pas d'effet visible et tombe à l'appel tout à fait. Par conséquent, la galliano 111ms est le temps que le vide de boucle nécessaire pour exécuter. (Je suis surpris que l'optimiseur a gardé la boucle.) Alors, soyez prudent avec les mesures de temps dans les boucles. Ce n'est pas aussi simple qu'elle le paraît.

Comme il a été souligné, l'optimiseur a plus de mal à comprendre std::function et ne bouge pas à l'appel de la boucle. Donc 1241ms est une mesure juste pour calc2.

Notez que, std::function est capable de stocker différents types de appelable objets. Par conséquent, il doit effectuer un certain type d'effacement de la magie pour le stockage. Généralement, cela implique une allocation dynamique de la mémoire (par défaut par le biais d'un appel à l' new). Il est bien connu que c'est assez cher.

La norme (20.8.11.2.1/5) encorages implémentations pour éviter l'allocation dynamique de la mémoire pour les petits objets qui, fort heureusement, VS2012 n' (en particulier, pour le code d'origine).

Pour avoir une idée de la façon dont beaucoup plus lent, il peut obtenir lors de l'allocation de mémoire est en cause, j'ai changé l'expression lambda pour capturer trois floats. Cela fait l'objet appelable trop grand pour appliquer le petit objet de l'optimisation:

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

Pour cette version, le temps est d'environ 16000ms (par rapport à 1241ms pour le code d'origine).

Enfin, notez que la durée de vie de la lambda renferme que de l' std::function. Dans ce cas, plutôt que de stocker une copie de la lambda, std::function pourrait stocker une "référence". Par "référence", je veux dire un std::reference_wrapper qui est facile à construire par des fonctions std::ref et std::cref. Plus précisément, en utilisant:

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

le temps diminue à environ 1860ms.

J'ai écrit à ce sujet il y a un moment:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

Comme je l'ai dit dans l'article, les arguments ne sont pas tout à fait s'appliquer pour VS2010 en raison de son mauvais support de C++11. Au moment de l'écriture, seulement une version bêta de VS2012 était disponible mais son soutien pour le C++11 est déjà assez bon pour cette question.

39voto

Johan Lundberg Points 5835

Avec Clang il n'y a pas de différence de performances entre les deux

À l'aide de clang (3.2, le tronc 166872) (-O2 sur Linux), les fichiers binaires à partir de deux cas sont identiques.

-Je vais revenir à clang à la fin du post. Mais d'abord, gcc 4.7.2:

Il y a déjà un aperçu de beaucoup de choses, mais je tiens à souligner que le résultat des calculs de calc1 et calcul2 ne sont pas les mêmes, en raison de l'in-lining, etc. Comparer par exemple la somme de tous les résultats:

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

avec calcul2 qui devient

1.71799e+10, time spent 0.14 sec

alors qu'avec calc1 il devient

6.6435e+10, time spent 5.772 sec

c'est un facteur de ~40 dans la différence de vitesse, et un facteur de ~4 dans les valeurs. La première est une différence beaucoup plus importante que ce que l'OP posté (à l'aide de visual studio). En fait l'impression de la valeur a la fin, c'est aussi une bonne idée d'empêcher le compilateur de la suppression de code avec aucun résultat visible (comme-si la règle). Cassio Neri déjà dit dans sa réponse. Notez comment les différents résultats sont -- Un seul doit être prudent lors de la comparaison de la vitesse des facteurs de codes d'effectuer des calculs différents.

Aussi, pour être juste, la comparaison de différentes façons, à plusieurs reprises, le calcul de f(3.3) est peut-être pas intéressant. Si l'entrée est constante, il ne devrait pas être dans une boucle. (Il est facile pour l'optimiseur avis)

Si j'ajoute un utilisateur a fourni une valeur d'argument pour calc1 et 2 le facteur de la vitesse entre calc1 et calcul2 vient jusqu'à un facteur de 5, à partir de 40! Avec visual studio, la différence est de près d'un facteur 2, et avec clang il n'y a pas de différence (voir ci-dessous).

Aussi, comme les multiplications sont rapides, parler sur les facteurs de ralentissement n'est pas souvent que c'est intéressant. Une question plus intéressante est, petite comment sont vos fonctions, et ces appels sont-ils le goulot d'étranglement dans un vrai programme?

Clang:

Clang (j'ai utilisé 3.2) effectivement produit identique binaires quand je flip entre calc1 et calcul2 pour l'exemple de code (ci-dessous). Avec l'exemple original publié dans la question, les deux sont identiques, mais prendre un rien de temps (les boucles sont juste complètement supprimés, comme décrit ci-dessus). Avec mon modifiés exemple, avec -O2:

Nombre de secondes à s'exécuter (best of 3):

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc:          calc1:           1.1 seconds
gcc:          calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

Les résultats calculés de tous les binaires sont les mêmes, et que tous les tests ont été exécutés sur la même machine. Il serait intéressant si quelqu'un avec plus clang ou VS de connaissances pourrait faire des commentaires sur ce que les optimisations ont été accomplies.

Mes modifié le code de test:

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    std::cout << result<< std::endl;
    return 0;
}

14voto

Pete Becker Points 27371

Différents n'est pas le même.

C'est plus lent, car il fait des choses qu'un modèle ne peut pas faire. En particulier, il vous permet d'appeler toute fonction qui peut être appelée avec l'argument donné les types et dont le type de retour est convertible à l'égard de ce type de retour à partir du même code.

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

Notez que le même objet de fonction, fun, est passée à deux appels d' eval. Il est titulaire de deux différentes fonctions.

Si vous n'avez pas besoin de le faire, alors vous ne devriez pas utiliser std::function.

8voto

TheAgitator Points 61

Vous avez déjà quelques bonnes réponses ici, donc je ne vais pas les contredire, en bref la comparaison de std::function pour les modèles, c'est comme comparer des fonctions virtuelles à des fonctions. Vous ne doit jamais "préfèrent" virtuel fonctions de fonctions, mais plutôt vous utilisez des fonctions virtuelles lorsqu'il adapte le problème, le déplacement des décisions à partir de la compilation à l'exécution. L'idée est que, plutôt que d'avoir à résoudre le problème à l'aide d'une solution sur mesure (comme le saut de table), vous utilisez quelque chose qui donne le compilateur une meilleure chance d'optimisation pour vous. Il contribue également à d'autres programmeurs, si vous utilisez une solution standard.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X