Riepilogo
Fin qui il corso ha giudicato gli algoritmi in base a una sola domanda: fanno ciò che dicono? Questa lezione aggiunge la seconda, che decide se un programma corretto serve in pratica: quanto costa? Il costo si misura prima senza orologio, contando quante volte si esegue l’operazione dominante, e risulta essere una funzione della dimensione dei dati. Di questa funzione non importa il valore per un dato, ma come cresce: una tabella di , , e mostra che le differenze tra algoritmi, invisibili con dieci dati, diventano abissi con un milione. La notazione si presenta come abbreviazione di questi ordini di crescita, senza definizione formale, con esempi già visti nel corso: Euclide, logaritmico; la somma di un vettore, lineare; confrontare tutte le coppie, quadratico; Fibonacci ingenuo, esponenziale. Poi si misura con l’orologio di <chrono>, std::chrono::steady_clock, e si impara perché una sola misura inganna, perché si misura con ottimizzazione e senza sanitizer, e come si confronta una misura con una previsione: raddoppiando , un algoritmo lineare raddoppia il suo tempo e uno quadratico lo quadruplica.
Obiettivi di apprendimento
- Contare le operazioni dominanti di un algoritmo con uno o due cicli in funzione della dimensione dei dati.
- Classificare un algoritmo come costante, logaritmico, lineare, o quadratico, e prevedere come cambia il suo tempo raddoppiando .
- Misurare il tempo di esecuzione con
std::chrono::steady_clocke confrontare la misura con la previsione. - Scegliere tra due algoritmi per una data dimensione dei dati, giustificando la scelta con l’ordine di crescita.
Contare le operazioni
La seconda domanda
Le lezioni Progettare cicli corretti e Ricorsione hanno insegnato a dimostrare che un algoritmo è corretto: che termina e che il suo risultato soddisfa la postcondizione. Ora, la lezione Algoritmi numerici ha mostrato due test di primalità ugualmente corretti, uno con circa divisioni e l’altro con circa , e la lezione Ricorsione, due modi corretti di calcolare , uno con chiamate e l’altro con 29 somme. Se è così, la correttezza non basta per scegliere: tra due algoritmi corretti, uno può rispondere in un istante e l’altro non terminare prima che la risposta abbia smesso di importare. La seconda domanda che si pone a un algoritmo è, pertanto, quanto costa.
Il costo si potrebbe misurare con un orologio, e la quarta sezione lo farà. Ma il tempo di un’esecuzione dipende dalla macchina, dal compilatore, dalle sue opzioni e da ciò che il computer sta facendo nello stesso momento; da esso non segue nulla sull’algoritmo su un’altra macchina. Ciò che non dipende da nulla di tutto questo è il numero di passi che l’algoritmo compie. Ne segue la prima decisione di questa lezione: misurare il costo contando, e lasciare l’orologio per verificare il conto.
La dimensione e l’operazione dominante
Contare tutti i passi (ogni somma, ogni confronto, ogni incremento dell’indice, ogni accesso alla memoria) sarebbe noioso e inutile, perché quasi tutti accompagnano un passo principale in proporzione fissa. Si sceglie, pertanto, l’operazione dominante: quella che si esegue almeno tante volte quanto qualsiasi altra, a meno di un fattore costante, e che per questo le rappresenta tutte. Nella somma degli elementi di un vettore è la somma; nella ricerca di un massimo, il confronto; in un test di primalità, la divisione. Il numero di volte in cui si esegue si esprime in funzione della dimensione dell’input, che si chiama : il numero di elementi del vettore, il numero che si verifica, il numero di cifre.
Un ciclo semplice che scorre un vettore di elementi e compie con ciascuno una quantità fissa di lavoro esegue la sua operazione dominante volte. Il massimo di un vettore non vuoto, invece, comincia con il primo elemento come candidato e confronta ciascuno degli altri: confronti. La differenza tra e è reale, ma, come si vedrà nella terza sezione, non cambia nulla di ciò che importa.
Il caso peggiore
Alcuni algoritmi non eseguono sempre lo stesso numero di operazioni con dati della stessa dimensione. La ricerca di un valore in un vettore, scorrendolo fino a trovarlo, fa un solo confronto se il valore è nella prima posizione e se non c’è. Per parlare del suo costo si sceglie una delle due situazioni, e la scelta abituale è il caso peggiore: il numero massimo di operazioni tra tutti gli input di dimensione . La ragione è la stessa per cui un contratto si scrive per tutti gli input che soddisfano la precondizione: il caso peggiore è una garanzia, e il caso migliore, una speranza. Sicché la ricerca lineare costa confronti nel caso peggiore, e quando questa lezione dice «il costo» senz’altro, intende quello del caso peggiore.
Cicli annidati
La lezione Cicli ha insegnato a contare le iterazioni di due cicli annidati. Se quello interno fa sempre iterazioni per ciascuna delle di quello esterno, il corpo interno si esegue volte. Se quello interno dipende da quello esterno, si somma riga per riga. Il caso che compare più spesso lavorando con collezioni è quello di tutte le coppie: confrontare ogni elemento con ciascuno di quelli che lo seguono, come quando si cerca se un vettore ha ripetuti.
#include <print>int main(){ for (int n{10}; n <= 10'000; n *= 10) { long long comparisons{0}; for (int i{0}; i < n; ++i) { for (int j{i + 1}; j < n; ++j) { ++comparisons; } } std::println("n = {:>6}: {:>10} confronti; n(n - 1)/2 = {}", n, comparisons, static_cast<long long>(n) * (n - 1) / 2); }}n = 10: 45 confronti; n(n - 1)/2 = 45n = 100: 4950 confronti; n(n - 1)/2 = 4950n = 1000: 499500 confronti; n(n - 1)/2 = 499500n = 10000: 49995000 confronti; n(n - 1)/2 = 49995000Il contatore prende il posto del confronto che farebbe l’algoritmo reale. Nell’iterazione del ciclo esterno, quello interno va da a e fa iterazioni; sicché il totale è , la somma della lezione Cicli:
L’output conferma la formula per ogni dimensione. Il contatore è long long perché, a partire da un di circa sessantacinquemila, il numero di coppie supera il massimo di int (a 32 bit su questa piattaforma), e la formula moltiplica dopo aver convertito per la stessa ragione: il contatore di un algoritmo quadratico trabocca molto prima dei dati.
Si osservi già ciò che la terza sezione generalizzerà: moltiplicando per 10, i confronti si moltiplicano per circa 100. Con dieci dati, 45 confronti; con diecimila, quasi cinquanta milioni.
Caricamento del contenuto…