Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Confronto delle Prestazioni: Cicli vs. Iteratori

Per decidere se utilizzare cicli o iteratori, è importante sapere quale implementazione sia più veloce: la versione della funzione search con un esplicito ciclo for o quella con gli iteratori.

Abbiamo eseguito un benchmark caricando l’intero contenuto de “Le Avventure di Sherlock Holmes” di Sir Arthur Conan Doyle in una String e cercando la parola “the” nel contenuto. Ecco i risultati del benchmark sulla versione di search utilizzando il ciclo for e quella utilizzando gli iteratori:

bash

test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)

La versione con gli iteratori è stata leggermente più veloce! Non spiegheremo il codice del benchmark qui, perché il punto non è dimostrare che le due versioni siano equivalenti, ma avere una sensazione generale di come queste due implementazioni si confrontino in termini di prestazioni.

Per un benchmark più completo, dovresti controllare utilizzando vari testi di varie dimensioni come contenuto, diverse parole e parole di lunghezze diverse come query e tutte le altre variazioni possibili. Il punto è questo: gli iteratori, sebbene siano un’astrazione ad alto livello, vengono compilati approssimativamente nello stesso codice come se avessi scritto il codice a basso livello da solo. Gli iteratori sono una delle astrazioni a costo zero di Rust, il che significa che utilizzare l’astrazione non comporta alcun costo aggiuntivo a livello di esecuzione. Questo è analogo a come Bjarne Stroustrup, il progettista e implementatore originale di C++, definisce zero-overhead in “Foundations of C++” (2012):

“In generale, le implementazioni di C++ rispettano il principio del zero-overhead: ciò che non usi, non lo paghi. E inoltre: ciò che usi, non potresti scriverlo a mano in modo migliore.”

Come altro esempio, il codice seguente è tratto da un decodificatore audio. L’algoritmo di decodifica utilizza l’operazione matematica della previsione lineare per stimare i valori futuri basandosi su una funzione lineare dei campioni precedenti. Questo codice utilizza una catena di iteratori per eseguire alcune operazioni matematiche su tre variabili in scope: una slice di buffer di dati, un array di 12 coefficienti e una quantità di spostamento dati in qlp_shift. Abbiamo dichiarato le variabili all’interno di questo esempio ma non abbiamo loro dato alcun valore; anche se questo codice non ha molto significato al di fuori del suo contesto, è comunque un esempio conciso e reale di come Rust traduca idee ad alto livello in codice a basso livello.

rust

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
let prediction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}

Per calcolare il valore della previsione, questo codice itera su ciascuno dei 12 valori nei coefficienti e utilizza il metodo zip per accoppiare i valori del coefficiente con i 12 valori precedenti nel buffer. Poi, per ogni coppia, moltiplichiamo i valori insieme, sommiamo tutti i risultati e spostiamo i bit nella somma di qlp_shift bit a destra.

I calcoli in applicazioni come i decodificatori audio spesso privilegiano le prestazioni in modo più elevato. Qui, stiamo creando un iteratore, utilizzando due adattatori e quindi consumando il valore. A quale codice assembly si compilerebbe questo codice Rust? Bene, fino a questa data di scrittura, si compila nello stesso assembly che scriveresti a mano. Non c’è alcun ciclo corrispondente all’iterazione sui valori nei coefficienti: Rust sa che ci sono 12 iterazioni, quindi “srotola” il ciclo. Lo srotolamento è un’ottimizzazione che rimuove l’overhead del codice di controllo del ciclo e genera invece codice ripetitivo per ogni iterazione del ciclo.

Tutti i coefficienti vengono memorizzati nei registri, il che significa che l’accesso ai valori è molto veloce. Non ci sono controlli sui limiti dell’array all’accesso a runtime. Tutte queste ottimizzazioni che Rust è in grado di applicare rendono il codice risultante estremamente efficiente. Ora che sai questo, puoi utilizzare iteratori e chiusure senza paura! Rendono il codice sembrare di livello superiore, ma non impongono alcun penalità sulle prestazioni a runtime.

Sommario

Le chiusure e gli iteratori sono funzionalità di Rust ispirate dalle idee dei linguaggi di programmazione funzionale. Contribuiscono alla capacità di Rust di esprimere chiaramente idee ad alto livello con prestazioni a basso livello. Le implementazioni di chiusure e iteratori sono tali che le prestazioni a runtime non vengono influenzate. Questo fa parte dell’obiettivo di Rust di fornire astrazioni a costo zero.

Ora che abbiamo migliorato l’espressività del nostro progetto I/O, diamo uno sguardo ad altre funzionalità di cargo che ci aiuteranno a condividere il progetto con il mondo.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *