Gli approcci contro la base di codice diventano uniformemente lenti

11

Stiamo lavorando su una base di codice C ++ di dimensioni moderate (10Mloc) che attraverso i nostri sforzi di ottimizzazione sta diventando uniformemente lento .

Questo codice base è un insieme di librerie che combiniamo per metterle al lavoro. Quando è stato sviluppato il quadro generale di come queste librerie comunicano, c'è stata una certa enfasi sulle prestazioni e in seguito, quando più parti sono state aggiunte, il quadro generale non è stato modificato molto. L'ottimizzazione è stata eseguita quando necessario e man mano che il nostro hardware si è evoluto. Ciò ha reso la decisione iniziale molto costosa solo molto più tardi. Siamo ora in un punto in cui ulteriori ottimizzazioni sono molto più costose in quanto richiederebbero la riscrittura di ampie parti della base di codice. Ci troviamo di fronte a un minimo locale indesiderabile poiché sappiamo che in linea di principio il codice dovrebbe essere in grado di funzionare molto più velocemente.

Esistono metodologie di successo che aiutano a decidere quali cambiamenti prendere l'evoluzione di una base di codice verso una soluzione globalmente performante che non sia facilmente confusa da facili opportunità di ottimizzazione?

Modifica

Per rispondere alla domanda su come stiamo attualmente profilo:

In realtà abbiamo solo 2 diversi scenari su come utilizzare questo codice, sia in modo imbarazzante che parallelo. La profilazione viene eseguita sia con una media temporale dell'orologio a muro su un ampio campione di input e con esecuzioni più dettagliate (costi di istruzione, previsioni errate delle branche e problemi di memorizzazione nella cache). Funziona bene poiché eseguiamo esclusivamente macchine estremamente omogenee (un cluster di un paio di migliaia di macchine identiche). Dato che di solito teniamo tutte le nostre macchine occupate per la maggior parte del tempo a correre più velocemente significa che possiamo guardare altre cose nuove. Il problema è ovviamente che quando si presentano nuove variazioni di input, potrebbero ottenere una penalità in ritardo poiché abbiamo rimosso le più evidenti micro-inefficienze per gli altri casi di utilizzo, riducendo quindi il numero di scenari "in esecuzione ottimale". Le risposte attuali suggeriscono giustamente di separare i dati e gli algoritmi in modo che questo sia più facile da regolare.

    
posta Benjamin Bannier 23.01.2012 - 20:19
fonte

4 risposte

9

Non conosco un approccio generale a questo problema, ma due approcci un po 'correlati hanno funzionato bene per me in passato: per mancanza di termini migliori, li ho chiamati bunching e ottimizzazione orizzontale .

L'approccio al raggruppamento è un tentativo di sostituire un numero elevato di operazioni brevi e veloci con un'unica operazione altamente specializzata, che esegue in modo univoco e che produce lo stesso risultato.

Esempio : dopo aver profilato un'operazione particolarmente lenta del nostro editor di regole visive, non abbiamo scoperto alcun "low hanging fruit": non c'era una singola operazione che richiedesse più del 2% del tempo di esecuzione, eppure l'operazione nel suo complesso si è sentita lenta. Tuttavia, abbiamo scoperto che l'editor stava inviando un gran numero di piccole richieste al server. Anche se l'editor stava elaborando rapidamente le singole risposte, il numero di interazioni richieste / risposte ha avuto un effetto moltiplicativo, quindi il tempo complessivo richiesto dall'operazione è stato di diversi secondi. Dopo aver attentamente catalogato le interazioni dell'editor durante l'operazione di lunga durata, abbiamo aggiunto un nuovo comando all'interfaccia del server. Il comando addizionale era più specializzato, in quanto accettava i dati richiesti per eseguire un sottoinsieme di operazioni brevi, esplorava le dipendenze dei dati per calcolare l'insieme finale di dati da restituire e forniva una risposta contenente le informazioni necessarie per completare tutte le singole operazioni minori in un singolo viaggio al server. Ciò non ha ridotto il tempo di elaborazione nel nostro codice, ma ha ridotto una notevole quantità di latenza a causa della rimozione di più costosi round trip client-server.

L'ottimizzazione orizzontale è una tecnica correlata quando elimini la "lentezza" che è distribuita in modo sottile tra più componenti del tuo sistema utilizzando una particolare funzionalità del tuo ambiente di esecuzione.

Esempio : dopo aver eseguito il profiling di un'operazione di lunga durata, abbiamo scoperto che facciamo molte chiamate attraverso il limite del dominio dell'applicazione (questo è molto specifico per .NET). Non siamo riusciti a eliminare nessuno degli inviti e non siamo stati in grado di raggrupparli insieme: venivano in momenti diversi da sezioni molto diverse del nostro sistema e le informazioni richieste erano dipendenti dai risultati restituiti da richieste precedenti. Ogni chiamata richiedeva la serializzazione e la deserializzazione di una quantità relativamente piccola di dati. Ancora una volta, le chiamate individuali erano di breve durata, ma di numero molto elevato. Abbiamo finito per progettare uno schema che evitava quasi completamente la serializzazione, sostituendolo con il passare di un puntatore attraverso il limite del dominio dell'app. Questa è stata una grande vittoria, perché molte richieste provenienti da classi completamente estranee sono diventate istantaneamente molto più veloci grazie all'applicazione di una singola soluzione orizzontale .

    
risposta data 23.01.2012 - 21:40
fonte
3

This made expensive early decision apparent only much later. We are now at a point where further optimizations are much more expensive since they would require rewriting large parts of the code base.

Quando inizi questa riscrittura devi fare diverse cose in modo diverso.

Per prima. E più importante. Smetti di "ottimizzare". "Ottimizzazione" non importa molto. Come hai visto, solo la riscrittura all'ingrosso è importante.

dunque.

Seconda. Comprendi le implicazioni di ogni struttura di dati e scelta dell'algoritmo.

Terzo. Rendere la scelta effettiva della struttura dei dati e dell'algoritmo una questione di "late binding". Interfacce di progettazione che possono avere una qualsiasi delle varie implementazioni utilizzate dietro l'interfaccia.

Quello che stai facendo ora (riscrittura) dovrebbe essere molto, molto meno doloroso se hai definito un insieme di interfacce che ti consente di apportare modifiche all'ingrosso alla struttura dei dati o all'algoritmo.

    
risposta data 23.01.2012 - 20:55
fonte
3

Un bel trucco pratico è utilizzare la suite di test delle unità come suite di test delle prestazioni .

Il seguente approccio ha funzionato bene nelle mie basi di codice:

  1. Assicurati che la copertura del test dell'unità sia buona (lo hai già fatto, giusto?)
  2. Assicurati che il tuo test esegua il runtime dei rapporti su ogni singolo test . Questo è importante perché vuoi trovare dove sta accadendo una performance lenta.
  3. Se un test è in esecuzione lentamente, utilizzalo come un modo per immergerti e l'ottimizzazione del target in questa area . Ottimizzare prima di identificare un problema di prestazioni potrebbe essere considerato prematuro, quindi la cosa grandiosa di questo approccio è che si ottiene prima una prova concreta di prestazioni scadenti. Se necessario, suddividere il test in test più piccoli che confrontino aspetti diversi in modo da poter identificare dove si trova il problema root.
  4. Se un test viene eseguito in modo estremamente rapido, di solito è buono, anche se si potrebbe quindi considerare di eseguire il test in un ciclo con parametri diversi. Questo rende un test delle prestazioni migliore e aumenta anche la copertura del test dello spazio dei parametri.
  5. Scrivi alcuni test extra che mirano specificamente alle prestazioni, ad es. tempi o tempi di transazione end-to-end per completare 1.000 applicazioni di regole. Se hai specifici requisiti di performance non funzionali (ad es. < 300ms tempo di risposta), allora fallisci il test se impiega troppo tempo.

Se continui a fare tutto questo, nel tempo le prestazioni medie del tuo codice base dovrebbero migliorare in modo organico.

Sarebbe anche possibile tenere traccia dei tempi di test storici e disegnare grafici delle prestazioni e regressioni spot nel tempo in termini di prestazioni medie. Non mi sono mai preoccupato di questo principalmente perché è un po 'complicato assicurarti di confrontarti come con i tuoi stessi cambiamenti e aggiungere nuovi test, ma potrebbe essere un esercizio interessante se le prestazioni sono sufficientemente importanti per te.

    
risposta data 24.01.2012 - 07:29
fonte
1

La risposta di @dasblinkenlight evidenzia un problema molto comune, specialmente con basi di codice di grandi dimensioni (nella mia esperienza). Potrebbero esserci seri problemi di prestazioni, ma sono non localizzati . Se si esegue un profiler, non ci sono routine che richiedono abbastanza tempo da soli per attirare l'attenzione. (Supponendo che tu guardi la percentuale del tempo compreso che include i calle. Non preoccuparti nemmeno del "tempo di auto".)

In effetti, in quel caso, il problema reale non è stato trovato dalla profilazione, ma da una visione fortunata.

Ho un case study, per il quale esiste un breve slide show PDF , illustrando questo problema in dettaglio e come gestirlo. Il punto di base è, dal momento che il codice è molto più lento di quanto potrebbe essere, ciò significa (per definizione) che il tempo in eccesso viene speso facendo qualcosa che potrebbe essere rimosso.

Se dovessi esaminare alcuni campioni a caso dello stato del programma, ti piacerebbe vedere a fare l'attività rimovibile, a causa della percentuale di tempo che impiega. È possibile che l'attività rimovibile non sia limitata a una funzione o a molte funzioni. Non si localizza in questo modo.

Non è un "punto caldo".

È una descrizione che fai di ciò che vedi, che è vero per una grande percentuale di tempo. Ciò lo rende semplice da scoprire, ma se è facile da risolvere dipende da quanto riscrittura richiede.

(C'è una critica spesso fatta con questo approccio, che il numero di campioni è troppo piccolo per la validità statistica.) Questo è risposto alla diapositiva 13 del PDF. In breve - sì, c'è un'alta incertezza nella "misurazione" del potenziale risparmi, ma 1) il valore atteso di quel potenziale risparmio non è sostanzialmente influenzato, e 2) quando il potenziale risparmio di $ x $ viene tradotto in un rapporto di accelerazione di $ 1 / (1-x) $, è strongmente inclinato verso fattori (benefici) elevati.)

    
risposta data 23.01.2012 - 22:30
fonte

Leggi altre domande sui tag