OK, stai definendo il problema in cui sembrerebbe che non ci sia molto margine di miglioramento. Questo è abbastanza raro, nella mia esperienza. Ho provato a spiegarlo in un articolo del Dr. Dobbs nel novembre 1993, partendo da un programma non banale convenzionalmente ben progettato senza sprechi evidenti e sottoponendolo a una serie di ottimizzazioni fino a ridurre il tempo del suo orologio da parete da 48 secondi a 1,1 secondi e la dimensione del codice sorgente è stata ridotta di un fattore 4. Il mio strumento diagnostico era questo . La sequenza dei cambiamenti è stata questa:
Il primo problema riscontrato è stato l'uso di cluster di elenchi (ora chiamati "iteratori" e "classi contenitore") che rappresentano oltre la metà del tempo. Questi sono stati sostituiti con un codice abbastanza semplice, portando il tempo a 20 secondi.
Ora il più grande acquirente è più lista-costruzione. In percentuale, non era così grande prima, ma ora è perché il problema più grande è stato rimosso. Trovo un modo per accelerarlo e il tempo scende a 17 secondi.
Ora è più difficile trovare colpevoli evidenti, ma ce ne sono alcuni più piccoli di cui posso fare qualcosa e il tempo scende a 13 sec.
Ora mi sembra di aver colpito un muro. I campioni mi dicono esattamente cosa sta facendo, ma non riesco a trovare nulla che possa migliorare. Quindi rifletto sulla progettazione di base del programma, sulla sua struttura guidata dalle transazioni e chiedo se tutta la ricerca dell'elenco che sta facendo sia effettivamente richiesta dai requisiti del problema.
Poi mi sono imbattuto in una riprogettazione, in cui il codice del programma è effettivamente generato (tramite macro preprocessore) da un set più piccolo di sorgente e in cui il programma non è costantemente alla ricerca di cose che il programmatore sa che sono abbastanza prevedibili. In altre parole, non "interpretare" la sequenza delle cose da fare, "compilarla".
- La riprogettazione viene eseguita, riducendo il codice sorgente di un fattore 4 e il tempo viene ridotto a 10 secondi.
Ora, poiché sta diventando così veloce, è difficile campionarlo, quindi gli do 10 volte più lavoro da fare, ma i seguenti tempi si basano sul carico di lavoro originale.
Ulteriori diagnosi rivelano che sta impiegando tempo nella gestione delle code. Il rivestimento interno riduce il tempo a 7 secondi.
Ora un grande impegno è la stampa diagnostica che stavo facendo. Lavare quello - 4 secondi.
Ora i più grandi che richiedono tempo sono le chiamate a malloc e gratuite . Ricicli gli oggetti - 2,6 secondi.
Continuando a campionare, trovo ancora operazioni non strettamente necessarie: 1,1 secondi.
Fattore di accelerazione totale: 43,6
Ora non ci sono due programmi uguali, ma nei software non giocattolo ho sempre visto una progressione come questa. Per prima cosa ottieni le cose facili, e poi le più difficili, fino a quando non arrivi a un calo dei rendimenti. Quindi l'intuizione che ottieni potrebbe portare a una riprogettazione, iniziando un nuovo round di accelerazioni, fino a quando non raggiungi nuovamente rendimenti decrescenti. Ora, questo è il punto in cui potrebbe dare un senso a chiedersi se ++i
o i++
o for(;;)
o while(1)
sono più veloci: il tipo di domande che vedo così spesso su Stack Overflow.
PS Ci si potrebbe chiedere perché non ho usato un profiler. La risposta è che quasi ognuno di questi "problemi" era un sito di chiamata di funzione, che impilava i punti di riferimento. I profilatori, anche oggi, stanno appena arrivando all'idea che le dichiarazioni e le istruzioni di chiamata sono più importanti da individuare e più facili da correggere rispetto a intere funzioni.
In realtà ho creato un profiler per farlo, ma per una vera intimità con ciò che il codice sta facendo, non c'è sostituto per avere le dita giuste. Non è un problema il fatto che il numero di campioni sia piccolo, perché nessuno dei problemi rilevati è così piccolo da poter essere facilmente perso.
AGGIUNTO: jerryjvl ha richiesto alcuni esempi. Ecco il primo problema. Consiste in un numero limitato di righe di codice separate, che insieme richiedono più della metà del tempo:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Questi utilizzavano il cluster di elenchi ILST (simile a una classe di elenchi). Sono implementati nel solito modo, con "occultamento delle informazioni" che significa che gli utenti della classe non dovevano preoccuparsi di come sono stati implementati. Quando sono state scritte queste righe (su circa 800 righe di codice) non è stata data l'idea che queste potrebbero essere un "collo di bottiglia" (odio quella parola). Sono semplicemente il modo raccomandato per fare le cose. Con il senno di poi è facile dire che questi avrebbero dovuto essere evitati, ma nella mia esperienza tutti i problemi di prestazione sono così. In generale, è bene cercare di evitare di creare problemi di prestazioni. È anche meglio trovare e correggere quelli che sono stati creati, anche se "avrebbero dovuto essere evitati" (col senno di poi).
Ecco il secondo problema, in due righe separate:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Si tratta di creare elenchi aggiungendo elementi alle loro estremità. (La correzione consisteva nel raccogliere gli elementi negli array e creare gli elenchi tutti in una volta.) La cosa interessante è che queste affermazioni costano solo (cioè erano nello stack di chiamate) 3/48 del tempo originale, quindi non erano in infatti un grosso problema all'inizio . Tuttavia, dopo aver rimosso il primo problema, costano 3/20 delle volte e quindi ora erano un "pesce più grande". In generale, è così che va.
Potrei aggiungere che questo progetto è stato distillato da un vero progetto su cui ho collaborato. In quel progetto, i problemi di prestazioni erano molto più drammatici (come lo erano le accelerazioni), come chiamare una routine di accesso al database all'interno di un ciclo interno per vedere se un'attività era terminata.
RIFERIMENTO AGGIUNTO: Il codice sorgente, sia originale che riprogettato, è disponibile in www.ddj.com , per il 1993, nel file 9311.zip, nei file slug.asc e slug.zip.
EDIT 2011/11/26: ora esiste un progetto SourceForge che contiene il codice sorgente in Visual C ++ e una descrizione dettagliata di come è stato messo a punto. Attraversa solo la prima metà dello scenario sopra descritto e non segue esattamente la stessa sequenza, ma ottiene comunque un accelerazione di grandezza di 2-3 ordini di grandezza.