Faites de vos programmes des programmes rapides avec le profilage

Go est souvent utilisé pour l'écriture de systèmes distribués, de magasins de données avancées et de microservices. La performance est la clé dans ces domaines. 

Dans ce didacticiel, vous apprendrez à profiler vos programmes pour les rendre rapides (utilisation plus efficace du processeur) ou légers (utilisez moins de mémoire). Je traiterai du profilage du processeur et de la mémoire, de l’utilisation de pprof (le profileur Go), de la visualisation des profils et même des graphiques de flamme..

Le profilage mesure la performance de votre programme dans différentes dimensions. Go fournit un excellent support pour le profilage et permet de profiler les dimensions suivantes prêtes à l'emploi:

  • un échantillon de temps CPU par fonction ET instruction
  • un échantillon de toutes les allocations de tas
  • empiler les traces de tous les goroutines actuelles
  • traces de pile qui ont conduit à la création de nouveaux threads de système d'exploitation
  • traces de pile qui ont conduit à un blocage sur les primitives de synchronisation
  • empiler les traces des détenteurs de mutex en conflit

Vous pouvez même créer des profils personnalisés si vous le souhaitez. Le profilage consiste à créer un fichier de profil, puis à l’analyser à l’aide de la pprof aller outil.

Comment créer des fichiers de profil

Il existe plusieurs façons de créer un fichier de profil..

Utiliser "go test" pour générer des fichiers de profil

Le moyen le plus simple est d'utiliser aller tester. Il comporte plusieurs indicateurs qui vous permettent de créer des fichiers de profil. Voici comment générer à la fois un fichier de profil de CPU et un fichier de profil de mémoire pour le test dans le répertoire actuel: allez tester -cpuprofile cpu.prof -memprofile mem.prof -bench .

Télécharger des données de profil en direct depuis un service de longue durée

Si vous souhaitez profiler un service Web de longue durée, vous pouvez utiliser l'interface HTTP intégrée pour fournir des données de profil. Ajoutez quelque part l'instruction d'importation suivante:

importer _ "net / http / pprof"

Maintenant, vous pouvez télécharger des données de profil en direct à partir du / debug / pprof / URL Plus d'informations sont disponibles dans la documentation du paquet net / http / pprof.

Profilage dans le code

Vous pouvez également ajouter un profilage direct dans votre code pour un contrôle complet. Vous devez d'abord importer runtime / pprof. Le profilage de la CPU est contrôlé par deux appels:

  • pprof.StartCPUProfile ()
  • pprof.StopCPUProfile ()

Le profilage de la mémoire se fait en appelant runtime.GC () suivi par pprof.WriteHeapProfile ().

Toutes les fonctions de profilage acceptent un descripteur de fichier que vous êtes responsable de l'ouverture et de la fermeture appropriées..

Le programme exemple

Pour voir le profileur en action, je vais utiliser un programme qui résout le problème 8 du projet Euler. Le problème est le suivant: à partir d'un nombre à 1 000 chiffres, trouvez les 13 chiffres adjacents de ce numéro qui ont le plus gros produit.. 

Voici une solution triviale qui effectue une itération sur toutes les séquences de 13 chiffres et, pour chacune de ces séquences, multiplie ces 13 chiffres et renvoie le résultat. Le résultat le plus important est stocké et finalement renvoyé:

paquet trivial import ("chaînes") func calcProduct (chaîne de la série) int64 chiffres: = make ([] int64, len (série)) pour i, c: = plage série digits [i] = int64 (c) - 48  produit: = int64 (1) pour i: = 0; je < len(digits); i++  product *= digits[i]  return product  func FindLargestProduct(text string) int64  text = strings.Replace(text, "\n", "", -1) largestProduct := int64(0) for i := 0; i < len(text); i++  end := i + 13 if end > len (texte) end = len (texte) série: = texte [i: fin] résultat: = calcProduct (série) si résultat> plus grand produit plus grand produit = résultat retour plus grandProduit 

Plus tard, après le profilage, nous verrons comment améliorer les performances avec une autre solution..

Profilage de la CPU

Profilons le CPU de notre programme. Je vais utiliser la méthode de test en utilisant ce test:

import ( "testing") texte const = '73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 85861560789112949495459501737958331952853208805511 12540698747158523863050715693290963295227443043557 66896648950445244523161731856403098711121722383113 62229893423380308135336276614282806444486645238749 30358907296290491560440772390713810515859307960866 70172427121883998797908792274921901699720888093776 65727333001053367881220235421809751254540594752243 52584907711670556013604839586446706324415722155397 53697817977846174064955149290862569321978468622482 83972241375657056057490261407972968652414535100474 82166370484403199890008895243450658541227588666881 16427171479924442928230863465674813919123162824586 17866458359124566529476545682848912883142607690042 24219022671055626321111109370544217506941658960408 07198403850962455444362981230987879927244284909188 84580156166097919133875499200524063689912560717606 0588611646710940507754100225698315520005593572 9725 71636269561882670428252483600823257530420752963450 'func TestFindLargestProduct (t * testing.T) pour i: = 0; je < 100000; i++  res := FindLargestProduct(text) expected := int64(23514624000) if res != expected  t.Errorf("Wrong!")    

Notez que j'exécute le test 100 000 fois, car le profileur go est un profileur d'échantillonnage qui a besoin du code pour passer un temps considérable (plusieurs millisecondes cumulées) sur chaque ligne de code. Voici la commande pour préparer le profil:

allez tester -cpuprofile cpu.prof -bench. ok _ / github.com / the-gigi / project-euler / 8 / go / trivial 13.243s 

Cela a pris un peu plus de 13 secondes (pour 100 000 itérations). Maintenant, pour afficher le profil, utilisez l'outil pprof go pour accéder à l'invite interactive. Il y a beaucoup de commandes et d'options. La commande la plus élémentaire est topN; avec l'option -cum, il affiche les N principales fonctions dont l'exécution a pris le plus de temps (ainsi, une fonction qui prend très peu de temps, mais qui est appelée à plusieurs reprises, peut être située en haut). C'est généralement ce que je commence avec.

> go tool pprof cpu.prof Type: cpu Heure: 23 oct. 2017 à 08:05 am (PDT) Durée: 13.22s, Total des échantillons = 13.10s (99.06%) Entrée en mode interactif (tapez "help" pour les commandes) (pprof ) top5 -cum Nombre de nœuds représentant 1,23 s, 9,39% du total des 13,10s Supprimé 76 nœuds (cum <= 0.07s) Showing top 5 nodes out of 53 flat flat% sum% cum cum% 0.07s 0.53% 0.53% 10.64s 81.22% FindLargestProduct 0 0% 0.53% 10.64s 81.22% TestFindLargestProduct 0 0% 0.53% 10.64s 81.22% testing.tRunner 1.07s 8.17% 8.70% 10.54s 80.46% trivial.calcProduct 0.09s 0.69% 9.39% 9.47s 72.29% runtime.makeslice 

Comprenons la sortie. Chaque ligne représente une fonction. J'ai élidé le chemin d'accès à chaque fonction en raison de contraintes d'espace, mais il sera affiché dans la sortie réelle dans la dernière colonne. 

Plat signifie le temps (ou le pourcentage) passé au sein de la fonction, et Cum signifie cumulatif: le temps passé au sein de la fonction et de toutes les fonctions qu'elle appelle. Dans ce cas, testing.tRunner appelle réellement TestFindLargestProduct (), qui appelle FindLargestProduct (), mais puisque pratiquement aucun temps n’y est passé, le profileur d’échantillonnage compte leur temps plat comme 0.

Profilage de la mémoire

Le profilage de la mémoire est similaire, sauf que vous créez un profil de mémoire:

allez tester -memprofile mem.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / trivial

Vous pouvez analyser votre utilisation de la mémoire en utilisant le même outil.

Utiliser pprof pour optimiser la vitesse de votre programme

Voyons ce que nous pouvons faire pour résoudre le problème plus rapidement. En regardant le profil, on voit que calcProduct () prend 8,17% de la durée d'exécution plate, mais makeSlice (), qui s'appelle de calcProduct (), prend 72% (cumulatif parce qu'il appelle d'autres fonctions). Cela donne une assez bonne indication de ce que nous devons optimiser. Que fait le code? Pour chaque séquence de 13 nombres adjacents, il attribue une tranche:

func calcProduct (series string) int64 chiffres: = make ([] int64, len (série))…… 

C'est presque 1 000 fois par course et nous courons 100 000 fois. Les allocations de mémoire sont lentes. Dans ce cas, il n'est vraiment pas nécessaire d'attribuer une nouvelle tranche à chaque fois. En fait, il n’est pas nécessaire d’attribuer une tranche du tout. Nous pouvons simplement numériser le tableau d'entrée. 

L'extrait de code suivant montre comment calculer le produit en cours en divisant simplement par le premier chiffre de la séquence précédente et en le multipliant par cabot chiffre. 

si cur == 1 currProduct / = old continue si old == 1 currProduct * = cur sinon currProduct = currProduct / old * cur si currProduct> plusProduit plusProduit = currProduct 

Voici une courte liste de certaines des optimisations algorithmiques:

  • Calculer un produit en cours d'exécution. Supposons que nous calculions le produit à l'indice N… N + 13 et que nous l'appelions P (N). Nous devons maintenant calculer le produit à l’indice N + 1… N + 13. P (N + 1) est égal à P (N), sauf que le premier nombre à l'indice N a disparu et que nous devons prendre en compte le nouveau nombre à l'indice N + 14T. Cela peut être fait en divisant le produit précédent par son premier numéro et en le multipliant par le nouveau nombre. 
  • Ne pas calculer une séquence de 13 nombres contenant 0 (le produit sera toujours zéro).
  • Éviter la division ou la multiplication par 1.

Le programme complet est ici. Il existe une logique épineuse de contourner les zéros, mais à part cela, c'est assez simple. L’important est que nous n’allouons qu’un tableau de 1000 octets au début, et nous le transmettons par pointeur (donc pas de copie) au  findLargestProductInSeries () fonction avec une plage d'index.

package scan func findLargestProductInSeries (digits * [1000] octet, début, fin int) int64 if (fin - début) < 13  return -1  largestProduct := int64((*digits)[start]) for i := 1; i < 13 ; i++  d := int64((*digits)[start + i]) if d == 1  continue  largestProduct *= d  currProduct := largestProduct for ii := start + 13; ii < end; ii++  old := int64((*digits)[ii-13]) cur := int64((*digits)[ii]) if old == cur  continue  if cur == 1  currProduct /= old continue  if old == 1  currProduct *= cur  else  currProduct = currProduct / old * cur  if currProduct > greatestProduct moindreProduit = currProduct renvoyer plus grandProduit funC FindLargestProduct (chaîne de texte) int64 var chiffres [1000] octet digIndex: = 0 pour _, c: = texte de la plage si c == 10 continue chiffres [digIndex] = octet (c) - 48 digIndex ++ début: = -1 fin: = -1 findStart: = vrai var mostProduct int64 pour ii: = 0; ii < len(digits) - 13; ii++  if findStart  if digits[ii] == 0  continue  else  start = ii findStart = false   if digits[ii] == 0  end = ii result := findLargestProductInSeries(&digits, start, end) if result > gestProduct plusProduit = résultat findStart = true renvoyer plusProduct

Le test est le même. Voyons comment nous avons fait avec le profil:

> allez tester -cpuprofile cpu.prof -bench. PASS OK _ / github.com / the-gigi / project-euler / 8 / go / scan 0.816s 

Dès le départ, nous pouvons constater que le temps d'exécution est passé de plus de 13 secondes à moins d'une seconde. C'est plutôt bien. Il est temps de jeter un coup d'œil à l'intérieur. Utilisons juste Top 10, qui trie par temps plat.

(pprof) top10 Nombre de nœuds représentant 560 ms, 100% des 560 ms total plat% somme% cum% 290ms 51.79% 51.79% 290ms 51.79% findLargestProductInSeries 250ms 44.64% 96.43% 540ms 96.43% FindLargestProductInSeries 250ms 44.64% 96.43% 540ms 96.43% .usleep 0 0% 100% 540ms 96.43% TestFindLargestProduct 0 0% 100% 20ms 3.57% runtime.mstart 0 0% 100% 20ms 3.57% runtime.mstart1 0 0% 100% 20ms 3.57% runtime.sysmon 0 0% 100% 540ms 96,43% testing.tRunner 

C'est bien. Presque toute la durée d'exécution est passée dans notre code. Aucune allocation de mémoire du tout. Nous pouvons approfondir et examiner le niveau de déclaration à l'aide de la commande list:

(pprof) list FindLargestProduct Total: 560ms ROUTINE ========================= scan.FindLargestProduct 250ms 540ms (à plat, 96,43% du total… 44: … 45:… 46: func FindLargestProduct (chaîne t) int64 … 47: var digits [1000] octet… 48: digIndex: = 0 70ms 70ms 49: pour _, c: = texte de plage … 50: si c == 10 … 51: continue… 52:… 53: digits [digIndex] = octet (c) - 48 10 ms 10 ms 54: digIndex ++… 55:… 56:… 57: début: = -1… 58: fin: = -1… 59: findStart: = true… 60: var plus grandProduit int64… 61: pour ii: = 0; ii < len(digits)-13; ii++  10ms 10ms 62: if findStart … 63: if digits[ii] == 0 … 64: continue… 65:  else … 66: start = ii… 67: findStart = false… 68: … 69: … 70: 70ms 70ms 71: if digits[ii] == 0 … 72: end = ii 20ms 310ms 73: result := f(&digits,start,end) 70ms 70ms 74: if result > plus grandProduit … 75: plusProduit = résultat… 76:… 77: findStart = true… 78:… 79:

C'est assez incroyable. Vous obtenez une déclaration par déclaration calendrier de tous les points importants. Notez que l'appel sur la ligne 73 pour fonction f () est en fait un appel à findLargestProductInSeries (), que j'ai renommé dans le profil pour des raisons d'espace. Cet appel prend 20 ms. Peut-être qu'en intégrant le code de fonction à la place, nous pourrions enregistrer l'appel de fonction (y compris l'allocation de pile et la copie d'arguments) et enregistrer ces 20 ms. Il peut y avoir d’autres optimisations intéressantes que cette vue peut aider à identifier.

Visualisation

Regarder ces profils de texte peut être difficile pour les gros programmes. Go vous donne beaucoup d'options de visualisation. Vous devrez installer Graphviz pour la prochaine section.

L'outil pprof peut générer une sortie dans de nombreux formats. Une des manières les plus simples (sortie svg) est simplement de taper "web" à partir de l'invite interactive pprof, et votre navigateur affichera un joli graphique avec le chemin chaud en rose.

Graphes de flamme

Les graphiques intégrés sont utiles et utiles, mais avec de grands programmes, même ces graphiques peuvent être difficiles à explorer. Le graphique de la flamme est l'un des outils les plus populaires pour visualiser les résultats de performance. L'outil pprof ne le supporte pas encore, mais vous pouvez jouer avec les graphes de flamme déjà en utilisant l'outil go-torch d'Uber. Des travaux sont en cours pour ajouter un support intégré pour les graphiques de flammes à pprof.

Conclusion

Go est un langage de programmation système utilisé pour créer des systèmes distribués et des magasins de données hautes performances. Go est fourni avec un excellent support qui améliore sans cesse le profilage de vos programmes, l'analyse de leurs performances et la visualisation des résultats.. 

L'équipe de Go et la communauté insistent beaucoup sur l'amélioration de l'outil de performance. Le code source complet avec trois algorithmes différents peut être trouvé sur GitHub.