Quanto costa? Costo di esecuzione Spazio e tempo Spazio

Università Roma La Sapienza
Corsi di Laurea
Informatica/Tecnologie Informatiche
Quanto costa?
Prof. Stefano Guerrini
[email protected]
Programmazione II (can. P-Z)
A.A. 2005-06
Spazio e tempo
• Non consideriamo altri costi come:
• costi di progettazione;
• costi di implementazione (tra cui, ad esempio, il
numero di ore di lavoro di un programmatore);
• costi di messa in opera;
• ...
• Restringiamo l’analisi a due aspetti principali
• spazio o memoria necessaria
• tempo di esecuzione
Costo di esecuzione
• Cosa contribuisce al costo di esecuzione di un
programma?
• Tutte le risorse utilizzate:
• tempo di occupazione della CPU
• memoria utilizzata
• numero di chiamate al sistema operativo
• numero di accessi alle periferiche
• ...
Spazio
• La quantità di memoria richiesta dal programma.
• Comprende la memoria necessaria alla:
• acquisizione dei dati di input;
• memorizzazione dei dati temporanei necessari
per l’esecuzione del programma;
• produzione dei dati di output.
• In un’analisi accurata occorre distinguere tra
occupazione della
• memoria centrale
• memoria secondaria o di massa.
Tempo
Approccio Sperimentale /1
• Il tempo necessario per completare il programma.
• Approccio sperimentale:
• L’analisi dipende dal computer su cui eseguo i test.
• Se cambio il computer con uno più veloce, o
• Questioni:
• su quale computer eseguire il programma?
• su quali dati?
• rispetto a quale parametro voglio analizzare la
• L’analisi dipende fortemente da come ho scelto i
Approccio Sperimentale /2
Cosa cerchiamo
• Per eseguire l’analisi devo avere a disposizione il
• Un modo per analizzare il costo di esecuzione di
eseguiamo il programma su varie istanze di input e
riportiamo i tempi di esecuzione su di un grafico.
dipendenza del tempo di esecuzione?
sistema su cui verrà eseguito il programma.
• Se il programma (o la funzione) è parte di un
programma più grande, dovrei avere già
implementato e disponibile l’intero programma.
•
L’analisi può essere eseguita solo dopo aver
implementato il programma, quindi
• per verificare l’efficienza di due soluzioni dovrei
prima implementarle entrambe e poi
confrontare i risultati delle prove sperimentali.
con una architettura innovativa, devo eseguire
nuovamente i test?
dati di input.
• Se il programma è non banale non posso
verificare tutti gli input possibili.
• Con diverse scelte dei dati di input otterrei gli
stessi risultati?
un programma
• indipendente dall’efficienza del sistema su cui
andrò ad eseguirlo;
• che mi consenta di poter confrontare l’efficienza
di due soluzioni di un problema prima di averle
effettivamente implementate e che, di
conseguenza, mi permetta di implementare solo
la più efficiente;
• semplice, che trascuri dettagli inutili.
La funzione costo /1
La funzione costo /2
• Per rappresentare su di un grafico i risultati delle
• Prendiamo il programma che calcola il fattoriale, la
prove sperimentali devo fissare:
• la variabile dipendente (l’ordinata), e.g., il tempo
di esecuzione o l’occupazione massima di
memoria
• la variabile indipendente (l’ascissa), ovvero, il
parametro rispetto a cui analizzo l’andamento
del costo
• Qual è il parametro dei dati di input che devo
scegliere per l’ordinata?
La funzione costo /3
• La scelta più naturale per la variabile indipendente
sembra essere la
!
dimensione dei dati di input
ad esempio, espressa in termini di celle di
memoria o byte occupati.
•
Funzione costo
Indicando con x la dimensione dell’input
!
f(x) = tempo di esecuzione
analogamente se misuro lo spazio (memoria)
scelta naturale per l’ascissa sembra essere il valore
dell’input.
• Se prendiamo un programma che calcola il minimo
di un vettore o che cerca un elemento in un
vettore, qual è il parametro che ci interessa?
• La grandezza del valore da cercare?
• La grandezza dei valori nel vettore?
• Oppure, la lunghezza del vettore?
La scelta dei dati /1
• Per scegliere i dati di input è sufficiente guardare
la loro dimensione?
• Esempio: calcolo del minimo di un vettore V di
lunghezza N
• il tempo di esecuzione è indipendente dagli
elementi presenti nel vettore e dall’ordine in cui
si trovano
• per determinare il minimo devo analizzare
necessariamente tutti gli elementi di V
• devo eseguire N confronti
La scelta dei dati /2
• Esempio: ricerca di un elemento e in un vettore V
di lunghezza N (vettore non ordinato)
• Il tempo varia notevolmente a seconda degli
elementi presenti nel vettore e di come sono
disposti:
• se e non è presente in V, per scoprirlo devo
sempre analizzare tutti gli N elementi di V
Caso peggiore
• Supponiamo di voler realizzare un ponte, il
progetto della struttura e il calcolo del
calcestruzzo saranno fatti tenendo conto degli
sforzi massimi a cui il ponte sarà sottoposto.
• Anche per i programmi, una scelta ragionevole per
i dati di input è il caso peggiore, ovvero, il caso
corrispondente al massimo tempo di esecuzione.
• se e è il primo elemento di V che l’algoritmo va
• Il costo così ottenuto è un upper-bound al tempo
Esplosione esponenziale /1
Esplosione esponenziale /2
ad analizzare (e.g., l’elemento nella prima cella di
V) è sufficiente un solo confronto
Confrontiamo il costo della versione di Fibonacci con doppia
ricorsione con quello della versione ricorsiva efficiente (o con
quella iterativa).
int fib (int n) {
! if (n < 2)
! ! return 1;
! else
! ! return fib(n-1) + fib(n-2);
}
c1+2t(n-2) ! t(n) ! c1+2t(n-1)
•
Proviamo a calcolare il tempo
di esecuzione t(n) di fib(n)
•
Per n<2, la funzione esegue il
test e termina nel ramo then.
Indichiamo con c0 il tempo di
esecuzione di questi casi.
!
t(0) = t(1) = c0
•
(c1+c0)2n/2-c1 ! t(n) ! (c1+c0)2n-1-c1
costo esponenziale
Per n"2, la funzione esegue il
test, richiama ricorsivamente
fib(n-1) e fib(n-2), somma i
risultati ottenuti e termina.
! t(n) = c1+t(n-1)+t(n-2)
di esecuzione di un’altra istanza della stessa
dimensione.
void fib_lin (int n, int *p0, int *p1) {
! if (n > 0) {
! ! fib_lin(n-1, p0, p1);
Indichiamo con b0 il costo
! ! int aux = *p1;
di esecuzione del caso
! ! *p1 = *p0 + *p1;
base n=0 e con b1 il costo
! ! *p0 = aux;
delle istruzioni eseguite
! } else
nel caso ricorsivo n>0
! ! *p1!= 1, *p0 = 0;
senza comprendere la
}
•
t(0) = b0
t(n+1) = b1 + t(n)
t(n) = b1 n + b0
chiamata ricorsiva.
costo lineare
Esplosione esponenziale /3
• Nel caso della doppia ricorsione il programma è
molto meno efficiente, cresce esponenzialmente,
mentre l’altro programma cresce linearmente.
• Per renderci concretamente conto della differenza
di efficienza dei programmi, prendiamo
• c +c e b dell’ordine di 10 s
• pari a soli 40 cicli di una CPU da 4 GHz.
1
0
-8
1
Le costanti
Esplosione esponenziale /4
n
lineare
doppia
ricorsione
10
20
30
40
50
60
70
10-7 s
2 10-7 s
3 10-7 s
4 10-7 s
5 10-7 s
6 10-7 s
6 10-7 s
10-5 s
10-2 s
10 s
3h
130 g
366 anni
374.000 anni
Ricerca in un vettore
• In questo caso le costanti erano dello stesso
ordine di grandezza.
• Supponiamo però che le costanti fossero diverse,
ad esempio, (c0 + c1) ~ 10-3 b0 = 10-11.
• per n = 70 il programma con costo esponenziale
impiega solo 374 anni per terminare!
• Il valore delle costanti non è molto significativo
per sapere cosa succede quando la dimensione
dell’input cresce.
• Anche un incremento di efficienza dei computer di
1000 volte non cambia granché (asintoticamente).
• Confrontiamo due differenti algoritmi per la
• ricerca di un elemento, una chiave, in un vettore,
• sotto l’ipotesi che sulle chiavi sia definito un
ordinamento
• e il vettore delle chiavi sia ordinato.
Ricerca lineare
Ricerca binaria /1
• L’algoritmo più semplice per ricercare la chiave nel
• La ricerca lineare non sfrutta affatto l’ordinamento
• Il caso peggiore è quello in cui la chiave è assente.
• Sarebbe come se, per cercare una parola in un
vettore esegue una scansione lineare.
int linearSearch(int array[], int key, int size) {
! int n;
! for (n = 0; n <= size - 1; ++n)
! ! if (array[n] == key)
! ! ! return n;
! return -1;
}
La funzione tempo varia con legge lineare
t(n) = c1 n + c0
Ricerca binaria /2
• Se prendiamo un qualsiasi elemento in posizione
m del vettore V e lo confrontiamo con la chiave k,
possiamo dire che
• k non è in una posizione i < m, se k > V[m]
• k non è in una posizione i > m, se k < V[m]
• per trovare k possiamo procedere applicando
ricorsivamente la tecnica di ricerca
precedentemente vista ristretta alla parte di V in
cui non posso escludere che si trovi k.
del vettore.
dizionario o un numero in un elenco telefonico,
scorressimo ordinatamente tutte le voci a partire
dalla prima fino all’ultima o fino a quella cercata.
• Nella realtà, cercheremo di aprire il dizionario o
l’elenco del telefono a una pagina p in cui
pensiamo possa essere l’elemento da cercare e, se
non lo troviamo, determinare in base all’ordine
alfabetico se la chiave si trova prima o dopo della
pagina p, applicando nuovamente il procedimento
di ricerca da un solo lato di p.
Ricerca binaria /3
• Nel caso del dizionario, la scelta di m è fatta in
base all’iniziale della parola:
• se l’iniziale è la lettera s, apriremo più vicino al
fondo del dizionario
• se l’iniziale è la lettera c, apriremo più vicino
all’inizio del dizionario
• se l’iniziale è la lettera m, apriremo circa al
centro del dizionario
• e così via...
Ricerca binaria /4
• Se non abbiamo idea del valore minimo e massimo
degli elementi presenti nel vettore,
• né della distribuzione statistica che gli elementi
presenti nel vettore hanno nell’intervallo
compreso tra i valori minimo e massimo che le
chiavi possono assumere,
• la scelta più naturale per il punto m in cui fare il
confronto con la chiave è il centro del vettore.
• In questo modo, ad ogni confronto, dimezziamo la
parte di vettore in cui rimane da eseguire la
ricerca.
Ricerca binaria: ricorsiva
int binarySearchRic(int b[], int key, int low, int high) {
! int middle = (low + high) / 2;
!
!
!
!
!
!
!
!
!
if (low > high) /* intervallo vuoto: chiave non trovata */
! return -1;
if (key == b[middle]) /* trovata */
! return middle;
else if (key < b[middle]) /* cerca nella meta' inferiore */
! high = middle - 1;
else /* cerca nella meta' superiore */
! low = middle + 1;
/* cerca nel nuovo intervallo [high,low] */
! return binarySearch(b, key, low, high);
}
Ricerca binaria: iterativa
int binarySearch(int b[], int key, int low, int high) {
! int middle;
!
!
!
!
!
!
!
!
while (low <= high) {
! middle = (low + high) / 2;
! if (key == b[middle]) /* trovata */
! ! return middle;
! else if (key < b[middle]) /* cerca nella meta' inferiore */
! ! high = middle - 1;
! else /* cerca nella meta' superiore */
! ! low = middle + 1;
}
! return -1;
}
/* non trovata */
Ricerca binaria: complessità
• Parte intera del logaritmo (base 2)
!
lg(n) = min { i | 2i ! n < 2i+1 }
• Il caso peggiore è ancora quello in cui la chiave è
assente.
• Dopo i confronti, nel caso peggiore, il vettore da
ricercare è ridotto ad una sotto-sequenza del
vettore iniziale di lunghezza n/2i.
• L’algoritmo termina al massimo dopo lg(n)+1 passi.
• La funzione costo è logaritmica
"
t(n) = b1 lg(n) + b0
Esempio: controllo ortografico /1
Esempio: controllo ortografico /2
• Supponiamo di voler utilizzare i precedenti
• Ricerca lineare
algoritmi per eseguire il controllo ortografico
• di una monografia di circa 1.000 pagine
• con circa 1.000 parole per pagina
• usando un dizionario con circa 36.000 lemmi
• supponendo che ogni confronto tra una parola
nel dizionario e quella da cercare costi 10-6 s
• Per ogni parola della monografia dovremo
eseguire una ricerca all’interno del dizionario. In
totale, 106 ricerche.
Esempio: controllo ortografico /3
• Ricerca binaria
Supponiamo di memorizzare il dizionario in un
vettore ordinato e di usare la ricerca lineare per
cercare le parole nel dizionario.
• Applicando il caso peggiore dobbiamo eseguire
!
106 lg(36.000)
~
106 15
• per un tempo totale di
!
106 15
10-6 s
confronti
supponiamo di memorizzare il dizionario in un
vettore e di usare la ricerca lineare per cercare le
parole nel dizionario.
• Applicando il caso peggiore dobbiamo eseguire
106 36.000 confronti
!
• per un tempo totale di
!
106 36.000 10-6 s = 36.000 s = 10 h
• Anche supponendo che ogni ricerca non scorra
tutto il dizionario, ma che in media ne scorra
solo la metà, il tempo totale rimane di 5 h.
Analisi asintotica /1
• Nei casi precedentemente analizzati l’enorme
differenza tra l’andamento delle funzioni da
confrontare era immediatamente evidente:
esponenziale/lineare o lineare/logaritmico.
• Prendiamo t (n) = c n e t (n) = c n
1
1
2
2
rapporto f(n) = t2(n)/t1(n)
2e
analizziamo il
• supponendo c /c = 100
• f(10 ) = 10
• f(1) = 0,01# f(100) = 1# # f(10.000) = 100
• inizialmente t è migliore di t , poi t comincia a
1 2
= 15 s
• Il tempo è dell’ordine dei secondi. Anche con un
miglioramento di un fattore 2, o 10 nella velocità
del confronto, nel caso della ricerca lineare, il
tempo totale rimane dell’ordine delle ore.
k
k-2
1
2
2
crescere molto più rapidamente di t1.
Analisi asintotica /2
•
A seconda delle costanti, anche una funzione con
andamento esponenziale può avere inizialmente
valori più piccoli, ma
•
al crescere dell’input, prima o poi supera quella
con andamento lineare (o con qualsiasi andamento
polinomiale).
• L’analisi asintotica analizza cosa succede
quando l’input cresce, più precisamente quando la
dimensione dell’input tende all’infinito.
Analisi asintotica /3
• Nell’analisi asintotica le costanti possono essere
ignorate, conta solo la velocità di crescita della
funzione.
• Anzi, conta solo la componente della funzione che
cresce più velocemente
• e.g., di un polinomio, conta solo il suo grado
• Siccome le costanti dipendono dal sistema su cui si
esegue il programma, l’indipendenza dalle costanti
garantisce l’indipendenza dal calcolatore e dal
compilatore.
Notazione asintotica
• Spesso non conosciamo l’andamento esatto della
funzione da analizzare, ma solo delle funzioni che
la approssimano
Upper-Bound
• La funzione f cresce al più come g
f
O(g)
c, k: x > k: f(x) ! c$g(x)
• garantendo che la funzione non supera certi
c$g(x)
• garantendo che la funzione non è inferiore a
f(x)
valori: upper-bound
certi valori: lower-bound
• garantendo che la funzione varia entro un certo
intervallo.
• Introduciamo una notazione per dire quando i
precedenti bound sono verificati asintoticamente.
Lower-Bound
Tight-Bound
• La funzione f cresce come g"
• La funzione f cresce almeno come g
f
#(g)
c, k: x > k: c$g(x) ! f(x)
f
!(g)
!(g) = O(g)
#(g)
c0, c1, k: x > k: c0$g(x) ! f(x) ! c1$g(x)
c1$g(x)
f(x)
f(x)
c$g(x)
c0$g(x)
Operazione dominante /1
• Per determinare l’andamento asintotico del
programma non è necessario eseguirlo.
Operazione dominante /2
• Dall’analisi del codice del programma o della
• Indichiamo con N (n) il numero di volte che viene
• La determinazione delle costanti richiede la
• Se t(n) è la funzione tempo nel caso peggiore
• I è un’operazione dominante se t O(N ).
• L’ operazione o istruzione dominante non è unica
struttura dell’algoritmo possiamo determinare
l’andamento asintotico del tempo di esecuzione
senza doverlo compilare ed eseguire.
conoscenza esatta del compilatore e
dell’esecutore del programma (al limite astratti).
•
Un ruolo chiave in quest’analisi lo giocano le
operazioni principali, il cuore dell’algoritmo, e.g., il
confronto tra la chiave e l’elemento nella ricerca.
I
eseguita l’istruzione I nel caso peggiore su input di
dimensione n.
I
e spesso può essere determinata facilmente
analizzando la struttura del programma.
Operazione dominante /3
• In una sequenza di istruzioni elementari ogni
istruzione è dominante.
• In un ciclo, che non contiene cicli interni, ogni
istruzione non annidata all’interno di costrutti
condizionali è dominante.
• In una sequenza di cicli annidati, ogni istruzione
dominante del ciclo più interno è dominante per la
sequenza di cicli annidati.
Dimensione dell’input
• Supponiamo che l’input di un programma sia un
vettore di n elementi.
• Se c è la dimensione di un elemento, la dimensione
dell’input è cn.
• Nella maggior parte dei casi il valore di c può
essere ignorato, e.g., in molti casi
• posta h(n) = f(cn)
• h O(g) quando f(c)
O(g)
Operazione dominante /4
• Nel caso di funzioni ricorsive, ogni istruzione
•
dominante per il corpo della funzione privo delle
chiamate ricorsive è dominante.
Noto il numero di volte che viene eseguita tale
istruzione per ogni chiamata ricorsiva
• occorre determinare il numero di volte che viene
richiamata la funzione.
Non sempre è facile eseguire questo calcolo in
modo accurato, perché richiede la soluzione di
equazioni di ricorrenza.
Quasi sempre si possono però trovare upper e
lower-bound.
•
•