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