Analyse et optimisation en C++ et Python Denis Steckelmacher [email protected] Université Libre de Bruxelles 12 décembre 2016 Débogage en C++ Analyse des performances Optimisation Introduction 1. Débogage en C++ 2. Profilage en C++ et Python 3. Optimisation en C++ et Python 2/47 Débogage en C++ Analyse des performances Optimisation Table des matires Débogage en C++ Analyse des performances Optimisation 3/47 Débogage en C++ Analyse des performances Optimisation Crash en C++ $ ./mon_programme Segmentation fault Pas beaucoup d’information ! 4/47 Débogage en C++ Analyse des performances Optimisation GDB I Surveille un programme et détecte les crashs I Affiche la pile d’appel I Permet l’exécution pas-à-pas 5/47 Débogage en C++ Analyse des performances Optimisation GDB $ gdb --args ./mon_programme [...] (gdb) run Program received signal SIGSEGV, Segmentation fault. 0x00000000004007f1 in do_crash(char**) () (gdb) Toujours pas beaucoup d’information ! 6/47 Débogage en C++ Analyse des performances Optimisation Options de compilation -g Ajoute des informations de débogage -O2 Optimise le code généré -O3 Optimise encore plus -Ofast Optimise au point de rendre des choses incorrectes -flto Optimise au moment du link 7/47 Débogage en C++ Analyse des performances Optimisation GDB $ gdb --args ./mon_programme [...] (gdb) run Program received signal SIGSEGV, Segmentation fault. 0x00000000004007f1 in do_crash (argv=0x7fffffffdb88) at mon_programme.cpp:5 5 std::cout << argv[1][1] << std::endl; (gdb) Numéro de ligne auquel le crash survient 8/47 Débogage en C++ Analyse des performances Optimisation GDB (gdb) bt #0 #1 0x00000000004007f1 in at 0x000000000040082e in at do_crash (argv=0x7fffffffdb88) mon_programme.cpp:5 main (argc=1, argv=0x7fffffffdb88 mon_programme.cpp:10 Pile d’appel 9/47 Débogage en C++ Analyse des performances Optimisation GDB (gdb) print argv[1] $2 = 0x0 Inspection de variables 10/47 Débogage en C++ Analyse des performances Optimisation Et s’il est déjà trop tard ? $ ulimit -c unlimited $ ./mon_programme Segmentation fault (core dumped) $ gdb mon_programme core [...] Program terminated with signal SIGSEGV, Segmentation fault. #0 0x00000000004007f1 in do_crash (argv=0x7ffeb27c3a28) at mon_programme.cpp:5 5 std::cout << argv[1][1] << std::endl; Analyse post-mortem 11/47 Débogage en C++ Analyse des performances Optimisation Table des matires Débogage en C++ Analyse des performances Optimisation 12/47 Débogage en C++ Analyse des performances Optimisation Performances en C++/Python $ perf record ./mon_programme $ perf annotate $ sudo perf top 13/47 Débogage en C++ Analyse des performances Optimisation Performances en C++/Python 14/47 Débogage en C++ Analyse des performances Optimisation Analyse détaillée de code Python $ sudo pip3 install line_profiler $ kernprof -l mon_programme.py $ python3 -m line_profiler mon_programme.py.lprof 15/47 Débogage en C++ Analyse des performances Optimisation Analyse détaillée de code Python % Time Line Contents ===================== @profile def main(): 0.0 N = 0 0.0 0.0 0.5 49.0 50.5 0.0 for i in range(1000): N += 80 for j in range(1000): for k in range(100): N += 2 print(N) 16/47 Débogage en C++ Analyse des performances Optimisation Table des matires Débogage en C++ Analyse des performances Optimisation 17/47 Débogage en C++ Analyse des performances Optimisation Pourquoi optimiser ? I Un programme lent est désagréable voire impossible à utiliser I Un programme pas trop lent mais inefficace consommera trop d’énergie I Le temps, c’est de l’argent, l’énergie aussi Performances et prix des derniers processeurs Intel : Processeur Core i7-6950X Core i7-6850K Core i7-6800K Performances 19 978 14 502 13 596 Prix 1567 $ 585 $ 392 $ 18/47 Débogage en C++ Analyse des performances Optimisation Optimisations automatiques I Les compilateurs modernes (GCC, Clang, ICC, Visual Studio) sont capables d’optimiser votre code eux-mêmes I Laissez-les faire ! Ne compliquez pas votre code inutilement si le compilateur sait faire la même chose I Écrivez du code clair en sachant que votre compilateur l’optimisera pour vous Avec GCC, Clang et ICC, les optimisations automatiques sont activées dès que l’option -O1, -O2 ou -O3 est présente sur la ligne de commande. Sous Visual Studio, il faut activer le mode Release. 19/47 Débogage en C++ Analyse des performances Optimisation Factorisation Ne calcule une valeur qu’une seule fois, même si elle est utilisée plusieurs fois. 1 2 players [ i + 1]. isA = rand () & 1; players [ i + 1]. isTolerant = rand () & 1; Devient : 1 Player * tmp = & players [ i + 1]; 2 3 4 tmp - > isA = rand () & 1; tmp - > isTolerant = rand () & 1; 20/47 Débogage en C++ Analyse des performances Optimisation Propagation des constantes Si un calcul est fait sur des données connues à la compilation, le compilateur fait les calculs 1 2 i n t a = 2 + 3 * 5; f l o a t f = exp (2); Devient : 1 2 i n t a = 17; f l o a t f = 7.3890561; 21/47 Débogage en C++ Analyse des performances Optimisation Réordonnancement Parfois, faire une opération avant une autre la rend plus rapide. 1 2 c = a + b; d = exp ( e ); Devient : 1 2 d = exp ( e ); c = a + b; 22/47 Débogage en C++ Analyse des performances Optimisation Inlining Recopie une fonction au lieu de l’appeler. Cette optimisation est l’une des plus importante et des plus efficaces. 1 2 3 i n t add ( i n t a , i n t b ) { r e t u r n a + b; } 4 5 i n t s = add (3 , 5) Devient : 1 i n t s = 8; // Constantes propag é es 23/47 Débogage en C++ Analyse des performances Optimisation Suppression du code mort Du code jamais appelé peut être supprimé. Généralement, l’inlining produit beaucoup de code mort. 1 2 3 4 i n t add ( i n t a , b o o l negate ) { i f ( negate ) { r e t u r n -a ; } e l s e { r e t u r n a; } } 5 6 i n t t = negate ( var , t r u e ); Devient : 1 i n t t = - var ; 24/47 Débogage en C++ Analyse des performances Optimisation Factorisation des invariants de boucle Ce qui ne dépend pas d’une variable qui change dans une boucle peut en être sorti. 1 2 3 f o r ( i n t i =0; i < N ; ++ i ) { tab [ i ] = exp ( a ); } Devient : 1 f l o a t tmp = exp ( a ); 2 3 4 5 f o r ( i n t i =0; i < N ; ++ i ) { tab [ i ] = tmp ; } 25/47 Débogage en C++ Analyse des performances Optimisation Duplication de boucles Si une boucle contient un gros if, elle sera remplacée par un gros if qui contient des boucles 1 2 3 4 5 6 7 f o r ( i n t i =0; i < N ; ++ i ) { i f ( a == b ) { tab [ i ] += 1; } else { tab [ i ] -= 1; } } 26/47 Débogage en C++ Analyse des performances Optimisation Duplication de boucles Si une boucle contient un gros if, elle sera remplacée par un gros if qui contient des boucles 1 2 3 4 5 6 7 8 9 i f ( a == b ) { f o r ( i n t i =0; tab [ i ] += } } else { f o r ( i n t i =0; tab [ i ] -= } } i < N ; ++ i ) { 1; i < N ; ++ i ) { 1; 27/47 Débogage en C++ Analyse des performances Optimisation Échange de boucles Deux boucles peuvent être échangées pour optimiser les calculs 1 2 3 4 5 f o r ( i n t x =0; x < N ; ++ x ) { f o r ( i n t y =0; y < N ; ++ y ) { tab [ y * N + x ] += 1; } } 28/47 Débogage en C++ Analyse des performances Optimisation Échange de boucles Deux boucles peuvent être échangées pour optimiser les calculs 1 2 f o r ( i n t y =0; y < N ; ++ y ) { i n t tmp = y * N ; 3 f o r ( i n t x =0; x < N ; ++ x ) { tab [ tmp + x ] += 1; } 4 5 6 7 } 29/47 Débogage en C++ Analyse des performances Optimisation Déroulage de boucles Les boucles dont le nombre de tours est connu peuvent être déroulées 1 2 3 f o r ( i n t i =0; i <30; ++ i ) { tab [ i ] *= 2; } Devient : 1 2 3 4 5 f o r ( i n t i =0; i <28; i += 3) { tab [ i ] *= 2; tab [ i +1] *= 2; tab [ i +2] *= 2; } 30/47 Débogage en C++ Analyse des performances Optimisation Déroulage de boucles De même quand le nombre de tours est inconnu (mais le code généré est alors plus compliqué) 1 2 3 f o r ( i n t i =0; i < N ; ++ i ) { tab [ i ] *= 2; } 31/47 Débogage en C++ Analyse des performances Optimisation Déroulage de boucles De même quand le nombre de tours est inconnu (mais le code généré est alors plus compliqué) 1 2 3 4 5 f o r ( i n t i =0; i <N -2; i += 3) { tab [ i ] *= 2; tab [ i +1] *= 2; tab [ i +2] *= 2; } 6 7 8 9 f o r ( i n t i = N /3*3; i < N ; ++ i ) { tab [ i ] *= 2; } 32/47 Débogage en C++ Analyse des performances Optimisation Link-Time Optimization L’optimisation se fait au moment de produire chaque fichier .o, qui sont ensuite fusionnés Optimisation g++ g++ ld g++ .cpp .o 33/47 Débogage en C++ Analyse des performances Optimisation Link-Time Optimization Le compilateur n’optimise plus rien, mais laisse le lieur faire le travail. Il a alors connaissance de tous les fichiers .o et l’inlining devient bien plus performant (30% de gains sur Firefox) g++ Optimisation g++ ld g++ .cpp .o 34/47 Débogage en C++ Analyse des performances Optimisation Link-Time Optimization I Toutes les optimisations présentées ici sont activées par -O2 I Sauf LTO, qui s’active à l’aide de -flto 35/47 Débogage en C++ Analyse des performances Optimisation Optimisations manuelles Les optimisations les plus difficiles mais également les plus intéressantes se font manuellement : I Meilleurs algorithmes I Placement des objets en mémoire I Contrôle de flux 36/47 Débogage en C++ Analyse des performances Optimisation Accès mémoire direct tab tab[i] += 1; tmp ← tab[i] tmp ← tmp + 1 tab[i] ← tmp tab[i] La mémoire centrale est beaucoup plus lente que le processeur ! 37/47 Débogage en C++ Analyse des performances Optimisation Hiérarchie des caches 32 KB tab tab[i] += 1; tmp ← tab[i] tmp ← tmp + 1 tab[i] ← tmp tab L1 tab[i] 5 cycles 6 GB 512 KB tab[i] ... tab[i+15] tab L2 12 cycles tab[i] ... tab[i+15] ~50 ns Les caches réduisent la latence à condition de respecter certaines conditions 38/47 Débogage en C++ Analyse des performances Optimisation Accès fragmentés Temps d’accès ralenti et des données inutiles sont lues tab[2] tab[18] tab[2] tab[18] 39/47 Débogage en C++ Analyse des performances Optimisation Accès non-alignés Entre 2 × S et 64 octets doivent être lus depuis la mémoire tab2[15] tab2[15] 40/47 Débogage en C++ Analyse des performances Optimisation Registres I Temps d’accès très rapide I Très peu nombreux (16 sur les derniers Intel et AMD 64-bit) I Le compilateur met les variables les plus utilisées dans les registres, les autres en mémoire I Évitez d’utiliser plus de 16 variables à la fois ! 41/47 Débogage en C++ Analyse des performances Optimisation Liste de structures (AOS) Petits accès fragmentés 42/47 Débogage en C++ Analyse des performances Optimisation Structure de listes (SOA) Un seul accès contigu 43/47 Débogage en C++ Analyse des performances Optimisation AOS vers SOA 1 2 players [ i ]. payoff = 0; players [ i ]. judgesGood [ j ] = rand () & 3; Le [i] change de place : 1 2 players . payoff [ i ] = 0; players . judgesGood [ i ][ j ] = rand () & 3; 44/47 Débogage en C++ Analyse des performances Optimisation Conditions I La condition de chaque if doit être évaluée I Le processeur ne peut que difficilement prédire si un if sera vrai ou faux I Les while et les for cachent des sauts conditionnels 45/47 Débogage en C++ Analyse des performances Optimisation Et Python ? S’applique à Python : I Meilleurs algorithmes I Organisation de la mémoire I Utilisation de Numpy, Scipy, Theano, etc I PyPy ! I Python est codé en C, plus le C est optimisé, plus Python va vite Le moins de code Python s’exécute, le mieux ! 46/47