J'ai trouvé une régression des performances intéressante dans un petit extrait C ++, lorsque j'active C ++ 11:
#include <vector>
struct Item
{
int a;
int b;
};
int main()
{
const std::size_t num_items = 10000000;
std::vector<Item> container;
container.reserve(num_items);
for (std::size_t i = 0; i < num_items; ++i) {
container.push_back(Item());
}
return 0;
}
Avec g ++ (GCC) 4.8.2 20131219 (version préliminaire) et C ++ 03 j'obtiens:
milian:/tmp$ g++ -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
35.206824 task-clock # 0.988 CPUs utilized ( +- 1.23% )
4 context-switches # 0.116 K/sec ( +- 4.38% )
0 cpu-migrations # 0.006 K/sec ( +- 66.67% )
849 page-faults # 0.024 M/sec ( +- 6.02% )
95,693,808 cycles # 2.718 GHz ( +- 1.14% ) [49.72%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
95,282,359 instructions # 1.00 insns per cycle ( +- 0.65% ) [75.27%]
30,104,021 branches # 855.062 M/sec ( +- 0.87% ) [77.46%]
6,038 branch-misses # 0.02% of all branches ( +- 25.73% ) [75.53%]
0.035648729 seconds time elapsed ( +- 1.22% )
Avec C ++ 11 activé en revanche, les performances se dégradent considérablement:
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
86.485313 task-clock # 0.994 CPUs utilized ( +- 0.50% )
9 context-switches # 0.104 K/sec ( +- 1.66% )
2 cpu-migrations # 0.017 K/sec ( +- 26.76% )
798 page-faults # 0.009 M/sec ( +- 8.54% )
237,982,690 cycles # 2.752 GHz ( +- 0.41% ) [51.32%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
135,730,319 instructions # 0.57 insns per cycle ( +- 0.32% ) [75.77%]
30,880,156 branches # 357.057 M/sec ( +- 0.25% ) [75.76%]
4,188 branch-misses # 0.01% of all branches ( +- 7.59% ) [74.08%]
0.087016724 seconds time elapsed ( +- 0.50% )
Quelqu'un peut-il expliquer cela? Jusqu'à présent, mon expérience était que la STL devient plus rapide en activant C ++ 11, en particulier. grâce à déplacer la sémantique.
EDIT: Comme suggéré, en utilisant à la container.emplace_back();
place les performances sont comparables à la version C ++ 03. Comment la version C ++ 03 peut-elle obtenir la même chose push_back
?
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
36.229348 task-clock # 0.988 CPUs utilized ( +- 0.81% )
4 context-switches # 0.116 K/sec ( +- 3.17% )
1 cpu-migrations # 0.017 K/sec ( +- 36.85% )
798 page-faults # 0.022 M/sec ( +- 8.54% )
94,488,818 cycles # 2.608 GHz ( +- 1.11% ) [50.44%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
94,851,411 instructions # 1.00 insns per cycle ( +- 0.98% ) [75.22%]
30,468,562 branches # 840.991 M/sec ( +- 1.07% ) [76.71%]
2,723 branch-misses # 0.01% of all branches ( +- 9.84% ) [74.81%]
0.036678068 seconds time elapsed ( +- 0.80% )
push_back(Item())
àemplace_back()
la version C ++ 11?Réponses:
Je peux reproduire vos résultats sur ma machine avec les options que vous écrivez dans votre message.
Cependant, si j'active également l' optimisation du temps de liaison (je passe également le
-flto
drapeau à gcc 4.7.2), les résultats sont identiques:(Je compile votre code d'origine, avec
container.push_back(Item());
)Quant aux raisons, il faut regarder le code assembleur généré (
g++ -std=c++11 -O3 -S regr.cpp
). En mode C ++ 11, le code généré est nettement plus encombré que pour le mode C ++ 98 et l' inclusion de la fonctionvoid std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
échoue en mode C ++ 11 avec la valeur par défaut
inline-limit
.Cet échec en ligne a un effet domino. Non pas parce que cette fonction est appelée (elle n'est même pas appelée!) Mais parce que nous devons être préparés: si elle est appelée, les arguments de la fonction (
Item.a
etItem.b
) doivent déjà être au bon endroit. Cela conduit à un code assez désordonné.Voici la partie pertinente du code généré pour le cas où l' inline réussit :
C'est une boucle sympa et compacte. Maintenant, comparons ceci à celui du cas inline échoué :
Ce code est encombré et il se passe beaucoup plus de choses dans la boucle que dans le cas précédent. Avant la fonction
call
(dernière ligne affichée), les arguments doivent être placés de manière appropriée:Même si cela n'est jamais réellement exécuté, la boucle organise les choses avant:
Cela conduit au code désordonné. S'il n'y a pas de fonction
call
parce que l'inline réussit, nous n'avons que 2 instructions de déplacement dans la boucle et il n'y a pas de problème avec le%rsp
(pointeur de pile). Cependant, si l'inline échoue, nous obtenons 6 coups et nous gâchons beaucoup avec le%rsp
.Juste pour étayer ma théorie (notez la
-finline-limit
), à la fois en mode C ++ 11:En effet, si nous demandons au compilateur de faire un peu plus d'efforts pour intégrer cette fonction, la différence de performances disparaît.
Alors, quelle est la conclusion de cette histoire? Les échecs en ligne peuvent vous coûter cher et vous devriez utiliser pleinement les capacités du compilateur: je ne peux que recommander l'optimisation du temps de liaison. Cela a donné une amélioration significative des performances à mes programmes (jusqu'à 2,5x) et tout ce que je devais faire était de passer le
-flto
drapeau. C'est une très bonne affaire! ;)Cependant, je ne recommande pas de jeter votre code avec le mot-clé en ligne; laissez le compilateur décider quoi faire. (L'optimiseur est autorisé à traiter le mot clé en ligne comme un espace blanc de toute façon.)
Grande question, +1!
la source
inline
n'a rien à voir avec la fonction inline; cela signifie «défini en ligne» et non «veuillez l'intégrer». Si vous voulez réellement demander l'inline, utilisez__attribute__((always_inline))
ou similaire.inline
est également une demande vers le compilateur que vous souhaitez que la fonction soit en ligne et par exemple le compilateur Intel C ++ utilisé pour donner des avertissements de performances s'il ne répond pas à votre demande. (Je n'ai pas vérifié icc récemment si c'est toujours le cas.) Malheureusement, j'ai vu des gens mettre leur code à la poubelleinline
et attendre que le miracle se produise. Je n'utiliserais pas__attribute__((always_inline))
; il y a de fortes chances que les développeurs de compilateurs sachent mieux quoi incorporer et quoi ne pas faire. (Malgré le contre-exemple ici.)inline
spécificateur indique à l'implémentation que la substitution en ligne du corps de fonction au point d'appel doit être préférée au mécanisme d'appel de fonction habituel.» (§7.1.2.2) Cependant, les implémentations ne sont pas nécessaires pour effectuer cette optimisation, car c'est en grande partie une coïncidence que lesinline
fonctions sont souvent de bons candidats pour l'inlining. Il est donc préférable d'être explicite et d'utiliser un pragma de compilation.