Analisi prestazionale di alcuni algoritmi di ordinamento Laboratorio di algoritmi e strutture dati – Università di Udine Cicuttin Matteo Introduzione Descrizione dell’esperimento Questa analisi si propone di verificare i tempi di esecuzione di alcuni diffusi algoritmi di ordinamento. Come noto, questi algoritmi richiedono un numero di operazioni non linearmente proporzionale al numero di elementi in ingresso per portare a termine il loro compito. Se con n intendiamo il numero di elementi da ordinare, algoritmi di sicuro interesse sono quelli che richiedono un numero di operazioni pari ad O(nlogn) o ad O(n2). La scelta di un particolare algoritmo non è però soltanto influenzata dal suo tempo di esecuzione: anche la richiesta di memoria per svolgere il lavoro può essere determinante. Immaginiamo di dover eseguire un ordinamento di alcuni dati su un microcontrollore; prendiamo come esempio il diffusissimo PIC16F84 prodotto dalla Microchip (è una MCU RISC ad 8 bit). Tale MCU dispone di 68 byte di memoria RAM: sarebbe decisamente una cattiva scelta utilizzare Merge Sort su un dispositivo del genere; è sicuramente più opportuno utilizzare ad esempio Insertion Sort, che come vedremo, è più lento, ma non richiede memoria aggiuntiva per ordinare i dati. Dall’altro lato, nel caso i dati da ordinare fossero i risultati di una ricerca di file sul disco di un attuale elaboratore, sarebbe sensato pensare a Merge Sort per ottenere una buona velocità nelle operazioni e dare una sensazione di prontezza dell’interfaccia utente. Tale osservazione divide gli algoritmi di ordinamento in algoritmi “in place” e “non in place”: si definisce “in place” un algoritmo che richiede una quantità di memoria pari ad O(1) per eseguire il proprio lavoro. Gli algoritmi “non in place” sono quelli per cui non è valida l’affermazione precedente. Analisi prestazionale di alcuni algoritmi di ordinamento Le prove sono state condotte in ambiente UNIX. Il sistema operativo utilizzato è FreeBSD 6.1-STABLE. L’hardware utilizzato è un personal computer basato su 2 CPU CISC a 32 bit Athlon MP 2200+, funzionanti a 1800 MHz ciascuna. La quantità di memoria RAM disponibile è pari ad 1 GB. I singoli algoritmi tuttavia non beneficiano in alcun modo della presenza di 2 CPU. Ogni algoritmo viene testato su un certo numero di vettori di diverse lunghezze, e per ognuno di essi l’esperimento prevede le seguenti fasi: - generazione di un vettore di prova - misura del tempo di copia di tale vettore - misura del tempo di copia e di ordinamento del medesimo vettore - calcolo della differenza tra il tempo di ordinamento e il tempo di copia Ognuno dei seguenti passi viene ripetuto un dato numero di volte per ogni lunghezza dell’input e per ogni dato algoritmo, per poi ricavare media, varianza e intervallo di confidenza dei tempi. Perchè eseguire molte ripetizioni? La metrologia insegna che non è possibile eseguire una misura esatta, infatti, intervengono, durante le misurazioni, errori di varia natura, che non è possibile trascurare, ne, a volte, eliminare. Esempi di tali errori possono essere la limitata precisione dell’orologio della macchina oppure tempi di esecuzione diversi dovuti a diverse situazioni di carico dell’elaboratore. Tuttavia, per limitare l’influenza dell’errore introdotto dal carico istantaneo della CPU, è possibile eseguire con nice pari a -20 il codice proposto, 1 operazione che però richiede privilegi di root. Occorre pertanto eseguire molteplici misure, e da esse, tramite opportune tecniche, dedurre l’errore commesso e l’intervallo di valori in cui il risultato è attendibile. Non solo. Oltre ad eseguire molte ripetizioni della misura sullo stesso campione, è necessario che le misure siano eseguite su campioni diversi. Questo per stimare il caso medio: supponiamo di eseguire le misure su un solo campione, se esso facesse € parte degli input per cui l’algoritmo è pessimo, otterremmo un valore non corrispondente alla realtà. Per questo motivo si misurano più campioni, in modo da valutare il comportamento medio. Riassumendo: Sia n il numero di campioni su cui eseguire le misure e sia i il campione su cui si sta lavorando: while ( i < n ) Si genera un campione, e si esegue su di esso un certo numero di misure; il tempo medio di esecuzione sul campione sarà dato dal tempo totale diviso il numero di ripetizioni della misura end while Il tempo medio di esecuzione del dato algoritmo è la media dei tempi misurati sui vari campioni presi in considerazione all’interno del costrutto while Oltre alla media dei tempi vengono calcolati anche la varianza e l’intervallo di confidenza. Mentre i primi due concetti sono abbastanza noti, il concetto di intervallo di confidenza potrebbe essere più oscuro. Un esempio molto semplice che spiega la cosa è tratto da wikipedia: “Se un marziano ci chiedesse quanto sono alti mediamente gli esseri umani, e noi rispondessimo: ‘mediamente 155cm’, egli potrebbe immaginare esseri umani alti 20cm ed altri alti 3 metri!” Occorre per cui specificare, oltre alla media, anche un intervallo nel quale la misura è atten- Analisi prestazionale di alcuni algoritmi di ordinamento dibile. Questo intervallo lo si ottiene dalla varianza nel seguente modo: Δ= 1 α ⋅ z(1− ) ⋅ s(n) 2 cn dove c n è il numero di campioni, ed s(n) è la deviazione standard o scarto quadratico medio. Una € più formale definizione di intervallo di confidenza è la seguente: Siano U, V variabili casuali che dipendono da qualche parametro θ e sia Pr(U< θ < V) = β, allora l’intervallo casuale (U, V) è un intervallo di confidenza al (100-β)% per θ. In altre parole, è la probabilità che la misura ricada all’interno dell’intervallo (U, V). Al fine di ottenere una confidenza del 95% si è fissato α z(1− ) = 1,96 2 Per ogni esperimento eseguito, viene presentata una tabella contenente i tempi medi di esecu€ zione dell’algoritmo in prova, la loro varianza e relativo intervallo di confidenza, un grafico ed una funzione f:campioni[numero]→tempo [ms] che approssima (grossolanamente) l’andamento dei tempi. Il generatore di numeri casuali Per ottenere i campioni di cui abbiamo bisogno, è necessario utilizzare un generatore di numeri casuali. Esso però è un algoritmo, sinonimo di determinismo. Come fare allora ad ottenere un campione casuale? È impossibile: per questo motivo dobbiamo cercare un algoritmo che simuli una distribuzione casuale il più fedelmente possibile. Per lo scopo si è utilizzato il PRNG assegnato, che utilizza una variante dell’algoritmo di Park & Miller, che tuttavia si rivela di poco migliore rispetto a quello fornito dalla libc. Di seguito le valutazioni eseguite con il tool ent, su campioni da 10 MB. 2 rand() libc Generatore assegnato /dev/random Entropia 7.994335 bit/byte 7.994336 bit/byte 7.999983 bit/byte Chi quadro 41385.09, 0.01% 41371.66, 0.01% 253.16, 50.00% 126.9865 127.0074 127.5456 Media Approssimazione Monte Carlo di π Correlazione seriale 3.168252246, errore 3.165205828, errore 3.140845925, errore 0.85% 0.75% 0.02% -0.000150 Il significato dei parametri è di seguito brevemente spiegato. Entropia: Indica la “densità di informazione” presente nella sequenza di dati considerata. Maggior valore indica maggiore casualità. Chi quadro: Si tratta di un test di significatività che permette di stabilire in modo semplice ed accurato se esiste correlazione tra più eventi. È comunemente utilizzato per valutare i generatori di numeri casuali. < 1% = Non casuale 1% - 5% = Sequenza sospetta 5% - 10% = Sequenza quasi sospetta 10%-90% = Sequenza casuale 90% - 95% = Sequenza quasi sospetta 95% - 99% = Sequenza sospetta > 99% = Non casuale Media: È la media matematica tra tutti i byte dell’input. Più si avvicina a 127.5 più l’input è realmente casuale. Approssimazione Monte Carlo di π: Sia P(x,y) un generico punto, e siano x ed y arbitrari tali che 0 < x < 1 e 0 < y < 1. Se x2 + y2 < 1 allora P(x,y) appartiene alla circonferenza centrata in (0,0) di raggio 1. Generando casualmente un alto numero di punti la probabilità che essi cadano all’interno della circonferenza è π/4. Correlazione seriale: Misura della correlazione presente tra un byte ed il successivo. 0.0 = nessuna correlazione. Analisi prestazionale di alcuni algoritmi di ordinamento 0.000041 0.000900 Misurazione dei tempi Per misurare i tempi di esecuzione in modo affidabile è necessario adottare alcuni trucchi. Il metodo utilizzato dall’esperimento è il seguente: una volta generato il vettore campione, viene copiato tante volte quante bastano perché il tempo di esecuzione sia superiore ad una certa quantità fissata e viene riportato tale numero di ripetizioni. Analogamente, si copia e si ordina il vettore per tante volte quante bastano per accumulare almeno il doppio del tempo del passo precedente. Successivamente si esegue il numero di copie e il numero di copie più ordinamenti calcolati, misurandone contemporaneamente il tempo di esecuzione. Una volta diviso per il numero di ripetizioni si ha il tempo medio di esecuzione sul dato campione. Il test viene ripetuto su un certo numero di diversi input. Il tempo di esecuzione sul campione è dato da: tr = Riplorde Riptara − Tempolordo Tempotara dove Tempolordo è il tempo comprensivo delle copie, mentre Tempotara è il solo tempo delle copie. Vale l’analogo per le ripetizioni. € Come capire quanto tempo è passato? UNIX mette a disposizione diversi timer; semplicemente basta leggerne uno prima e dopo l’esecuzione del test dell’algoritmo e riportarne la differenza. 3 La funzione di libreria clock() potrebbe essere un buon candidato. clock() riporta il tempo di CPU in user-mode utilizzato dal processo. Per cui, supponiamo di prendere in esame il frammento di codice allegato come clock1.c: il suo scopo è di installare un gestore per il segnale SIGALRM, leggere il timer, aspettare SECS secondi, leggere il timer, entrare in un ciclo infinito e attendere il segnale SIGALRM, per poi riportare per la terza volta la lettura del timer. Il suo output è il seguente: da leggere è CLOCK_VIRTUAL, come esemplificato nel programma clock2.c. Linux, pur mettendo a disposizione le funzioni sopra indicate (il programma che le usa va linkato contro librt), non è pienamente aderente allo standard. Per questo motivo, nel codice relativo all’esperimento viene data la possibilità di scegliere quale timer usare, semplicemente definendo o meno USE_POSIX_TIMERS in plib.h. Le prove sono state condotte utilizzando i timer POSIX. % ./clock1 0 0 1279 % Gli algoritmi in prova sono stati 4, varianti escluse. Essi, riassuntivamente sono: sleep() chiede al sistema di sospendere il processo che la chiama per il numero di secondi che viene passato come argomento, per cui per tale intervallo al chiamante non viene assegnata CPU. E’ lecito per cui attendere che la seconda chiamata a clock() ritorni zero. Nell’intervallo tra la seconda e la terza chiamata invece il processo è in un loop infinito per cui stà utilizzando la cpu. Nel momento il cui arriva il segnale, in questo caso viene stampato il numero di “clocks” di CPU utilizzati. clock() è per cui un indicatore dell’effettivo tempo di CPU utilizzato dal processo. In FreeBSD ci sono 128 “clocks” per secondo, e il minimo scarto rilevabile è di 1 “clock”, cioè 1/128 di secondo. In Linux ci sono 1.000.000 “clocks” per secondo e il minimo scarto rilevabile è di 10.000 “clocks” ovvero 1/100 di secondo. Tuttavia, lo standard POSIX.4 mette a disposizione clock_gettime(), che in FreeBSD è in grado di risolvere 1/HZ secondi come riportato da clock_getres(). HZ è una costante presente in qualsiasi kernel UNIX che definisce quale sia la frequenza degli interrupt dell’orologio. Per ottenere la stessa informazione di clock(), soltanto in modo più accurato il timer Analisi prestazionale di alcuni algoritmi di ordinamento Algoritmi provati Algoritmo Complessità In place Bubble Sort O(n2) Si Insertion Sort O(n2) Si Merge Sort O(nlogn) No Quick Sort O(nlogn) No (stack) Per tutti gli algoritmi a parte Merge Sort sono state introdotte delle varianti al fine di cercare di ridurre i tempi di esecuzione. Ogni algoritmo è stato testato per 500 campioni con Tmin pari a 100 ms. Il codice sorgente è stato compilato con il massimo livello di ottimizzazione (-O3). 4 Bubble Sort BubbleSort(A) for j = length(A) downto 2 do for i = 2 to j do if A[i-1] < A[i] then swap(A[i-1], A[i]) end if end for end for Bubble Sort è un algoritmo di ordinamento semplice, ma molto inefficiente. Il suo funzionamento si basa sullo scorrere il vettore di input n volte. Nel caso vengano incontrati due elementi tali che ai > ai+1 essi vengono scambiati. In n passi il vettore è ordinato: supponiamo che il minimo sia in ultima posizione; ad ogni scansione un elemento si sposta di al più una posizione, per cui all’ultimo elemento serviranno n passi per trovarsi in posizione corretta. Vediamo i dati relativi all’esecuzione: Dimensione Tempo medio(ms) Varianza Δ 10 0.000201624 2.25467e-10 2.08105e-06 20 0.000886864 2.6825e-09 7.17813e-06 50 0.00527096 4.49827e-08 2.93943e-05 100 0.0215636 5.23159e-07 0.000100244 200 0.0895205 6.35554e-06 0.000349396 500 0.569271 8.46188e-05 0.0012749 1000 2.25646 0.00071653 0.00370987 2000 8.93493 0.00584215 0.0105932 5000 55.2732 0.0783233 0.038787 Tempi di esecuzione di Bubble Sort [fbs(n)≈2.25E-6n2] Analisi prestazionale di alcuni algoritmi di ordinamento 5 Tempi di esecuzione di Bubblesort 0.1 0.08 50 0.06 0.04 Tempo (secondi) 40 0.02 30 0 0 50 100 150 200 20 10 Tempi misurati Funzione tempo stimata 0 0 1000 2000 3000 4000 5000 Numero di elementi Come si può vedere, i tempi di ordinamento crescono quadraticamente al raddoppiare della dimensione del problema. Una prima ottimizzazione, ovvia, a cui si potrebbe pensare è la seguente: nel caso si scopra che non sono stati effettuati scambi allora il vettore è ordinato e si può uscire. Subito si nota che nel caso migliore questa variante richiede O(n) operazioni, a differenza della versione standard il cui caso migliore è O(n2). Il caso pessimo tuttavia richiede ancora O(n2). Una seconda variante deriva dall’osservazione che ad ogni iterazione del ciclo esterno un determinato elemento viene spostato nella sua corretta posizione: se ad una data iterazione i non vengono eseguiti scambi in posizione successiva ad n, significa che a partire dalla posizione n+1-esima in poi i numeri sono già in ordine e si può essere certi che in questa zona non avverranno ulteriori scambi nelle iterazioni che seguono ad i, per cui si può smettere di esplorare quella parte di vettore. A questa variante è applicata anche l’ottimizzazione precedentemente descritta. Anche in questo caso l’algoritmo si comporta meglio se il vettore in input è quasi ordinato, richiedendo un numero lineare di operazioni rispetto ad n. Il caso pessimo rimane quadratico. La terza variante proposta è il Bidirectional Bubble Sort (conosciuto anche come Cocktail Shaker Sort). Analisi prestazionale di alcuni algoritmi di ordinamento CocktailShakerSort(A) st = -1 limit = A.length while st < limit do swapped = false st = st + 1 len = len - 1 for j = st to limit-1 do if a[j] > a [j+1] then SWAP(a[j], a[j+1]) swapped = true end if end for if swapped then exit while for j = limit downto st do if a[j] > a[j+1] then SWAP(a[j], a[j+1]) swapped = true end if end for if swapped then exit while end while All’interno del ciclo principale il vettore non viene percorso soltanto in avanti, ma anche all’indietro. In questo modo, ad ogni iterazione 6 del ciclo principale, un elemento può essere spostato nella corretta posizione sia verso l’inizio che verso la fine del vettore (da questo fatto il nome Cocktail Shaker Sort). In bubble sort un elemento può infatti muoversi solo verso la fine. Ad ogni iterazione del ciclo esterno vengono perciò messi nella corretta posizione due elementi. Osservando lo pseudocodice, si pos- sono vedere i due costrutti for che implementano l’idea descritta. Il caso migliore, analogamente alle precedenti ottimizzazioni, si presenta su di un vettore già ordinato. In questa condizione, questo algoritmo presenta una complessità pari ad O(n). Di seguito le tabelle relative ai tempi di esecuzione delle tre varianti proposte. Prima variante: terminazione in caso di nessuno scambio eseguito Dimensione Tempo medio(ms) Varianza Δ 10 0.00019217 8.66498e-10 4.07967e-06 20 0.000862944 5.79675e-09 1.0552e-05 50 0.00523499 7.65079e-08 3.83349e-05 100 0.0214639 5.85818e-07 0.000106077 200 0.0894983 5.54904e-06 0.000326475 500 0.568253 8.82856e-05 0.00130222 1000 2.25282 0.000759972 0.00382067 2000 8.93219 0.00452426 0.00932213 5000 55.3304 0.0995923 0.0437375 Tempi di esecuzione di Bubblesort variante 1 0.1 0.08 50 0.06 0.04 Tempo (secondi) 40 0.02 0 30 0 50 100 150 200 20 10 Tempi misurati Funzione tempo stimata 0 0 1000 2000 3000 4000 5000 Numero di elementi Analisi prestazionale di alcuni algoritmi di ordinamento 7 Come si vede dalle misurazioni effettuate, il miglioramento apportato da questa modifica è poco significante. La condizione infatti spesso si verifica, ma soltanto vicino alla fine del ciclo più esterno, cioè quando il vettore è praticamente ordinato. Da ciò si può intuire che bubble sort in questa variante può prestarsi bene per l’ordinamento di dati già parzialmente in ordine, arrivando a richiedere un numero di operazioni vicino ad O(n). Seconda variante: controllo sulle posizioni in cui avvengono spostamenti Dimensione Tempo medio(ms) Varianza Δ 10 0.000185132 1.21865e-09 4.83817e-06 20 0.000853911 8.08129e-09 1.2459e-05 50 0.00529698 9.76744e-08 4.33143e-05 100 0.0219491 8.88338e-07 0.000130626 200 0.0925406 7.15898e-06 0.000370823 500 0.588482 0.000114476 0.00148285 1000 2.34375 0.000949466 0.00427052 2000 9.29413 0.00572882 0.01049 5000 57.7282 0.0712081 0.0369833 Tempi di esecuzione [fbs2(n)≈2.3E-6n2] Tempi di esecuzione di Bubblesort variante 2 60 0.1 0.08 50 Tempo (secondi) 0.06 0.04 40 0.02 0 30 0 50 100 150 200 20 10 Tempi misurati Funzione tempo stimata 0 0 1000 2000 3000 4000 5000 Numero di elementi Analisi prestazionale di alcuni algoritmi di ordinamento 8 Anche l’accorgimento adottato dalla seconda variante si rivela poco utile. Tuttavia in entrambi i casi i miglioramenti si osservano per vettori di piccole dimensioni, ovvero la situazione in cui bubblesort potrebbe essere ancora una scelta interessante. Dai tempi misurati si nota anche come ci sia un prezzo da pagare per il codice aggiunto all’ultima variante. Infatti, per vettori di dimensioni maggiori a 50 i tempi di esecuzione non fanno che peggiorare rispetto all’algoritmo originale. Terza variante: bidirectional bubblesort Solo osservando il grafico subito si nota che la variante bidirezionale di Bubble sort ordina il vettore in tempi più brevi. Tuttavia, l’andamento del grafico ricorda molto quello dell’algoritmo di base, a prova del fatto che entrambi gli algoritmi richiedono un tempo quadratico rispetto alla dimensione dell’input. La velocità di Cocktail Shaker sort misurata nelle condizioni di test sopra descritte si aggira attorno al 140% di quella di Bubble sort. Facendo un quadro della situazione si può affermare che Bubble sort, nelle sue diverse varianti, ben si presta quando si ha a che fare con piccoli insiemi di dati da ordinare oppure quando i dati sono già parzialmente ordinati. Inoltre, in tutte le sue varianti ha la caratteristica di essere stabile. Rimane comunque un algoritmo dispendioso in termini di tempi di esecuzione, e ciò lo esclude da moltissime applicazioni pratiche. Dimensione Tempo medio(ms) Varianza Δ 10 0.000179491 8.20496e-10 3.9699e-06 20 0.000757944 6.1547e-09 1.08729e-05 50 0.00443623 8.70278e-08 4.08856e-05 100 0.0169024 7.8673e-07 0.000122929 200 0.0664207 9.18039e-06 0.000419925 500 0.4094 0.000155312 0.00172721 1000 1.61494 0.00143436 0.00524892 2000 6.3557 0.00797929 0.0123801 5000 39.0888 0.183968 0.0594445 Tempi di esecuzione di Cocktail Shaker Sort [fbs(n)≈1.62E-6n2] Analisi prestazionale di alcuni algoritmi di ordinamento 9 Tempi di esecuzione di <oc=tail >?a=er sort 40 0.06 0.04 Tempo (secondi) 30 0.02 0 20 0 50 100 150 200 10 Tempi misurati Funzione tempo stimata 0 0 1000 2000 3000 4000 5000 Numero di elementi Barianti di (u**lesort a conCronto 60 0.1 0.0E 50 Tempo (secondi) 0.06 0.04 40 0.02 0 30 0 50 100 150 200 20 (u**le sort std (u**lesort 31 (u**lesort 32 Cocktail Shaker sort 10 0 0 1000 2000 3000 4000 5000 Numero di elementi Insertion Sort Insertion Sort è un altro algoritmo di ordinamento molto semplice. Il suo funzionamento ricorda molto il modo di ordinare una mano di carte. La sua complessità tuttavia, nel caso peggiore è quadratica anche se nella pratica le prestazioni sono migliori di quelle di Bubble Sort. Insertion sort ha l’interessante caratteristica di poter ordinare online, ovvero non serve che riceva in input l’array completo per poter Analisi prestazionale di alcuni algoritmi di ordinamento cominciare ad eseguire l’ordinamento. Guardando il codice si nota subito che vi sono due cicli: il ciclo for esegue comunque n iterazioni mentre il ciclo while può eseguirne n come pure nessuna, rispettivamente nel caso pessimo e nel caso ottimo. 10 InsertionSort(A) for j = 2 to length(A) do key = A[j] i=j-1 while (i > 0) and (key < A[i]) do A[i+1] = A[i] i = i - 1; end while A[i+1] = key end for La migliore performance di Insertion Sort si osserva quando viene dato come input un array ordinato. Infatti, in questo caso la guardia del costrutto while non può risultare mai vera, e di conseguenza non viene mai eseguito. La sua correttezza si dimostra per induzione sulla lunghezza dell’array: nel caso vi sia un solo elemento l’algoritmo non fa nulla in quanto un array composto da un singolo oggetto è ordinato per definizione. Nel caso vi siano più elementi, il ciclo while termina soltanto quando il valore contenuto in key è minore al valore in A[j] per j > i+1, per cui A[n] = key viene correttamente posizionato. Tempi di esecu2ione di )nsertionsort 0.025 14 0.02 Tempo (secondi) 12 0.015 0.01 10 0.005 8 0 0 6 50 100 150 200 4 2 )mport /un2ione tempo stimata 0 0 1000 2000 3000 4000 5000 Numero di elementi Dimensione Tempo medio(ms) Varianza Δ 10 0.000131832 5.96994e-10 3.3863e-06 20 0.000415083 1.30357e-09 5.00389e-06 50 0.00190172 1.65145e-08 1.78104e-05 100 0.00659395 1.55447e-07 5.46427e-05 200 0.0242137 9.54418e-07 0.000135397 500 0.144402 1.3632e-05 0.000511707 Analisi prestazionale di alcuni algoritmi di ordinamento 11 Dimensione Tempo medio(ms) Varianza Δ 1000 0.570545 0.000131361 0.00158846 2000 2.24733 0.00119151 0.00478399 5000 13.9732 0.0333317 0.0253029 Tempi di esecuzione di Insertion Sort [fis(n)≈5.9E-7n2] Dal grafico si osserva che, similmente a bubble sort, l’andamento è quadratico. A bubble sort però serve quattro volte il tempo di insertion sort per portare a termine il suo compito. Insertion sort condivide con bubble sort la caratteristica di riuscire ad ordinare in tempi prossimi ad O(n) insiemi di dati già parzialmente ordinati. Vedremo, in seguito, una variante di Quick sort che sfrutta proprio questa caratteristica per ordinare un vettore in tempi brevissimi. La richiesta di memoria è costante qualunque sia il vettore in ingresso. Algoritmi “Divide et impera” Ci accingiamo ora a trattare gli algoritmi che seguono la filosofia “divide et impera”. Fanno parte di questa categoria Merge Sort e Quick Sort. In entrambi i casi si divide il problema fino ad una dimensione banale (ad esempio all’array di un elemento) per poi ricomporre il tutto nel giusto ordine. Quick Sort QuickSort(A, p, r) if p < r then q = partition(A, p, r) QuickSort(A, p, q) QuickSort(A, q+1, r) end if La prima cosa che si nota guardando lo pseudocodice di Quick Sort è la sua natura ricorsiva. Questa formulazione rende l’algoritmo di semplice comprensione: l’array da ordinare Analisi prestazionale di alcuni algoritmi di ordinamento viene spezzato in due parti attorno al pivot, e i due sottoarray vengono ordinati tramite quick sort stesso. La chiave di questo algoritmo è la procedura partition: essa ha lo scopo di scegliere un elemento pivot e di inserire alla sua destra gli elementi più grandi e alla sua sinistra quelli più piccoli. Una cattiva implementazione di partition potrebbe abbattere le performance di quick sort. Di seguito verranno analizzate sei varianti di Quick sort oltre all’implementazione classica: a singola ricorsione, senza ricorsione, con partition “in place”, con strategia “median of 3”, con bubble sort e con insertion sort. La prima variante di quick sort prevede l’eliminazione di una delle due chiamate ricorsive , mentre la seconda di eliminarle entrambe ed utilizzare esplicitamente uno stack Lo scopo è quello di ridurre l’overhead introdotto dalle chiamate di procedura. Qualunque linguaggio di programmazione si usi, infatti, nel ,momento in cui si chiama una funzione è necessario che l’ambiente runtime manipoli lo stack per salvare indirizzo di ritorno ed altre informazioni. A prima vista Quick sort potrebbe sembrare un algoritmo in-place, ma non lo è a causa della presenza delle chiamate ricorsive che costringono ad utilizzare un nuovo frame dello stack per ognuna di esse. Nel caso pessimo si può arrivare ad avere bisogno di O(n) frames. Modificando la versione di quick sort con una singola chiamata ricosiva in modo che la chiamata avvenga sempre sulla parte più lunga dell’array si può forzare un utilizzo dello stack pari ad O(logn) Come al solito, ci si aspetta un comportamento asintotico simile, in quanto questo genere di ottimizzazioni può eliminare soltanto delle operazioni di costo costante. 12 Dimensione Tempo medio(ms) Varianza Δ 10 0.000344601 5.3506e-10 3.20584e-06 20 0.000870841 2.39858e-09 6.78763e-06 50 0.00299808 1.35933e-08 1.61586e-05 100 0.00731581 2.99027e-08 2.3966e-05 200 0.0168873 8.82764e-08 4.11778e-05 500 0.0484548 3.75111e-07 8.4883e-05 1000 0.105361 1.17865e-06 0.000150464 2000 0.226866 1.30343e-05 0.000500363 5000 0.621718 9.38068e-05 0.00134233 Tempi di esecuzione di Quick Sort Standard [fqss(n)≈3.58E-5nlogn] Dimensione Tempo medio(ms) Varianza Δ 10 0.000320897 6.30363e-10 3.47966e-06 20 0.000838324 2.89181e-09 7.45291e-06 50 0.00291273 1.42476e-08 1.65429e-05 100 0.0071217 3.53444e-08 2.60556e-05 200 0.0166375 8.29127e-08 3.99072e-05 500 0.0479976 3.26547e-07 7.91979e-05 1000 0.10453 1.48815e-06 0.000169069 2000 0.225682 4.57843e-06 0.000296551 5000 0.619223 9.27412e-05 0.00133468 Tempi di esecuzione di Quick Sort a singola ricorsione Dimensione Tempo medio(ms) Varianza Δ 10 0.000541272 7.62773e-10 3.82771e-06 20 0.00109773 2.16342e-09 6.44631e-06 Analisi prestazionale di alcuni algoritmi di ordinamento 13 Dimensione Tempo medio(ms) Varianza Δ 50 0.00330586 9.82308e-09 1.37361e-05 100 0.00767038 3.98248e-08 2.76578e-05 200 0.0171316 1.31992e-07 5.03518e-05 500 0.0485603 3.71486e-07 8.44719e-05 1000 0.104373 1.07114e-06 0.000143438 2000 0.224417 4.82375e-06 0.000304392 5000 0.613518 2.45266e-05 0.000686373 Tempi di esecuzione di Quick Sort non ricorsivo Le tabelle qui sopra mostrano i tempi di esecuzione di Quick sort nelle sue diverse varianti: si può notare in particolare l’inefficienza di quicksort non ricorsivo per array molto corti. Il rallentamento rispetto alla versione originale è dovuto alla necessità di allocare uno stack per tener traccia delle operazioni da eseguire. Per lunghezze maggiori a 1000 l’eliminazione della ricorsione da un, seppur modesto, incremento di prestazioni Un buon compilatore tuttavia riesce a capire quando ha a che fare con codice ricorsivo e a tradurre la ricorsione di coda in un ciclo. Ne segue che nella pratica la ricorsione non crea troppi problemi di performance, in quanto l’overhead dell’inizializzazione del record di attivazione è automaticamente eliminato. Lo stesso non si può dire della chiamata a partition. Il record di attivazione va sempre e comunque gestito, richiedendo una certa spesa in termini di operazioni. La terza ottimizzazione proposta mira ad eliminare appunto la perdita di tempo necessaria a chiamare partition. In questa variante partition è integrata all’interno di quick sort. Dalle misure effettuate, si può notare come questo accorgimento riduca sensibilmente i tempi di esecuzione. Dimensione Tempo medio(ms) Varianza Δ 10 0.000316301 1.60052e-09 5.54462e-06 20 0.000788902 3.5593e-09 8.26843e-06 50 0.00261053 1.81758e-08 1.86848e-05 100 0.0061502 4.83432e-08 3.04726e-05 200 0.0139342 1.43949e-07 5.2583e-05 500 0.0398007 5.26368e-07 0.000100551 1000 0.0865558 2.37491e-06 0.000213582 2000 0.186278 1.0501e-05 0.000449115 Analisi prestazionale di alcuni algoritmi di ordinamento 14 Dimensione Tempo medio(ms) Varianza Δ 5000 0.508326 0.000121352 0.00152674 Tempi di esecuzione di Quick Sort con partition “in place”[fqsfast(n)≈3E-5nlogn] Qualunque delle quattro varianti finora descritte venga scelta, non si può evitare di scontrarsi con un problema di base di quick sort, ovvero la necessità di scegliere al meglio il pivot. Per ottenere una buona velocità infatti l’elemento attorno al quale si esegue lo split deve essere più possibile vicino all’elemento medio del vettore da ordinare. Si è voluto proporre un’ulteriore analisi in questa direzione, prendendo in considerazione la strategia median-of-3 per partition. Questa strategia, pur non eliminando il problema, cerca di raffinare la scelta. A differenza della classica partition, che prende ciecamente il primo elemento del vettore come pivot, questa tecnica prende tre elementi dal vettore e usa l’elemento medio come pivot. La procedura non restituisce un vettore completamente ordinato, per cui il restante lavoro da fare viene portato a termine da Insertion sort, che, come si è precedentemente visto, in questa condizione richiede Ω(n) operazioni. Dimensione Tempo medio(ms) Varianza Δ 10 0.000144081 5.12021e-10 3.13607e-06 20 0.000369725 1.33573e-09 5.06525e-06 50 0.00133519 5.76597e-09 1.05239e-05 100 0.00351465 1.28967e-08 1.57391e-05 200 0.00883438 3.63695e-08 2.64308e-05 500 0.027493 1.39567e-07 5.17765e-05 1000 0.0616629 1.30984e-06 0.000158617 2000 0.134872 1.18079e-06 0.000150601 5000 0.373345 7.64736e-06 0.000383263 Tempi di esecuzione di Quick Sort con strategia Median-of-3 [fqsm3(n)≈1.95E-5nlogn] Dalle misure si può vedere che una migliore scelta del pivot porta a netti miglioramenti. Pur risultando un algoritmo quadratico dagli studi teorici, nella pratica il caso pessimo si verifica raramente e in molti casi quick sort può essere una buona scelta. Può però rivelarsi anche un punto debole: supponiamo che quick sort sia impiegato in un servizio esposto su internet; un ipotetico attaccante potrebbe utilizzare delle “killer sequences”, ovvero un input su cui Analisi prestazionale di alcuni algoritmi di ordinamento quick sort è quadratico, per sferrare un attacco di tipo Denial Of Service. E’ infatti possibile, analizzando a runtime l’esecuzione dell’algoritmo, generare un siffatto input. Per quanto si possa cercare di scrivere una versione “furba” di quick sort (l’implementazione della funzione qsort della libreria standard C della GNU è di circa 200 righe) non c’è modo di evitare il fenomeno. Nel file pdf allegato (mdmspe.pdf) si 15 può trovare una descrizione dettagliata del problema e un generatore di killer sequences. Confrontando i tempi di esecuzione con quelli di bubble sort e di insertion sort si nota che per vettori corti quick sort non da proprio il massimo. Le ultime due varianti di quick sort presentate tentano di ottimizzare questo comportamento. Nella prima variante, quando viene dato in input un vettore di una lunghezza inferiore ad una certa soglia, non viene più utilizzato quick sort, ma si cambia a bubble sort. L’idea per la seconda variante è la stessa, solo che al posto di bubble sort si utilizza insertion sort. Per trovare il valore di soglia si è proceduto nel seguente modo: guardando le misure effettuate sui singoli algoritmi si è cercato l’intervallo in cui avviene l’incrocio tra i tempi di esecuzione. Una volta individuato l’intervallo, si sono misurati i tempi di esecuzione dei singoli algoritmi aumentando di volta in volta la lunghezza del vettore di una unità. Il valore per cui la differenza tra i due algoritmi era minima è stato scelto come soglia. Per quick sort + bubble sort è stato rilevato un valore di soglia pari a 15, mentre per quick sort + insertion sort il valore rilevato è 95. Dimensione Tempo medio(ms) Varianza Δ 10 0.000225937 1.20386e-09 4.80872e-06 20 0.000591974 5.32406e-09 1.01126e-05 50 0.00214872 2.07987e-08 1.99875e-05 100 0.00559418 6.53033e-08 3.54167e-05 200 0.0136521 1.6674e-07 5.65927e-05 500 0.0409926 4.83336e-07 9.6353e-05 1000 0.0909046 1.59186e-06 0.000174861 2000 0.19902 6.27188e-06 0.000347088 5000 0.551448 7.51731e-05 0.00120163 Tempi di esecuzione di QuickBubble Sort [fqb(n)≈2.7E-5nlogn] Dimensione Tempo medio(ms) Varianza Δ 10 0.000156049 7.17582e-10 3.71259e-06 20 0.000478635 1.4827e-09 5.33664e-06 50 0.00207519 2.30903e-08 2.10599e-05 100 0.00492316 4.46997e-07 9.26602e-05 200 0.0115035 6.70574e-07 0.000113492 500 0.0344135 1.79465e-06 0.000185665 1000 0.0781754 3.91136e-06 0.000274097 Analisi prestazionale di alcuni algoritmi di ordinamento 16 Dimensione Tempo medio(ms) Varianza Δ 2000 0.171445 1.48201e-05 0.00053354 5000 0.483218 7.98328e-05 0.00123832 Tempi di esecuzione di QuickInsert Sort [fqi(n)≈2.5E-5nlogn] Approfondimento su Quicksort Median-of-three Come già precedentemente detto, Quicksort Median-of-three utilizza un metodo più furbo per scegliere il pivot: vediamo come. QuicksortM3Aux(A, p, r) if (r - p) < 4 then i = (r + p)/2 if A[p] > A[i] then swap( A[p], A[i] ) if A[p] > A[r] then swap( A[p], A[r] ) if A[i] > A[r] then swap( A[i], A[r] ) j = r-1 swap( A[i], A[j] ) i=p z = A[j] while true do while A[++i] < z do nothing while A[--j] > z do nothing if j < i then exit while swap( A[j], A[j] ) end while swap( A[i], A[r-1] ) QuicksortM3Aux(A, p, j) QuicksortM3Aux(A, i+1, r) end if QuicksortM3(A) QuicksortM3Aux(A, 0, A.length-1) InsertionSort(A) Analisi prestazionale di alcuni algoritmi di ordinamento Nonostante la sua apparente complessità, il codice di Quicksort Median-of-three non deve spaventare. Il punto chiave sta nelle righe 4-9: vengono analizzati il primo elemento, l’ultimo ed il centrale del vettore. Di essi, l’elemento mediano (da cui median-of-3), viene scelto come pivot. Per il resto si procede come il normale Quicksort, con tanto di doppia ricorsione. Quest’ultima potrebbe essere eliminata per ottimizzare ulteriormente l’esecuzione. Da notare la chiamata finale ad InsertionSort. Quicksort Median-of-three infatti riesce ad ordinare la maggior parte del vettore, ma non tutto. Per questo motivo si ricorre ad un algoritmo diverso per eseguire l’ultimo passaggio. Si utilizza solitamente InsertionSort perchè, come già visto, sotto l’ipotesi di input quasi ordinato gira in O(1), e presenta delle basse costanti nascoste. Considerazioni finali su Quicksort Quicksort è un algoritmo molto diffuso e molto studiato. Le varianti qui proposte sono solo alcune delle possibili. È un algoritmo semplice e veloce, disponibile nelle librerie di molti linguaggi di programmazione. L’implementazione banale già fornisce buoni risultati, e come si è visto, lo spazio per le ottimizzazioni è ampio. La variante Median-of-three si è rivelata la migliore. Tuttavia, presenta l’inconveniente di essere quadratico in alcune circostanze. Per questo motivo, in diverse applicazioni si preferisce affidarsi a MergeSort o, meglio, ad HeapSort. 17 Analisi prestazionale di alcuni algoritmi di ordinamento Varianti di Quicksort a confronto 0.6 0.015 0.01 0.5 0.005 0.4 0 0 50 100 150 200 Tempo (secondi) 0.3 0.2 Quicksort std Quicksort ricors. singola Quicksort non ricors. Quicksort no chiam. esterne Quicksort median-of-three QuickBubble QuickInsert 0.1 0 0 1000 2000 Numero di elementi 3000 4000 5000 18 Merge Sort MergeSort(A, p, r) if p < r then q = floor( (p+r)/2 ) MergeSort(A, p, q) MergeSort(A, q+1, r) Merge(A, p, q, r) end if L’ultimo algoritmo analizzato in questa relazione è Merge Sort, sempre appartenente alla categoria “divide et impera”. Se non per la chiamata a Merge dopo e non prima le due chiamate ricorsive, la struttura ricorda molto quella di quick sort. L’idea di merge sort è quella di spezzare il vettore prima a metà poi in quattro, poi in otto e così via fino ad arrivare ai singoli elementi per poi rimontarli in maniera ordinata. Il cuore di merge sort è la funzione “merge”: essa prende come input i sottovettori ordinati A[p..q] e A[q+1..r] e li fonde in A[p..r] in modo ordinato. Ad esempio, se abbiamo il vettore A = {1,3,5,7,2,4,6,8} l’esecuzione di Merge(A,1,4,8) produce Al = {1,3,5,7} ed Ar = {2,4,6,8}. In seguito entra in un ciclo e ad ogni iterazione viene preso l’elemento più piccolo e messo nel vettore originale, producendo A = {1,2,3,4,5,6,7,8}. Ogni chiamata a Merge richiede spazio pari ad O(n), in quanto le due metà in cui è suddiviso il vettore devono necessariamente essere copiate. Scrivere una versione di merge sort in-place è tutt’altro che banale. Merge sort sia nel caso medio che nel caso pessimo ha complessità O(nlogn). Come insertion sort può essere utilizzato online. Dimensione Tempo medio(ms) Varianza Δ 10 0.000764773 4.20398e-10 2.84166e-06 20 0.00168058 1.71027e-09 6.39874e-06 50 0.00516361 5.08464e-09 2.21664e-05 100 0.0115362 1.26674e-08 7.24797e-05 200 0.0255249 1.50344e-07 0.000227361 500 0.0701661 9.682e-07 0.000872909 1000 0.153679 2.93483e-07 0.00408803 2000 0.331103 6.98326e-07 0.011073 5000 0.926093 6.40069e-06 0.0292042 Tempi di esecuzione di Merge Sort [fms(n)≈5E-5nlogn] Conclusioni I risultati ottenuti dai test sono in linea con i risultati degli studi teorici. Dagli esperimenti Analisi prestazionale di alcuni algoritmi di ordinamento condotti si può ottenere una chiara visione dei vantaggi e dei punti deboli dei singoli algoritmi. Di seguito una tabella riassuntiva. 19 Algoritmo Complessità in tempo (migliore/ peggiore) Complessità in spazio (migliore/ peggiore) Stabile Applicazioni Bubble Sort O(n2)/O(n2) O(1)/O(1) Si Quasi nessuna a causa della complessità Bubble Sort #1 O(n)/O(n2) O(1)/O(1) Si c.s. Bubble Sort #2 O(n)/O(n2) O(1)/O(1) Si c.s. Cocktail Shaker Sort O(n)/O(n2) O(1)/O(1) Si c.s. Insertion Sort O(n)/O(n2) O(1)/O(1) Si Vettori corti e/o poca memoria disponibile o all’interno di altri algoritmi Quick Sort O(nlogn)/O(n2) O(logn)/O(n) No General-purpose QuickSort s.r. O(nlogn)/O(n2) O(logn)/O(n) (si può forzare a O(logn)) No c.s. Quick Sort n.r. O(nlogn)/O(n2) O(logn)/O(n) No c.s. Quicksort M.3 O(nlogn)/O(n2) O(logn)/O(n) No c.s. QuickBubble O(nlogn)/O(n2) O(logn)/O(n) No c.s. QuickInsert O(nlogn)/O(n2) O(logn)/O(n) No c.s. MergeSort O(nlogn)/O(nlogn) O(n)/O(n) Si Necessario tempo di esecuzione pari ad O(nlogn) Due parole sulla macro SWAP(a,b) Organizzazione del codice sorgente Il codice è diviso in tre moduli principali: algo.c, measure.c e utils.c. Come si può intuire, in algo.c vi sono le implementazioni degli algoritmi di ordinamento, in measure.c vi sono le routine per l’esecuzione delle misure dei tempi e in utils.c vi sono alcune funzioni di vario utilizzo necessarie quà e là nel programma. In plib.h si trovano i prototipi delle funzioni esportate da ogni modulo e alcune macro. Il codice è stato ampiamente testato soltanto in ambiente FreeBSD, e parzialmente in Mac OS X. Da notare che Mac OS X non supporta i timer POSIX, per cui per compilare il codice su tale sistema è necessario commentare la direttiva USE_POSIX_TIMERS in plib.h. Analisi prestazionale di alcuni algoritmi di ordinamento Eseguendo il test su una macchina PowerPC (7450 a 1500 MHz) , si sono misurati tempi di esecuzione nettamente inferiori per alcuni algoritmi. Riguardando il codice, si è notato che gli algoritmi in questione erano quelli che facevano uso della funzione swap() definita in utils.c. Indagando sul fatto, è emerso che il problema è dovuto al modo il cui il compilatore esegue l’inlining della funzione swap(). Dai listati assembler allegati, infatti, si può notare che nella funzione m_bubble sort_ext viene utilizzato ecx come contatore vero e proprio (la variabile “j”) e l’indirizzo relativo all’elemento del vettore viene calcolato ad ogni iterazione del ciclo for più esterno a partire dal valore presente a 0x18(%esp), cioè il primo argomento della funzione. I costosi accessi in me- 20 moria eseguiti da “mov (%esp), eax” e da “add 0x18(%esp), %eax” spiegano la discrepanza tra i tempi misurati. Grazie a questa macro il compilatore riesce a “spingere” ulteriormente l’ottimizzazione. Il guadagno di performance è notevole: per un vettore di 100.000 elementi bubble sort gira in 48 secondi contro i 64 della versione che non usa la macro. Guardando i listati assembler delle due funzioni compilate per PowerPC si nota che entrambe vengono compilate nel modo “efficiente”. Non c’è stato il tempo di verificare se il problema si manifestava anche su altri sistemi o se il problema è stato sollevato dal livello di ottimizzazione utilizzato (si ricorda che non è garantito che gcc produca codice corretto utilizzando -O3), ma si può sicuramente affermare che la macro non può incidere negativamente sulle prestazioni dell’algoritmo che ne fa uso, per cui è preferibile utilizzare questo sistema per scambiare due variabili. Analisi prestazionale di alcuni algoritmi di ordinamento 21 Bibliografia e riferimenti: [1] Wikipedia - en.wikipedia.org, it.wikipedia.org [2] POSIX.4 - Programming for the real world - Bill O. Gallmeister - O’Reilly [3] An introduction to algorithms - Cormen, Leiserson, Rivest, Stein - MIT Press [4] A killer adversary for quicksort - M. D. McIlroy Impaginato con Apple Pages Grafici disegnati con Plot di Michael Wesemann & Barend J. Thijsse Analisi prestazionale di alcuni algoritmi di ordinamento 22 clock.c Date: Thursday, August 31, 2006 #include #include #include #include <stdio.h> <unistd.h> <signal.h> <time.h> #define TIME 10 void alarm_handler(int v) { printf("clocks: %d\n", clock()); exit(0); } int main(void) { struct sigaction sa; sa.sa_flags = 0; sa.sa_handler = alarm_handler; } if (sigaction(SIGALRM, &sa, NULL) < 0) { perror("sigaction"); exit(-1); } printf("CLOCKS_PER_SEC = %d\n", CLOCKS_PER_SEC); printf("clocks: %d\n", clock()); sleep(TIME); printf("clocks: %d\n", clock()); alarm(TIME); while(1) ; Page 1 of 1 clock2.c Date: Friday, August 11, 2006 #include #include #include #include <time.h> <sys/time.h> <stdio.h> <signal.h> #define SECS 10 struct timespec t0, t1; void print_ts(struct timespec *ts) { printf("seconds:%d, nsec: %u\n", ts->tv_sec, ts->tv_nsec); } void alarm_handler(void) { clock_gettime(CLOCK_VIRTUAL, &t0); print_ts(&t0); exit(0); } int main(void) { struct sigaction sa; extern void alarm_handler(); sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sa.sa_handler = alarm_handler; if ( sigaction(SIGALRM, &sa, NULL) < 0 ) { perror("sigaction"); exit(-1); } clock_getres(CLOCK_VIRTUAL, &t0); print_ts(&t0); clock_gettime(CLOCK_VIRTUAL, &t0); print_ts(&t0); sleep(SECS); clock_gettime(CLOCK_VIRTUAL, &t0); print_ts(&t0); alarm(SECS); } while(1); Page 1 of 1 bubbles.c Date: Friday, August 11, 2006 #include <stdio.h> #define SWAP(a,b) { \ register int __t; \ __t = a; \ a = b; \ b = __t; \ } inline void swap(int *a, int *b) { register int t; t = *a; *a = *b; *b = t; } void m_bubblesort_inl (int *a, int len) { int i, j; for (i = 0; i < len; i++) { for (j = len - 1; j > i; j--) { if (a[j] < a[j - 1]) SWAP(a[j], a[j - 1]); } } } void m_bubblesort_ext (int *a, int len) { int i, j; for (i = 0; i < len; i++) { for (j = len - 1; j > i; j--) { if (a[j] < a[j - 1]) swap(&a[j], &a[j - 1]); } } } main() { int a[100]; m_bubblesort_inl(a,100); m_bubblesort_ext(a,100); } Page 1 of 1 disas.S Date: Friday, August 11, 2006 Page 1 of 1 powerbook:~ matteodj$ gcc -O3 -fomit-frame-pointer -o bubbles bubbles.c powerbook:~ matteodj$ gdb bubbles GNU gdb 6.1-20040303 (Apple version gdb-437) (Sun Dec 25 08:31:29 GMT 2005) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "powerpc-apple-darwin"...Reading symbols for shared libraries .. done (gdb) disas m_bubblesort_inl Dump of assembler code for function m_bubblesort_inl: 0x00002bec <m_bubblesort_inl+0>: cmpwi r4,0 0x00002bf0 <m_bubblesort_inl+4>: blelr 0x00002bf4 <m_bubblesort_inl+8>: addi r10,r4,-1 0x00002bf8 <m_bubblesort_inl+12>: li r11,0 0x00002bfc <m_bubblesort_inl+16>: rlwinm r8,r10,2,0,29 0x00002c00 <m_bubblesort_inl+20>: cmpw cr7,r11,r10 0x00002c04 <m_bubblesort_inl+24>: bgecr7,0x2c34 <m_bubblesort_inl+72> 0x00002c08 <m_bubblesort_inl+28>: subf r0,r11,r10 0x00002c0c <m_bubblesort_inl+32>: add r2,r3,r8 0x00002c10 <m_bubblesort_inl+36>: mtctr r0 0x00002c14 <m_bubblesort_inl+40>: lwz r9,0(r2) 0x00002c18 <m_bubblesort_inl+44>: lwz r0,-4(r2) 0x00002c1c <m_bubblesort_inl+48>: cmpw cr7,r9,r0 0x00002c20 <m_bubblesort_inl+52>: bgecr7,0x2c2c <m_bubblesort_inl+64> 0x00002c24 <m_bubblesort_inl+56>: stw r0,0(r2) 0x00002c28 <m_bubblesort_inl+60>: stw r9,-4(r2) 0x00002c2c <m_bubblesort_inl+64>: addi r2,r2,-4 0x00002c30 <m_bubblesort_inl+68>: bdnz+ 0x2c14 <m_bubblesort_inl+40> 0x00002c34 <m_bubblesort_inl+72>: addi r11,r11,1 0x00002c38 <m_bubblesort_inl+76>: cmpw cr7,r4,r11 0x00002c3c <m_bubblesort_inl+80>: bne+ cr7,0x2c00 <m_bubblesort_inl+20> 0x00002c40 <m_bubblesort_inl+84>: blr End of assembler dump. (gdb) disas m_bubblesort_ext Dump of assembler code for function m_bubblesort_ext: 0x00002c44 <m_bubblesort_ext+0>: cmpwi r4,0 0x00002c48 <m_bubblesort_ext+4>: blelr 0x00002c4c <m_bubblesort_ext+8>: addi r10,r4,-1 0x00002c50 <m_bubblesort_ext+12>: li r11,0 0x00002c54 <m_bubblesort_ext+16>: rlwinm r8,r10,2,0,29 0x00002c58 <m_bubblesort_ext+20>: cmpw cr7,r11,r10 0x00002c5c <m_bubblesort_ext+24>: bgecr7,0x2c8c <m_bubblesort_ext+72> 0x00002c60 <m_bubblesort_ext+28>: subf r0,r11,r10 0x00002c64 <m_bubblesort_ext+32>: add r2,r3,r8 0x00002c68 <m_bubblesort_ext+36>: mtctr r0 0x00002c6c <m_bubblesort_ext+40>: lwz r9,0(r2) 0x00002c70 <m_bubblesort_ext+44>: lwz r0,-4(r2) 0x00002c74 <m_bubblesort_ext+48>: cmpw cr7,r9,r0 0x00002c78 <m_bubblesort_ext+52>: bgecr7,0x2c84 <m_bubblesort_ext+64> 0x00002c7c <m_bubblesort_ext+56>: stw r0,0(r2) 0x00002c80 <m_bubblesort_ext+60>: stw r9,-4(r2) 0x00002c84 <m_bubblesort_ext+64>: addi r2,r2,-4 0x00002c88 <m_bubblesort_ext+68>: bdnz+ 0x2c6c <m_bubblesort_ext+40> 0x00002c8c <m_bubblesort_ext+72>: addi r11,r11,1 0x00002c90 <m_bubblesort_ext+76>: cmpw cr7,r4,r11 0x00002c94 <m_bubblesort_ext+80>: bne+ cr7,0x2c58 <m_bubblesort_ext+20> 0x00002c98 <m_bubblesort_ext+84>: blr End of assembler dump. (gdb) disas386.S Date: Thursday, August 31, 2006 dieselpower# gcc -O3 -fomit-frame-pointer -o bubbles bubbles.c dieselpower# gdb bubbles GNU gdb 6.1.1 [FreeBSD] Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-marcel-freebsd"...(no debugging symbols found)... (gdb) disas m_bubblesort_inl Dump of assembler code for function m_bubblesort_inl: 0x080484a0 <m_bubblesort_inl+0>: push %ebp 0x080484a1 <m_bubblesort_inl+1>: push %edi 0x080484a2 <m_bubblesort_inl+2>: push %esi 0x080484a3 <m_bubblesort_inl+3>: push %ebx 0x080484a4 <m_bubblesort_inl+4>: mov 0x18(%esp),%edi 0x080484a8 <m_bubblesort_inl+8>: xor %esi,%esi 0x080484aa <m_bubblesort_inl+10>: cmp %edi,%esi 0x080484ac <m_bubblesort_inl+12>: mov 0x14(%esp),%ebx 0x080484b0 <m_bubblesort_inl+16>: jge 0x80484dc <m_bubblesort_inl+60> 0x080484b2 <m_bubblesort_inl+18>: lea 0xffffffff(%edi),%ebp 0x080484b5 <m_bubblesort_inl+21>: lea 0x0(%esi),%esi 0x080484b8 <m_bubblesort_inl+24>: mov %ebp,%eax 0x080484ba <m_bubblesort_inl+26>: cmp %esi,%eax 0x080484bc <m_bubblesort_inl+28>: jle 0x80484d7 <m_bubblesort_inl+55> 0x080484be <m_bubblesort_inl+30>: mov %esi,%esi 0x080484c0 <m_bubblesort_inl+32>: mov (%ebx,%eax,4),%ecx 0x080484c3 <m_bubblesort_inl+35>: mov 0xfffffffc(%ebx,%eax,4),%edx 0x080484c7 <m_bubblesort_inl+39>: cmp %edx,%ecx 0x080484c9 <m_bubblesort_inl+41>: jge 0x80484d2 <m_bubblesort_inl+50> 0x080484cb <m_bubblesort_inl+43>: mov %edx,(%ebx,%eax,4) 0x080484ce <m_bubblesort_inl+46>: mov %ecx,0xfffffffc(%ebx,%eax,4) 0x080484d2 <m_bubblesort_inl+50>: dec %eax 0x080484d3 <m_bubblesort_inl+51>: cmp %esi,%eax 0x080484d5 <m_bubblesort_inl+53>: jg 0x80484c0 <m_bubblesort_inl+32> 0x080484d7 <m_bubblesort_inl+55>: inc %esi 0x080484d8 <m_bubblesort_inl+56>: cmp %edi,%esi 0x080484da <m_bubblesort_inl+58>: jl 0x80484b8 <m_bubblesort_inl+24> 0x080484dc <m_bubblesort_inl+60>: pop %ebx 0x080484dd <m_bubblesort_inl+61>: pop %esi 0x080484de <m_bubblesort_inl+62>: pop %edi 0x080484df <m_bubblesort_inl+63>: pop %ebp 0x080484e0 <m_bubblesort_inl+64>: ret 0x080484e1 <m_bubblesort_inl+65>: lea 0x0(%esi),%esi End of assembler dump. (gdb) disas m_bubblesort_ext Dump of assembler code for function m_bubblesort_ext: 0x080484e4 <m_bubblesort_ext+0>: push %ebp 0x080484e5 <m_bubblesort_ext+1>: push %edi 0x080484e6 <m_bubblesort_ext+2>: push %esi 0x080484e7 <m_bubblesort_ext+3>: push %ebx 0x080484e8 <m_bubblesort_ext+4>: push %edx 0x080484e9 <m_bubblesort_ext+5>: mov 0x1c(%esp),%ebp 0x080484ed <m_bubblesort_ext+9>: xor %esi,%esi 0x080484ef <m_bubblesort_ext+11>: cmp %ebp,%esi 0x080484f1 <m_bubblesort_ext+13>: jge 0x804852b <m_bubblesort_ext+71> 0x080484f3 <m_bubblesort_ext+15>: lea 0xffffffff(%ebp),%edi 0x080484f6 <m_bubblesort_ext+18>: lea 0x0(,%edi,4),%eax 0x080484fd <m_bubblesort_ext+25>: mov %eax,(%esp) 0x08048500 <m_bubblesort_ext+28>: cmp %esi,%edi 0x08048502 <m_bubblesort_ext+30>: mov %edi,%ecx 0x08048504 <m_bubblesort_ext+32>: jle 0x8048526 <m_bubblesort_ext+66> 0x08048506 <m_bubblesort_ext+34>: mov (%esp),%eax 0x08048509 <m_bubblesort_ext+37>: add 0x18(%esp),%eax 0x0804850d <m_bubblesort_ext+41>: lea 0x0(%esi),%esi 0x08048510 <m_bubblesort_ext+44>: mov (%eax),%ebx 0x08048512 <m_bubblesort_ext+46>: mov 0xfffffffc(%eax),%edx 0x08048515 <m_bubblesort_ext+49>: cmp %edx,%ebx 0x08048517 <m_bubblesort_ext+51>: jge 0x804851e <m_bubblesort_ext+58> 0x08048519 <m_bubblesort_ext+53>: mov %edx,(%eax) 0x0804851b <m_bubblesort_ext+55>: mov %ebx,0xfffffffc(%eax) 0x0804851e <m_bubblesort_ext+58>: dec %ecx 0x0804851f <m_bubblesort_ext+59>: sub $0x4,%eax Page 1 of 2 disas386.S Date: Thursday, August 31, 2006 0x08048522 <m_bubblesort_ext+62>: 0x08048524 <m_bubblesort_ext+64>: 0x08048526 <m_bubblesort_ext+66>: 0x08048527 <m_bubblesort_ext+67>: 0x08048529 <m_bubblesort_ext+69>: 0x0804852b <m_bubblesort_ext+71>: 0x0804852c <m_bubblesort_ext+72>: 0x0804852d <m_bubblesort_ext+73>: 0x0804852e <m_bubblesort_ext+74>: 0x0804852f <m_bubblesort_ext+75>: 0x08048530 <m_bubblesort_ext+76>: 0x08048531 <m_bubblesort_ext+77>: End of assembler dump. (gdb) Page 2 of 2 cmp jg inc cmp jl pop pop pop pop pop ret lea %esi,%ecx 0x8048510 <m_bubblesort_ext+44> %esi %ebp,%esi 0x8048500 <m_bubblesort_ext+28> %eax %ebx %esi %edi %ebp 0x0(%esi),%esi