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 lenta .

Questa base di codice è un insieme di librerie che combiniamo per metterle al lavoro. Quando è stato sviluppato il quadro generale di come comunicano queste biblioteche, c'è stata una certa enfasi sulle prestazioni e in seguito, quando sono state aggiunte più parti, il quadro generale non è stato cambiato molto. L'ottimizzazione è stata effettuata quando necessario e con l'evolversi del nostro hardware. Ciò rese evidente la costosa decisione iniziale solo molto più tardi. Siamo ora a un punto in cui ulteriori ottimizzazioni sono molto più costose poiché richiederebbero riscrivere gran parte della base di codice. Ci troviamo ad avvicinarci 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 sono le conseguenze per l'evoluzione di una base di codice verso una soluzione globalmente ottimale che non sia facilmente confusa da facili opportunità di ottimizzazione?

MODIFICARE

Per rispondere alla domanda su come stiamo attualmente profilando:

Abbiamo davvero solo 2 diversi scenari su come utilizzare questo codice, entrambi imbarazzanti parallelamente. La profilatura viene eseguita sia con il tempo dell'orologio a parete mediato su un ampio campione di input sia su esecuzioni più dettagliate (costi di istruzione, previsioni errate delle filiali e problemi di memorizzazione nella cache). Funziona bene poiché giriamo esclusivamente su macchine estremamente omogenee (un gruppo di un paio di migliaia di macchine identiche). Dal momento che di solito teniamo occupate tutte le nostre macchine per la maggior parte del tempo a correre più velocemente significa che possiamo esaminare ulteriori novità. Il problema è ovviamente che quando si presentano nuove variazioni di input, potrebbero ricevere una penalità in ritardo poiché abbiamo rimosso le micro-inefficienze più ovvie per gli altri casi d'uso, riducendo in tal modo il numero di scenari "in esecuzione ottimale".


10
10Mloc è in realtà un grande progetto
BЈовић

1
Sono 10 milioni di loc (prefisso SI) contati da sloc. L'ho chiamato "moderatamente dimensionato" perché non ho idea di cosa sia considerato "grande" qui.
Benjamin Bannier,

5
abbastanza sicuro 10 milioni sono almeno grandi ovunque e probabilmente enormi posti.
Ryathal,

1
Fantastico, grazie @honk Per 10M LOC sembra che tu stia ottimizzando a un livello molto basso, quasi a livello hardware? Il tradizionale OOP ("array di strutture" AOS) è orribilmente inefficiente nelle cache, hai provato a riorganizzare le tue classi in SOA (struttura di array) in modo che i punti di dati su cui sta lavorando il tuo codice siano coerenti nella memoria? Con così tante macchine ti imbatti in blocchi di comunicazione o sincronizzazione consumando tempo? Domanda finale, hai a che fare con volumi elevati di dati in streaming o questo è principalmente un problema di operazioni complesse sui tuoi set di dati?
Patrick Hughes,

1
Quando hai così tanto codice, le probabilità vanno dall'eccellente alla scommessa che ci sono grandi potenziali accelerazioni del tipo non locale che ho citato. Non fa differenza se ci sono migliaia di thread / processi. Qualche pausa casuale li colpirà per te o mi dimostrerà che mi sbaglio.
Mike Dunlavey,

Risposte:


9

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

L'approccio di raggruppamento è un tentativo di sostituire un gran numero di operazioni brevi e veloci con una singola operazione più lenta e altamente specializzata che alla fine produce lo stesso risultato.

Esempio: Dopo aver profilato un'operazione particolarmente lenta del nostro editor di regole visive non abbiamo scoperto "frutti bassi": non c'era una singola operazione che richiedesse più del 2% del tempo di esecuzione, eppure l'operazione nel suo complesso sembrava 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 richiesta / risposta ha avuto un effetto moltiplicativo, quindi il tempo complessivo dell'operazione è stato di diversi secondi. Dopo aver accuratamente catalogato le interazioni dell'editor durante quell'operazione di lunga durata, abbiamo aggiunto un nuovo comando all'interfaccia del server. Il comando aggiuntivo era più specializzato, poiché accettava i dati richiesti per eseguire un sottoinsieme di operazioni brevi, ha esplorato le dipendenze dei dati per capire la serie finale di dati da restituire e ha fornito una risposta contenente le informazioni necessarie per completare tutte le singole piccole operazioni in un unico viaggio verso il server. Ciò non ha ridotto i tempi di elaborazione nel nostro codice, ma ha ridotto una notevole latenza a causa della rimozione di più costosi round-client client-server.

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

Esempio: Dopo aver profilato un'operazione di lunga durata, abbiamo scoperto che effettuiamo molte chiamate oltre il limite del dominio dell'applicazione (questo è altamente specifico di .NET). Non siamo riusciti a eliminare nessuna delle chiamate e non siamo riusciti a raggrupparli insieme: venivano in momenti diversi da sezioni ampiamente diverse del nostro sistema e le cose che richiedevano dipendevano 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 singole chiamate erano di breve durata, ma di numero molto elevato. Abbiamo finito per progettare uno schema che ha evitato quasi completamente la serializzazione, sostituendola con il passaggio di un puntatore attraverso il confine del dominio dell'app. Questa è stata una grande vittoria, perché molte richieste da classi completamente non correlate sono diventate immediatamente molto più veloci a seguito dell'applicazione di un singolosoluzione orizzontale .


Grazie per aver condiviso la tua esperienza, queste sono utili ottimizzazioni da tenere a mente. Inoltre, poiché sollevano parti problematiche in un luogo distinto, sarà molto meglio controllarle in futuro. In un certo senso hanno messo in atto ciò che sarebbe dovuto accadere in primo luogo, ora solo in seguito a dati concreti.
Benjamin Bannier,

3

Ciò rese evidente la costosa decisione iniziale solo molto più tardi. Siamo ora a un punto in cui ulteriori ottimizzazioni sono molto più costose poiché richiederebbero riscrivere gran parte della base di codice.

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

Primo. E soprattutto. Smetti di "ottimizzare". L '"ottimizzazione" non ha molta importanza. Come hai visto, solo la riscrittura all'ingrosso conta.

Perciò.

Secondo. 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". Progettare interfacce che possono avere una delle diverse implementazioni utilizzate dietro l'interfaccia.

Quello che stai facendo ora (riscrittura) dovrebbe essere molto, molto meno doloroso se hai definito una serie di interfacce che ti consentono di apportare una modifica all'ingrosso alla struttura o all'algoritmo dei dati.


1
Grazie per la tua risposta. Mentre dobbiamo ancora ottimizzare (che rientra nei punti 1. e 2. per me) Mi piace molto il pensiero organizzato dietro 3. Rendendo la struttura dei dati, l'algoritmo e l'accesso definiti in ritardo ed espliciti, si dovrebbe essere in grado di gestire molti problemi che stiamo affrontando. Grazie per averlo inserito in un linguaggio coerente.
Benjamin Bannier,

In realtà non è necessario ottimizzare. Una volta che hai la struttura dei dati corretta, l'ottimizzazione verrà mostrata come uno spreco di sforzi. La profilazione mostrerà dove hai una struttura dei dati sbagliata e un algoritmo sbagliato. Giocare con la differenza di prestazioni tra ++e +=1sarà irrilevante e quasi non misurabile. È la cosa che devi durare .
S. Lott,

1
Non tutti gli algoritmi dannosi possono essere trovati con un ragionamento puro. Di tanto in tanto bisogna sedersi e profilarsi. Questo è l'unico modo per scoprire se l'ipotesi iniziale era corretta. Questo è l'unico modo per stimare il costo reale (BigO + const).
Benjamin Bannier,

La profilatura rivelerà algoritmi errati. Totalmente corretto. Non è ancora "ottimizzazione". Questa è ancora la correzione di un difetto di progettazione fondamentale che ho apportato una modifica di progettazione. L'ottimizzazione (messa a punto, messa a punto, ecc.) Raramente sarà visibile alla profilazione.
S.Lott

3

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

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

  1. Assicurati che la copertura del test unitario sia buona (lo hai già fatto, giusto?)
  2. Accertarsi che il test che esegue framework segnala il runtime su ogni singolo test . Questo è importante perché vuoi scoprire dove si stanno verificando prestazioni lente.
  3. Se un test viene eseguito lentamente, utilizzare questo come un modo per immergersi e ottimizzare l'ottimizzazione in questa area . L'ottimizzazione prima di identificare un problema di prestazioni potrebbe essere considerata prematura, quindi la cosa grandiosa di questo approccio è che prima ottieni prove concrete di scarse prestazioni. Se necessario, suddividere il test in test più piccoli che confrontano aspetti diversi in modo da poter identificare dove si trova il problema alla radice.
  4. Se un test viene eseguito molto velocemente, di solito va bene, anche se potresti quindi considerare di eseguire il test in un ciclo con parametri diversi. Questo lo 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 esempio tempi di transazione end-to-end o tempo per completare 1.000 applicazioni di regole. Se si hanno specifici requisiti prestazionali non funzionali (ad es. <300 ms tempo di risposta), fare fallire il test se impiega troppo tempo.

Se continui a fare tutto ciò, nel tempo le prestazioni medie della tua base di codice dovrebbero migliorare organicamente.

Sarebbe anche possibile tenere traccia dei tempi storici dei test e tracciare grafici delle prestazioni e individuare regressioni nel tempo nelle prestazioni medie. Non mi sono mai preso la briga di farlo principalmente perché è un po 'complicato assicurarti di fare un paragone con un simile mentre cambi e aggiungi nuovi test, ma potrebbe essere un esercizio interessante se le prestazioni sono sufficientemente importanti per te.


mi piace la tecnica - intelligente - comunque, questa è micro ottimizzazione e migliorerà al minimo locale - non risolverà i problemi architettonici che ti permetteranno di raggiungere i minimi globali
jasonk

@jasonk - hai assolutamente ragione. Anche se aggiungerei che a volte può darti le prove necessarie per dimostrare perché un particolare cambiamento architettonico sia giustificato .....
Mikera,

1

La risposta di @dasblinkenlight evidenzia un problema molto comune, specialmente con basi di codice di grandi dimensioni (secondo la mia esperienza). Potrebbero esserci seri problemi di prestazioni, ma non sono localizzati . Se gestisci un profiler, non ci sono routine che impiegano abbastanza tempo, da sole, per attirare l'attenzione. (Supponendo che tu guardi la percentuale di tempo inclusiva che include i callees. Non preoccuparti nemmeno di "auto-tempo".)

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

Ho un case study, per il quale c'è una breve presentazione in PDF , che illustra in dettaglio questo problema e come gestirlo. Il punto fondamentale è che, poiché 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 guardare alcuni campioni a tempo casuale dello stato del programma, vedresti che sta facendo l'attività rimovibile, a causa della percentuale di tempo impiegata. È possibile che l'attività rimovibile non sia limitata a una funzione o anche a molte funzioni. Non si localizza in quel modo.

Non è un "hot spot".

È una descrizione che fai di ciò che vedi, che sembra essere vero una grande percentuale di volte. Questo lo rende semplice da scoprire, ma se è facile da risolvere dipende da quanta riscrittura richiede.

(Esiste spesso una critica a questo approccio, secondo cui il numero di campioni è troppo piccolo per la validità statistica. Risposte sulla diapositiva 13 del PDF. In breve: sì, c'è un'alta incertezza nella "misurazione" dei potenziali risparmi, ma 1) il valore atteso di quel potenziale risparmio è sostanzialmente inalterato, e 2) quando il potenziale risparmio $ x $ viene tradotto nel rapporto di accelerazione di $ 1 / (1-x) $, è fortemente inclinato verso fattori (benefici) elevati.)


Grazie per la tua risposta. Non crediamo nel campionamento statistico e utilizziamo la strumentazione con valgrind. Questo ci dà buone stime sia del costo "auto" che "inclusivo" della maggior parte delle cose che facciamo.
Benjamin Bannier,

@honk: giusto. Ma purtroppo, la strumentazione porta ancora l'idea che i problemi di prestazioni siano localizzati e quindi possono essere trovati misurando la frazione del tempo trascorso in routine, ecc. Puoi certamente eseguire valgrind ecc., Ma se vuoi una visione reale delle prestazioni controlla quella presentazione .
Mike Dunlavey,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.