Quali sono alcune opportunità di ottimizzazione degli algoritmi comuni, matematiche o di altro tipo

6

Quali sono alcune comuni opportunità di ottimizzazione algoritmica di cui tutti dovrebbero essere a conoscenza? Recentemente ho revisionato / revisionato del codice da un'applicazione, e ho notato che sembrava funzionare molto più lentamente di quanto avrebbe potuto. Il seguente loop si è rivelato essere il colpevole,

...
    float s1 = 0.0;
    for (int j = 0; j < size; ++j) {
        float diff = a[j] - b[j]; 
        s1 += (diff*diff * c[j]) + log(1.0/c[j]);
    }
...

Questo è equivalente a,

Σ j {(a j -b j ) 2 * c j + log (1 / c j )}

Ogni volta che si esegue il programma, questo ciclo viene chiamato forse più di 100k volte, quindi le chiamate ripetute per registrare e dividere risultano in un risultato di prestazioni molto grande. Un rapido sguardo alla rappresentazione del sigma rende abbastanza chiaro che esiste una soluzione banale, assumendo che tu ricordi le tue identità di logaritmo abbastanza bene da individuarlo,

Σ j {(a j -b j ) 2 * c j + log (1 / c j )} =

Σ j {(a j -b j ) 2 * c j } + Σ j {log (1.0 / c j )} =

Σ j {(a j -b j ) 2 * c j } + log (1.0 / (Π j c j ))

e porta a uno snippet molto più efficiente,

...
    float s1 = 0.0;
    float s2 = 1.0;
    for (int j = 0; j < size; ++j) {
        float diff = a[j] - b[j]; 
        s2 *= c[j];
        s1 += (diff*diff * c[j]);
    }
    s1 += log(1.0/s2);
...

questo ha comportato un notevole aumento della velocità e dovrebbe essere entrato nell'implementazione originale. Presumo che non fosse perché gli sviluppatori originali non erano a conoscenza, o non erano 'attivamente consapevoli' di questo semplice miglioramento.

Questo mi ha fatto chiedere, quali altre, simili, comuni opportunità e I perdere o trascurare, e come posso imparare a individuarle meglio? Non sono tanto interessato a casi limite complessi per particolari algoritmi, ma piuttosto esempi come quello sopra che coinvolgono quelli che potreste pensare come concetti "ovvi" che emergono frequentemente, ma che altri potrebbero non farlo.

    
posta xhs7is82wl 09.07.2011 - 04:20
fonte

4 risposte

4

I miei 2 centesimi:

  1. Se possibile, cambiare la struttura dei dati del programma potrebbe essere molto utile, anche se la modifica era banale. Una volta ho modificato la presentazione di una matrice sparsa dalla tabella di adiacenza a una tipica rappresentazione a matrice sparsa e il tempo di esecuzione medio dimezzato per il mio programma.

  2. Sbarazzati della ricorsione . Questo è difficile da fare ma potrebbe essere utile. Tuttavia, se fatto in modo non corretto, ciò potrebbe portare a seri problemi e il codice non ricorsivo in genere non è così intuitivo come la versione ricorsiva.

  3. Cache alcuni dei valori usati di frequente. Anche se questo sembra un imbroglio, potrebbe essere molto utile - tutti i programmatori di contest dovrebbero già saperlo. Vedi anche memorizzazione , menzionata nel commento di James Black.

  4. Utilizza la valutazione del collegamento correttamente. Questo non porterà a un incremento delle prestazioni in genere e può portare a codice illeggibile. Ma se l'espressione che viene valutata ha un lavoro molto pesante da fare, questo può aiutare un bel po '.

  5. EDIT: se il tuo lavoro è di tipo computazionale e comporta un calcolo a virgola mobile (soprattutto quando implica approssimazione), a volte riduci la formula (NON ridisegnare l'algoritmo, basta cambiare la formula ad uno equivalente) potrebbe velocizzare notevolmente il tuo programma a causa della aritmetica in virgola mobile utilizzata dai computer. Molti esempi potrebbero essere trovati in analisi numerica e libri di calcolo scientifico. Per i più interessati, Cosa dovrebbe sapere ogni scienziato informatico sull'aritmetica virgola mobile è un ottimo documento.

risposta data 09.07.2011 - 04:31
fonte
3

L'implementazione può imbrogliare, ma non deve essere catturato

Dopo aver profilato un'applicazione per determinare dove viene speso il tempo, il passo successivo è determinare esattamente ciò che il resto del programma si aspetta dal codice problema. Poiché i loop interni sono spesso il colpevole, la quantità di codice è spesso piccola, ma la differenza tra il lavoro importante e quello estraneo può essere molto sottile. Una volta che sai a cosa si affidano i chiamanti, puoi trovare nuovi modi per produrre lo stesso risultato.

Ad esempio, la tua esecuzione di profilazione diventa strlen() all'inizio dell'elenco. Viene chiamato molte, molte volte. Esaminate strlen() e vedete che trova la lunghezza della stringa contando tutti i byte. Ora, quale parte di questo è importante? Come possiamo imbrogliare? Il chiamante si preoccupa davvero di toccare ogni byte? Probabilmente no. Ci interessa anche se dereferenziamo qualsiasi parte della memoria delle stringhe? Forse no. Forse possiamo memoizzare i risultati. Ci prenderemo? Ora devi assicurarti che i risultati memorizzati nella cache siano invalidati se la stringa cambia. In quale altro modo possiamo imbrogliare? Se esamini i chiamanti potresti scoprire che stanno facendo strlen(s) > 10 . Ora smetti di contare alle 11 e farai meno lavoro e non farti prendere.

L'esempio nella domanda è sottile, come altri hanno sottolineato. Imbrogliare tirando fuori un'operazione matematica da un ciclo. Verrai catturato? È meglio riflettere sulle problematiche di precisione coinvolte e su come i valori intermedi in virgola mobile influenzeranno i risultati.

In un esempio del mondo reale ho scoperto che l'avvio di un database con mirroring era molto lento. Il codice garantiva l'integrità del database selezionando una delle copie, assicurandosi che fosse la più recente, e quindi caricando tutte le copie N rimanenti per confrontarle con la prima. Questo non poteva essere prontamente parallelizzato perché non c'era abbastanza memoria per leggere tutte le copie N contemporaneamente. Come possiamo imbrogliare? Bene, qual è il vero obiettivo? Il codice in realtà non interessa affatto la lettura e il confronto. Ciò che importa è che ogni copia sia uguale a quella scelta. E se invece di leggere tutte le altre copie, invece, li sovrascriviamo tutti con la nota buona copia? Ora possiamo fare un sacco di IO paralleli e l'operazione va molto più veloce. Come possiamo essere catturati? Bene, la nostra scrittura può essere interrotta a metà. O solo alcune delle nostre scritture possono essere interrotte. Trattare con questi casi angusti era il grosso del lavoro. Tuttavia, la scrittura rapida e parallela valeva la pena.

    
risposta data 09.07.2011 - 05:21
fonte
1

Opportunità di ottimizzazione algoritmiche. Ecco come le penso in generale:

La complessità dell'algoritmo O (NlogN) o inferiore? Se è così, è probabilmente abbastanza buono.

In caso contrario, inizio a cercare altri algoritmi.

In definitiva con set di dati molto grandi, la modifica della costante proporzionale non fa molto (che è essenzialmente ciò che fa l'esempio pubblicato). Solo cambiando la complessità dell'algoritmo si otterranno accelerazioni nel caso asintotico.

Se hai set di dati di dimensioni fisse, allora forse i miglioramenti valgono la pena.

Oh quasi dimenticato !: Assicurati di misurare prima di ottimizzare.

    
risposta data 09.07.2011 - 04:38
fonte
1

C'è una quantità infinita di equivalenze algebriche, per tutte le varie algebre con le quali si potrebbe calcolare. Non penso che tu possa scrivere un elenco esteso specifico utile.

Allo stesso modo ci sono molte equivalenze algoritmiche. Questi sono generalmente utili, poiché possono influenzare il tempo di calcolo in modi strongmente non lineari.

Scoprirai anche che ci sono una varietà di ottimizzazioni motivate dalle strutture hardware informatiche, come le somme invece di riduzioni lineari, e la cache dei risultati di calcolo riutilizzabili quando hai un sacco di cache.

È meglio essere consapevoli che tali equivalenze esistono (modulo possibili variazioni di precisione e richieste di risorse diverse), durante la codifica e quando si scopre dove sono realmente i colli di bottiglia del codice.

    
risposta data 09.07.2011 - 04:34
fonte

Leggi altre domande sui tag