Ricordiamo che la regola 6, da Parte 1mostra come rendere gli algoritmi SIMD di Rust completamente generici per tipo e LANES. Successivamente dobbiamo scegliere il nostro algoritmo e impostare LANES.
In questa regola vedremo come utilizzare il popolare criterio cassa per confrontare e valutare i nostri algoritmi e opzioni. Nel contesto di range-set-blaze
valuteremo:
- 5 algoritmi: Regolare, Splat0, Splat1, Splat2, Ruota
- 3 livelli di estensione SIMD —
sse2
(128 bit),avx2
(256 bit),avx512f
(512 bit) - 10 tipi di elementi —
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,isize
,usize
- 5 numeri di corsia: 4, 8, 16, 32, 64
- 4 lunghezze di ingresso — 1024; 10.240; 102.400; 1.024.000
- 2 CPU: AMD 7950X con
avx512f
Intel i5–8250U conavx2
Il benchmark misura il tempo medio per eseguire ciascuna combinazione. Quindi calcoliamo il throughput in Mbyte/sec.
Guarda questo nuovo articolo complementare su come iniziare con Criterion. L’articolo mostra anche come spingere (abuso?) Criterion per misurare gli effetti delle impostazioni del compilatore, come il livello di estensione SIMD.
L’esecuzione dei benchmark risulta in un valore di 5000 righe *.csv
file che inizia:
Group,Id,Parameter,Mean(ns),StdErr(ns)
vector,regular,avx2,256,i16,16,16,1024,291.47,0.080141
vector,regular,avx2,256,i16,16,16,10240,2821.6,3.3949
vector,regular,avx2,256,i16,16,16,102400,28224,7.8341
vector,regular,avx2,256,i16,16,16,1024000,287220,67.067
vector,regular,avx2,256,i16,16,32,1024,285.89,0.59509
...
Questo file è adatto per l’analisi tramite tabelle pivot dei fogli di calcolo o strumenti per frame di dati come polare.
Algoritmi e corsie
Ecco una tabella pivot di Excel che mostra, per ciascun algoritmo, il throughput (MByte/sec) rispetto alle corsie SIMD. La tabella calcola la velocità effettiva media tra i livelli di estensione SIMD, i tipi di elemento e la lunghezza di input.
Sul mio computer desktop AMD:
Su un computer portatile Intel:
Le tabelle mostrano che Splat1 e Splat2 funzionano meglio. Mostrano anche più corsie, sempre meglio fino a 32 o 64.
Come può, ad esempio,
sse2
(128 bit di larghezza) elabora 64 corsie dii64
(larghezza 4096 bit)? La rugginecore::simd
Il modulo rende possibile questa magia dividendo automaticamente ed efficientemente i 4096 bit in 32 blocchi da 128 bit ciascuno. L’elaborazione congiunta dei 32 blocchi da 128 bit (apparentemente) consente ottimizzazioni che vanno oltre l’elaborazione indipendente dei blocchi da 128 bit.
Livelli di estensione SIMD
Impostiamo LANES su 64 e confrontiamo i diversi livelli di estensione SIMD sulla macchina AMD. La tabella calcola la velocità effettiva media in base al tipo di elemento e alla lunghezza di input.
Sulla mia macchina AMD, quando utilizzo 64 corsie, sse2
è il più lento. Confrontoavx2
A avx512f
i risultati sono contrastanti. Ancora una volta, gli algoritmi Splat1 e Splat2 funzionano meglio.
Tipi di elementi
Successivamente impostiamo il livello di estensione SIMD su avx512f
e confrontare diversi tipi di elementi. Manteniamo LANES
a 64 e throughput medio su tutta la lunghezza di input.
Vediamo che gli elementi bit per bit, 32 bit e 64 bit vengono elaborati più velocemente. (Tuttavia, per elemento, i tipi più piccoli sono più veloci.) Splat1 e Splat2 sono gli algoritmi più veloci, mentre Splat1 è leggermente migliore.
Lunghezza di input
Infine, impostiamo il tipo di elemento su i32
e vedere la lunghezza dell’input rispetto alla velocità effettiva.
Vediamo che tutti gli algoritmi SIMD fanno più o meno lo stesso con 1 milione di input. Apparentemente Splat1 fa meglio di altri algoritmi su input brevi.
Sembra anche che più corto sia più veloce che più lungo. Questo potrebbe essere il risultato della memorizzazione nella cache o potrebbe essere un artefatto dei benchmark che eliminano dati non allineati.
Conclusioni sui benchmark
Sulla base di questi benchmark, utilizzeremo l’algoritmo Splat1. Per ora imposteremo LANES su 32 o 64, ma per eventuali complicazioni vedere la regola successiva. Infine, consiglieremo agli utenti di impostare il livello di estensione SIMD almeno su avx2
.
as_simd
Prima di aggiungere il supporto SIMD, RangeSetBlaze
era il costruttore principale di from_iter
:
let a = RangeSetBlaze::from_iter((1, 2, 3));
Le operazioni SIMD, tuttavia, funzionano meglio sugli array, non sugli iteratori. Inoltre, costruendo a RangeSetBlaze
da un array è spesso una cosa naturale da fare, quindi ho aggiunto un nuovo file from_slice
costruttore:
#(inline)
pub fn from_slice(slice: impl AsRef<(T)>) -> Self {
T::from_slice(slice)
}
Il nuovo costruttore esegue una chiamata in linea al proprio numero intero from_slice
metodo. Per tutti i tipi interi, eccetto i128
/u128
questo successivo chiama:
let (prefix, middle, suffix) = slice.as_simd();
La ruggine è notturna as_simd
Il metodo trasmuta in modo rapido e sicuro la fetta in:
- un non allineato
prefix
– con cui elaboriamofrom_iter
come prima. middle
un array allineato diSimd
strutturare pezzi- Un non allineato
suffix
– con cui elaboriamofrom_iter
come prima.
Pensa a middle
come suddividere i nostri interi di input in blocchi di dimensione 16 (o qualunque sia la dimensione impostata su LANES). Quindi iteriamo i blocchi attraverso our is_consecutive
funzione, alla ricerca di sequenze di true
. Ogni esecuzione diventa un singolo intervallo. Ad esempio, una sequenza di 160 singoli numeri interi consecutivi da 1000 a 1159 (incluso) verrebbe identificata e sostituita con un singolo Rust RangeInclusive
1000..=1159
. Questo intervallo viene quindi elaborato da from_iter
molto più veloce di from_iter
avrebbe elaborato i 160 numeri interi individuali. Quando is_consecutive
ritorna false
torniamo all’elaborazione dei singoli numeri interi del pezzo from_iter
.
i128
/u128
Come gestire gli array di tipi that core::simd
non gestisce, vale a direi128
/u128
? Per ora, li elaboro solo con la velocità più lenta from_iter
.
Benchmark nel contesto
Come passaggio finale, confronta il tuo codice SIMD nel contesto del tuo codice principale, idealmente su dati rappresentativi.
IL range-set-blaze
la cassa include già punti di riferimenti. Un benchmark misura le prestazioni acquisendo 1.000.000 di numeri interi con vari livelli di complessità. La dimensione media dei ciuffi varia da 1 (nessun grumo) a 100.000 ciuffi. Eseguiamo il benchmark con LANES impostato su 4, 8, 16, 32 e 64. Utilizzeremo l’algoritmo Splat1 e il livello di estensione SIMD avx512f
.
Per ciascuna dimensione del cluster, le barre mostrano la velocità relativa di acquisizione di 1.000.000 di numeri interi. Per ogni dimensione del grumo, il più veloce LANES
è impostato al 100%.
Vediamo che per i ciuffi di dimensione 10 e 100, LANES
=4 è il migliore. Con cespi di dimensione 100.000, invece, LANES
=4 è 4 volte peggiore del migliore. All’altro estremo, LANES=64 sembra buono con gruppi di dimensione 100.000, ma è 1,8 e 1,5 volte peggiore del migliore con 100 e 1000, rispettivamente.
Ho deciso di impostare LANES
a 16. È il migliore per cespi di dimensione 1000. Inoltre non è mai più di 1,25 volte peggiore del migliore.
Con questa impostazione possiamo eseguire altri benchmark. La tabella seguente mostra varie librerie di set di intervalli (inclusi range-set-blaze
) lavorando sullo stesso compito: ingerire 1.000.000 di numeri interi di varia complessità. IL y
-axis è millisecondi dove inferiore è migliore.
Con cespi di dimensione 1000, l’esistente RangeSetBlaze::into_iter
metodo (rosso) era già 30 volte più veloce di HashSet (arancione). Nota che la scala è logaritmica. Con avx512f
il nuovo alimentato SIMD RangeSetBlaze::into_slice
l’algoritmo (azzurro) è 230 volte più veloce di HashSet. Con sse2
(blu scuro), è 220 volte più veloce. Con avx2
(giallo), è 180 volte più veloce. Su questo benchmark, rispetto a RangeSetBlaze::into_iter
, avx512f
RangeSetBlaze::into_slice
è 7 volte più veloce.
Dovremmo considerare anche il caso peggiore, ovvero l’acquisizione di dati senza grumi. Ho eseguito quel benchmark. Ha mostrato l’esistente RangeSetBlaze::into_iter
è circa 2,2 volte più lento di HashSet. Il nuovo RangeSetBlaze::into_slice
è 2,4 volte più lento di HashSet.
Quindi, a conti fatti, il nuovo codice SIMD offre un enorme vantaggio per i dati che si presume siano scomodi. Se il presupposto è sbagliato, il processo sarà più lento, ma non in modo catastrofico.
Con il codice SIMD integrato nel nostro progetto, siamo pronti per la spedizione, giusto? Purtroppo no. Dato che il nostro codice dipende ogni notte da Rust, dovremmo renderlo facoltativo. Vedremo come farlo nella prossima regola.
Il nostro bellissimo nuovo codice SIMD dipende da Rust ogni notte che può cambiare e cambia ogni notte. Richiedere agli utenti di dipendere da Rust ogni notte sarebbe crudele. (Inoltre, ricevere reclami quando le cose si rompono sarebbe fastidioso.) La soluzione è nascondere il codice SIMD dietro una funzione di carico.
Caratteristica, Caratteristica, Caratteristica — Nel contesto del lavoro con SIMD e Rust, la parola “caratteristica” viene utilizzata in tre modi diversi. Innanzitutto, “caratteristiche CPU/destinazione”: descrivono le capacità di una CPU, comprese le estensioni SIMD supportate. Vedere
target-feature
Eis_x86_feature_detected!
. In secondo luogo, “cancelli delle funzionalità notturne”: Rust controlla la visibilità delle nuove funzionalità linguistiche in Rust ogni notte presentano cancelli. Per esempio,#!(feature(portable_simd))
. Terzo, “caratteristiche del carico”- consentono a qualsiasi contenitore o libreria di Rust di offrire/limitare l’accesso a parte delle proprie capacità. Li vedi nel tuoCargo.toml
quando, ad esempio, aggiungi una dipendenza aitertools/use_std
.
Ecco i passaggi che il range-set-blaze
crate impiega per rendere facoltativo il codice SIMD dipendente dalla notte:
- In
Cargo.toml
definire una caratteristica del carico relativa al codice SIMD:
(features)
from_slice = ()
- In cima
lib.rs
file, fai la notteportable_simd
il cancello delle funzionalità, dipende dafrom_slice
caratteristica del carico:
#!(cfg_attr(feature = "from_slice", feature(portable_simd)))
- Utilizza l’attributo di compilazione condizionale, ad esempio,
#(cfg(feature = “from_slice”))
per includere selettivamente il codice SIMD. Ciò include i test.
/// Creates a (`RangeSetBlaze`) from a collection of integers. It is typically many
/// times faster than (`from_iter`)(1)/(`collect`)(1).
/// On a representative benchmark, the speed up was 6×.
///
/// **Warning: Requires the nightly compiler. Also, you must enable the `from_slice`
/// feature in your `Cargo.toml`. For example, with the command:**
/// ```bash
/// cargo add range-set-blaze --features "from_slice"
/// ```
///
/// **Caution**: Compiling with `-C target-cpu=native` optimizes the binary for your current CPU architecture,
/// which may lead to compatibility issues on other machines with different architectures.
/// This is particularly important for distributing the binary or running it in varied environments.
/// (1): struct.RangeSetBlaze.html#impl-FromIterator<T>-for-RangeSetBlaze<T>
#(cfg(feature = "from_slice"))
#(inline)
pub fn from_slice(slice: impl AsRef<(T)>) -> Self {
T::from_slice(slice)
}
- Come mostrato nei documenti sopra, aggiungi avvisi e precauzioni alla documentazione.
- Utilizzo
--features from_slice
per verificare o testare il tuo codice SIMD.
cargo check --features from_slice
cargo test --features from_slice
- Utilizzo
--all-features
per eseguire tutti i test, generare tutta la documentazione e pubblicare tutte le funzionalità del carico:
cargo test --all-features --doc
cargo doc --no-deps --all-features --open
cargo publish --all-features --dry-run
Quindi, ecco qua: nove regole per aggiungere operazioni SIMD al tuo codice Rust. La facilità di questo processo riflette il core::simd
l’eccellente design della biblioteca. Dovresti sempre utilizzare SIMD ove applicabile? Alla fine sì, quando la biblioteca si sposta di notte da Rust a Stable. Per ora, utilizza SIMD laddove i suoi vantaggi in termini di prestazioni sono cruciali o rendine l’utilizzo facoltativo.
Idee per migliorare l’esperienza SIMD in Rust? La qualità di core::simd
è già alto; la necessità principale è stabilizzarla.
Grazie per esserti unito a me in questo viaggio nella programmazione SIMD. Spero che se hai un problema appropriato al SIMD, questi passaggi ti aiuteranno ad accelerarlo.
Per favore segui Carl su Medium. Scrivo di programmazione scientifica in Rust e Python, machine learning e statistica. Tendo a scrivere circa un articolo al mese.
Fonte: towardsdatascience.com