Optimisation d’une twig pour un chemin plus commun connu

Veuillez considérer le code suivant:

void error_handling(); bool method_impl(); bool method() { const bool res = method_impl(); if (res == false) { error_handling(); return false; } return true; } 

Je sais que method_impl() renverra 99,999% du temps (si trois décimales), mais mon compilateur ne le fait pas. method() est partiellement critique en termes de consommation de temps.

  1. Devrais-je réécrire method() (et le rendre moins lisible) pour m’assurer qu’un saut ne peut se produire que lorsque method_impl() renvoie false ? Si oui comment?
  2. Devrais-je laisser le compilateur faire le travail pour moi?
  3. Devrais-je laisser la prédiction de twig de mon processeur faire le travail pour moi?

Le matériel sous-jacent effectue déjà ces optimisations. Il “échouera” à le prédire les premières fois, mais ensuite, l’option correcte sera fr.wikipedia.org/wiki/Branch_predictor.

Vous pouvez essayer d’appliquer l’extension GCC et vérifier si elle est plus rapide avec elle ou non, mais je pense que vous verrez à peine une différence avec et sans elle. La prédiction de twig est appliquée toujours, ce n’est pas quelque chose que vous activez

Vous pouvez suggérer au compilateur que method_impl() renvoie true:

 void error_handling(); bool method_impl(); bool method() { const bool res = method_impl(); if (__builtin_expect (res, 0) == false) { error_handling(); return false; } return true; } 

Cela fonctionnera dans GCC.

Suite aux suggestions d’autres réponses, j’ai comparé les solutions. Si vous envisagez de revigorer cette réponse, votez également pour les autres.

Code de référence

 #include  #include  #include  // solutions #include  // benchmak #include  #include  #include  #include  #include  // // Solutions // namespace { volatile std::time_t near_futur = -1; void error_handling() { std::cerr << "error\n"; } bool method_impl() { return std::time(NULL) != near_futur; } bool method_no_builtin() { const bool res = method_impl(); if (res == false) { error_handling(); return false; } return true; } bool method_builtin() { const bool res = method_impl(); if (__builtin_expect(res, 1) == false) { error_handling(); return false; } return true; } bool method_rewritten() { const bool res = method_impl(); if (res == true) { return true; } else { error_handling(); return false; } } } // // benchmark // constexpr std::size_t BENCHSIZE = 50'000'000; class Clock { std::chrono::time_point _start; public: static inline std::chrono::time_point now() { return std::chrono::steady_clock::now(); } Clock() : _start(now()) { } template std::size_t end() { return std::chrono::duration_cast(now() - _start).count(); } }; // // Entry point // int main() { { Clock clock; bool result = true; for (std::size_t i = 0 ; i < BENCHSIZE ; ++i) { result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); result &= method_no_builtin(); } const double unit_time = clock.end() / static_cast(BENCHSIZE); std::cout << std::setw(40) << "method_no_builtin(): " << std::setprecision(3) << unit_time << " ns\n"; } { Clock clock; bool result = true; for (std::size_t i = 0 ; i < BENCHSIZE ; ++i) { result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); result &= method_builtin(); } const double unit_time = clock.end() / static_cast(BENCHSIZE); std::cout << std::setw(40) << "method_builtin(): " << std::setprecision(3) << unit_time << " ns\n"; } { Clock clock; bool result = true; for (std::size_t i = 0 ; i < BENCHSIZE ; ++i) { result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); result &= method_rewritten(); } const double unit_time = clock.end() / static_cast(BENCHSIZE); std::cout << std::setw(40) << "method_rewritten(): " << std::setprecision(3) << unit_time << " ns\n"; } } 

Résultats de référence

g++ -std=c++14 -O2 -Wall -Wextra -Werror main.cpp

  method_no_builtin(): 45 ns method_builtin(): 44.8 ns method_rewritten(): 44.5 ns 

Démo

g++ -std=c++14 -O3 -Wall -Wextra -Werror main.cpp

  method_no_builtin(): 32.9 ns method_builtin(): 32.1 ns method_rewritten(): 32.3 ns 

Démo

Conclusion

La différence entre ces optimisations est trop minime pour que l'on puisse en tirer une conclusion autre que celle-ci: s'il y a un gain de performance à trouver dans l'optimisation d'une twig pour un chemin plus connu, ce gain est trop petit pour valoir la peine et la perte de lisibilité .

Sans connaître l’implémentation de std :: time (), je ne conclurais pas grand chose de ce test. De vos propres résultats, il semble que cela domine le temps dans la boucle.

FWIW, j’utilise moi-même libéralement et probablement () / probable () lors du réglage du code. Je ne veux pas changer la structure du code, mais lors de la lecture de l’assemblage, j’aimerais voir le chemin commun être une ligne droite de twigs non sockets. La clé ici (pour moi) est la lisibilité de l’assemblage. Le fait que ce soit aussi ce qui sera le plus rapide est secondaire (la twig la plus rapide possible est une twig non prise correctement prédite).