Go viene spesso utilizzato per scrivere sistemi distribuiti, archivi dati avanzati e microservizi. Le prestazioni sono fondamentali in questi domini.
In questo tutorial, imparerai come profilare i tuoi programmi per renderli velocissimi (usa meglio la CPU) o illuminare (usa meno memoria). Tratterò la CPU e la profilatura della memoria, usando il pprof (il profiler Go), visualizzando i profili e persino i grafici di fiamma.
Il profilo sta misurando le prestazioni del tuo programma in varie dimensioni. Go viene fornito con un ottimo supporto per la profilazione e può profilare le seguenti dimensioni fuori dalla scatola:
Puoi persino creare profili personalizzati se lo desideri. Go Profiling comporta la creazione di un file di profilo e quindi l'analisi utilizzando il pprof
vai strumento.
Esistono diversi modi per creare un file di profilo.
Il modo più semplice è usare vai alla prova
. Ha diversi flag che ti permettono di creare file di profilo. Ecco come generare sia un file di profilo della CPU sia un file di profilo di memoria per il test nella directory corrente: vai test -cpuprofile cpu.prof -memprofile mem.prof -bench .
Se si desidera profilare un servizio Web di lunga durata, è possibile utilizzare l'interfaccia HTTP integrata per fornire i dati del profilo. Aggiungi da qualche parte la seguente dichiarazione di importazione:
importare "net / http / pprof"
Ora è possibile scaricare i dati del profilo live dal / Debug / pprof /
URL. Ulteriori informazioni sono disponibili nella documentazione del pacchetto net / http / pprof.
Puoi anche aggiungere il profilo diretto al tuo codice per un controllo completo. Per prima cosa devi importare runtime / pprof
. Il profilo della CPU è controllato da due chiamate:
pprof.StartCPUProfile ()
pprof.StopCPUProfile ()
La profilazione della memoria viene effettuata chiamando Runtime.gc ()
seguito da pprof.WriteHeapProfile ()
.
Tutte le funzioni di profilazione accettano un handle di file di cui si è responsabili per l'apertura e la chiusura in modo appropriato.
Per vedere il profiler in azione, userò un programma che risolve il problema 8 di Project Euler. Il problema è: dato un numero di 1.000 cifre, trova le 13 cifre adiacenti all'interno di questo numero che hanno il prodotto più grande.
Ecco una soluzione banale che itera su tutte le sequenze di 13 cifre, e per ciascuna di tali sequenze moltiplica tutte e 13 le cifre e restituisce il risultato. Il risultato più grande viene archiviato e infine restituito:
package trivial import ("strings") func calcProduct (stringa di serie) int64 digit: = make ([] int64, len (series)) per i, c: = range series digits [i] = int64 (c) - 48 product: = int64 (1) per i: = 0; io < 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 (testo) end = len (testo) serie: = testo [i: fine] risultato: = calcProduct (serie) se risultato> largestProduct largestProduct = result return largestProduct
Successivamente, dopo il profiling, vedremo alcuni modi per migliorare le prestazioni con un'altra soluzione.
Diamo un profilo alla CPU del nostro programma. Userò il metodo go test usando questo test:
importazione ( "testing") Testo 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) per i: = 0; io < 100000; i++ res := FindLargestProduct(text) expected := int64(23514624000) if res != expected t.Errorf("Wrong!")
Nota che eseguo il test 100.000 volte perché il go profiler è un profiler di campionamento che ha bisogno del codice per passare effettivamente un tempo significativo (diversi millisecondi cumulativi) su ogni riga di codice. Ecco il comando per preparare il profilo:
vai test -cpuprofile cpu.prof -bench. ok _ / github.com / the-gigi / project-euler / 8 / go / trivial 13.243s
Ci sono voluti poco più di 13 secondi (per 100.000 iterazioni). Ora, per visualizzare il profilo, utilizzare lo strumento pprof go per accedere al prompt interattivo. Ci sono molti comandi e opzioni. Il comando più elementare è topN; con l'opzione -cum mostra le principali funzioni N che hanno avuto il tempo di esecuzione più cumulativo (quindi una funzione che richiede pochissimo tempo per essere eseguita, ma viene chiamata più volte, può essere in cima). Questo di solito è quello con cui inizio.
> vai tool pprof cpu.prof Tipo: cpu Tempo: 23 ottobre 2017 alle 8:05 am (PDT) Durata: 13.22s, Totale campioni = 13.10s (99.06%) Accesso alla modalità interattiva (digitare "help" per i comandi) (pprof ) top5 -cum Visualizzazione dei nodi corrispondenti a 1.23s, 9.39% di 13.10s totali Dropped 76 nodi (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
Capiamo l'output. Ogni riga rappresenta una funzione. Ho eluito il percorso di ciascuna funzione a causa dei vincoli di spazio, ma verrà mostrato nell'output reale come l'ultima colonna.
Flat indica il tempo (o la percentuale) speso all'interno della funzione e Cum indica cumulativo, il tempo trascorso all'interno della funzione e tutte le funzioni che chiama. In questo caso, testing.tRunner
in realtà chiama TestFindLargestProduct ()
, che chiama FindLargestProduct ()
, ma dal momento che praticamente non viene trascorso lì il tempo, il profiler del campionamento conta il tempo piatto come 0.
La profilazione della memoria è simile, tranne per il fatto che si crea un profilo di memoria:
vai test -memprofile mem.prof -bench. PASSA OK _ / github.com / the-gigi / project-euler / 8 / go / banale
È possibile analizzare l'utilizzo della memoria utilizzando lo stesso strumento.
Vediamo cosa possiamo fare per risolvere il problema più velocemente. Guardando il profilo, lo vediamo calcProduct ()
prende l'8,17% del runtime piatto, ma makeSlice ()
, da cui viene chiamato calcProduct ()
, sta prendendo il 72% (cumulativo perché chiama altre funzioni). Ciò fornisce una buona indicazione di ciò che dobbiamo ottimizzare. Cosa fa il codice? Per ciascuna sequenza di 13 numeri adiacenti, assegna una sezione:
func calcProduct (serie string) int64 digit: = make ([] int64, len (series)) ...
Questo è quasi 1.000 volte per corsa, e corriamo 100.000 volte. Le allocazioni di memoria sono lente. In questo caso, non è davvero necessario allocare una nuova fetta ogni volta. In realtà, non è necessario assegnare alcuna fetta. Possiamo solo scansionare l'array di input.
Il seguente frammento di codice mostra come calcolare il prodotto in esecuzione semplicemente dividendo per la prima cifra della sequenza precedente e moltiplicando per il cane bastardo
cifra.
if cur == 1 currProduct / = old continue if old == 1 currProduct * = cur else currProduct = currProduct / vecchio * cur se currProduct> largestProduct largestProduct = currProduct
Ecco una breve lista di alcune delle ottimizzazioni algoritmiche:
Il programma completo è qui. C'è qualche logica spinosa per aggirare gli zeri, ma a parte questo è piuttosto semplice. La cosa principale è che assegniamo solo una matrice di 1000 byte all'inizio e la passiamo per puntatore (quindi nessuna copia) al findLargestProductInSeries ()
funzione con un intervallo di indici.
pacchetto scan func findLargestProductInSeries (cifre * [1000] byte, start, end int) int64 if (end - start) < 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 > largestProduct largestProduct = currProduct return largestProduct func FindLargestProduct (stringa di testo) int64 var digits [1000] byte digIndex: = 0 per _, c: = range text if c == 10 continue digits [digIndex] = byte (c) - 48 digIndex ++ start: = -1 end: = -1 findStart: = true var largestProduct int64 per 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 > largestProduct largestProduct = result findStart = true return largestProduct
Il test è lo stesso. Vediamo come abbiamo fatto con il profilo:
> vai test -cpuprofile cpu.prof -bench. PASSA OK _ / github.com / the-gigi / project-euler / 8 / go / scan 0.816s
A prima vista, possiamo vedere che il tempo di esecuzione è sceso da più di 13 secondi a meno di un secondo. È piuttosto buono. È tempo di sbirciare dentro. Usiamo solo top10
, che classifica in base al tempo piatto.
(pprof) top10 Visualizzazione dei nodi che corrispondono a 560 ms, 100% di 560ms flat flat% sum% cum cum% 290ms 51.79% 51.79% 290ms 51.79% findLargestProductInSeries 250ms 44.64% 96.43% 540ms 96.43% FindLargestProduct 20ms 3.57% 100% 20ms 3.57% runtime .sleep 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
È grandioso Praticamente tutto il tempo di esecuzione è trascorso all'interno del nostro codice. Nessuna allocazione di memoria. Possiamo approfondire e osservare il livello di istruzione con il comando list:
(pprof) elenco FindLargestProduct Totale: 560ms ROUTINE ======================== scan.FindLargestProduct 250ms 540ms (flat, cum) 96,43% del totale ... 44: ... 45: ... 46: func FindLargestProduct (t string) int64 ... 47: var digit [1000] byte ... 48: digIndex: = 0 70ms 70ms 49: per _, c: = intervallo testo ... 50: se c == 10 ... 51: continua ... 52: ... 53: cifre [digIndex] = byte (c) - 48 10 ms 10 ms 54: digIndex ++ ... 55: ... 56: ... 57: inizio: = -1 ... 58: fine: = -1 ... 59: findStart: = true ... 60: var largestProduct int64 ... 61: for 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 > largestProduct ... 75: largestProduct = result ... 76: ... 77: findStart = true ... 78: ... 79:
Questo è abbastanza sorprendente. Ottieni una dichiarazione con il tempo di dichiarazione di tutti i punti importanti. Si noti che la chiamata sulla linea 73 a funzione f ()
è in realtà una chiamata a findLargestProductInSeries ()
, che ho rinominato nel profilo a causa di limiti di spazio. Questa chiamata richiede 20 ms. Forse, incorporando il codice funzione in posizione, possiamo salvare la chiamata alla funzione (incluso allocare stack e copiare gli argomenti) e salvare quei 20 ms. Potrebbero esserci altre ottimizzazioni utili che questa vista può aiutare a individuare.
Guardare questi profili di testo può essere difficile per i programmi di grandi dimensioni. Go ti offre molte opzioni di visualizzazione. Dovrai installare Graphviz per la prossima sezione.
Lo strumento pprof può generare output in molti formati. Uno dei modi più semplici (svg output) è semplicemente digitare 'web' dal prompt interattivo di pprof, e il tuo browser mostrerà un bel grafico con il percorso caldo segnato in rosa.
I grafici integrati sono belli e utili, ma con programmi di grandi dimensioni, anche questi grafici possono essere difficili da esplorare. Uno degli strumenti più popolari per visualizzare i risultati delle prestazioni è il grafico della fiamma. Lo strumento pprof non lo supporta ancora, ma puoi giocare con i grafici delle fiamme già usando lo strumento per torcia di Uber. È in corso un lavoro per aggiungere il supporto integrato per i grafici di fiamma a pprof.
Go è un linguaggio di programmazione dei sistemi che viene utilizzato per creare sistemi distribuiti e archivi dati ad alte prestazioni. Go viene fornito con un supporto eccellente che continua a migliorare per la profilazione dei programmi, l'analisi delle loro prestazioni e la visualizzazione dei risultati.
C'è molta enfasi da parte del team di Go e della comunità sul miglioramento degli strumenti per le prestazioni. Il codice sorgente completo con tre diversi algoritmi può essere trovato su GitHub.