Nove regole per l’accelerazione SIMD del tuo codice Rust (Parte 1)
Nove regole per ottimizzare la velocità SIMD del tuo codice Rust (Parte 1)
Lezioni generali sull’incremento dell’ingestione dei dati nella Crate range-set-blaze
del 7x
Grazie a Ben Lichtman (B3NNY) al Seattle Rust Meetup per avermi indirizzato nella giusta direzione su SIMD.
SIMD (Single Instruction, Multiple Data) sono state una caratteristica dei processori Intel/AMD e ARM fin dai primi anni 2000. Queste operazioni ti consentono, ad esempio, di aggiungere un array di otto i32
a un altro array di otto i32
con un’unica operazione CPU su un singolo core. L’uso delle operazioni SIMD accelera notevolmente determinati compiti. Se non utilizzi SIMD, potresti non sfruttare appieno le capacità della tua CPU.
Questo è “Ancora un articolo su Rust e SIMD”? Sì e no. Sì, ho applicato SIMD a un problema di programmazione e mi sono sentito spinto a scrivere un articolo al riguardo. No, spero che questo articolo approfondisca sufficientemente il tema da poterti guidare nel tuo progetto. Spiega le nuove funzionalità e impostazioni SIMD disponibili in Rust nightly. Include un vademecum sulle SIMD in Rust. Illustra come rendere il tuo codice SIMD generico senza uscire dalla sicurezza di Rust. Ti aiuta a iniziare con strumenti come Godbolt e Criterion. Infine, introduce nuovi comandi di compilazione che semplificano il processo.
La crate range-set-blaze
utilizza il suo metodo RangeSetBlaze::from_iter
per inglobare sequenze potenzialmente lunghe di interi. Quando gli interi sono “aggregati”, può farlo 30 volte più velocemente rispetto alla HashSet::from_iter
standard di Rust. Possiamo fare ancora meglio se utilizziamo operazioni SIMD? Sì!
- Implementare LoRA da zero
- La Terra non è piatta, e nemmeno dovrebbero esserlo i tuoi diagrammi di Voronoi
- Segmentare qualsiasi cosa in 3D per le nuvole di punti Guida completa (SAM 3D)
Vedi la regola 2 di un articolo precedente per la definizione di “aggregati”. Inoltre, cosa succede se gli interi non sono aggregati?
RangeSetBlaze
è 2-3 volte più lento diHashSet
.
Sugli interi aggregati, il nuovo metodo RangeSetBlaze::from_slice
basato sulle operazioni SIMD è 7 volte più veloce di RangeSetBlaze::from_iter
. Ciò lo rende oltre 200 volte più veloce di HashSet::from_iter
. (Quando gli interi non sono aggregati, rimane comunque 2-3 volte più lento di HashSet
.)
Nel corso dell’implementazione di questa accelerazione, ho imparato nove regole che possono aiutarti ad accelerare i tuoi progetti con le operazioni SIMD.
Le regole sono:
- Usa Rust nightly e
core::simd
, il modulo sperimentale SIMD standard di Rust. - CCC: Controlla, Controlla e Scegli le capacità SIMD del tuo computer.
- Impara
core::simd
, ma in modo selettivo. - Fai brainstorming di algoritmi candidati.
- Usa Godbolt e l’intelligenza artificiale per comprendere l’assembly del tuo codice, anche se non conosci il linguaggio assembly.
- Generalizza a tutti i tipi e LANES con generici in linea, (e quando ciò non funziona) macro e (quando ciò non funziona) trait.
Guarda la prossima Parte 2 per queste regole:
7. Utilizza il benchmarking di Criterion per scegliere un algoritmo e scoprire che le LANES dovrebbero (quasi) sempre essere 32 o 64.
8. Integrare il tuo miglior algoritmo SIMD nel tuo progetto con as_simd
, codice speciale per i128
/u128
, e benchmarking aggiuntivo in contesto.
9. Estrarre il tuo miglior algoritmo SIMD dal tuo progetto (per ora) con una funzione cargo opzionale.
Nota: Per evitare ambiguità, li chiamo “regole”, ma sono ovviamente solo suggerimenti.
Regola 1: Usa Rust nightly e core::simd
, il modulo SIMD sperimentale di Rust.
Prima di cercare di utilizzare le operazioni SIMD in un progetto più ampio, assicuriamoci che funzionino correttamente. Ecco i passaggi:
Prima di tutto, crea un progetto chiamato simd_hello
:
cargo new simd_hellocd simd_hello
Modifica il file src/main.rs
per contenere (Rust playground):
// Dici a Rust nightly di abilitare 'portable_simd'#![feature(portable_simd)]use core::simd::prelude::*;// costanti strutture dati Simdconst LANES: usize = 32;const THIRTEENS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([13; LANES]);const TWENTYSIXS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([26; LANES]);const ZEES: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([b'Z'; LANES]);fn main() { // crea una struttura dati Simd da una slice di 32 byte let mut data = Simd::<u8, LANES>::from_slice(b"URYYBJBEYQVQBUBCRVGFNYYTBVATJRYY"); data += THIRTEENS; // aggiunge 13 a ogni byte // confronta ogni byte con 'Z': se il byte è maggiore di 'Z' sottrai 26 let mask = data.simd_gt(ZEES); // confronta ogni byte con 'Z' data = mask.select(data - TWENTYSIXS, data); let output = String::from_utf8_lossy(data.as_array()); assert_eq!(output, "HELLOWORLDIDOHOPEITSALLGOINGWELL"); println!("{}", output);}
Successivamente – le capacità SIMD complete richiedono la versione nightly di Rust. Assumendo di avere Rust installato, installa la versione nightly (rustup install nightly
). Assicurati di avere l’ultima versione nightly (rustup update nightly
). Infine, imposta il progetto per utilizzare nightly (rustup override set nightly
).
Ora puoi eseguire il programma con cargo run
. Il programma applica la decifratura ROT13 a 32 byte di lettere maiuscole. Con SIMD, il programma può decifrare tutti i 32 byte contemporaneamente.
Esaminiamo ogni sezione del programma per capire come funziona. Inizia con:
#![feature(portable_simd)]use core::simd::prelude::*;
Rust nightly offre le sue capacità aggiuntive (o “funzionalità”) solo su richiesta. La dichiarazione #![feature(portable_simd)]
richiede a Rust nightly di rendere disponibile il nuovo modulo sperimentale core::simd
. La dichiarazione use
importa quindi i tipi e i tratti più importanti del modulo.
Nella sezione successiva del codice, definiamo costanti utili:
const LANES: usize = 32;const THIRTEENS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([13; LANES]);const TWENTYSIXS: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([26; LANES]);const ZEES: Simd<u8, LANES> = Simd::<u8, LANES>::from_array([b'Z'; LANES]);
La struttura Simd
è un tipo speciale di array in Rust. (Ad esempio, è sempre allineato in memoria.) La costante LANES
indica la lunghezza dell’array Simd
. Il costruttore from_array
copia un array regolare in Rust per creare un Simd
. In questo caso, poiché vogliamo dei Simd
const
, anche gli array che costruiamo devono essere const
.
Le due righe successive copiano il nostro testo criptato in data
e quindi aggiungono 13 a ogni lettera.
let mut data = Simd::<u8, LANES>::from_slice(b"URYYBJBEYQVQBUBCRVGFNYYTBVATJRYY");data += THIRTEENS;
Cosa succede se commetti un errore e il tuo testo criptato non ha esattamente la lunghezza LANES
(32)? Sfortunatamente, il compilatore non te lo dirà. Invece, quando esegui il programma, from_slice
genererà un’eccezione. Cosa succede se il testo criptato contiene lettere minuscole? In questo esempio di programma, ignoriamo questa possibilità.
L’operatore +=
esegue l’addizione elemento per elemento tra il Simd
data
e il Simd
THIRTEENS
. Il risultato viene messo in data
. Ricorda che nelle versioni di debug dell’addizione regolare in Rust viene controllato l’overflow. Non è così con SIMD. Rust definisce gli operatori aritmetici SIMD per sempre fare l’overflow. I valori di tipo u8
fanno l’overflow dopo 255.
Coincidentalmente, la decrittazione Rot13 richiede anche l’overflow, ma dopo la ‘Z’ anziché dopo 255. Ecco un approccio per gestire l’overflow di Rot13. Sottrai 26 a tutti i valori oltre ‘Z’.
let mask = data.simd_gt(ZEES);data = mask.select(data - TWENTYSIXS, data);
Questo dice di trovare gli elementi oltre ‘Z’ in modo elemento per elemento. Poi, sottrai 26 a tutti i valori. Nei punti di interesse, usa i valori sottratti. Negli altri punti, usa i valori originali. Sembra uno spreco sottrarre a tutti i valori e poi usarne solo alcuni? Con SIMD, ciò non richiede tempo di elaborazione extra e evita i salti. Questa strategia è quindi efficiente e comune.
Il programma termina così:
let output = String::from_utf8_lossy(data.as_array());assert_eq!(output, "HELLOWORLDIDOHOPEITSALLGOINGWELL");println!("{}", output);
Nota il metodo .as_array()
. Trasforma in modo sicuro una struttura Simd
in un array regolare in Rust senza copiarlo.
Sorprendentemente, questo programma funziona bene anche su computer senza estensioni SIMD. Rust nightly compila il codice in istruzioni regolari (non SIMD). Ma non vogliamo solo che funzioni bene, vogliamo che funzioni più velocemente. Questo richiede di attivare la potenza SIMD del nostro computer.
Regola 2: CCC: Controlla, Controlla e Scegli le capacità SIMD del tuo computer.
Per far funzionare i programmi SIMD più velocemente sul tuo computer, devi prima scoprire quali estensioni SIMD supporta il tuo computer. Se hai un computer Intel/AMD, puoi usare il mio comando cargo simd-detect
.
Eseguito con:
rustup override set nightlycargo install cargo-simd-detect --forcecargo simd-detect
Sul mio computer, il risultato è:
extension width available enabledsse2 128-bit/16-bytes true trueavx2 256-bit/32-bytes true falseavx512f 512-bit/64-bytes true false
Questo dice che il mio computer supporta le estensioni SIMD sse2
, avx2
e avx512f
. Di queste, per impostazione predefinita, Rust abilita l’estensione ventennale ubiquitaria sse2
.
Le estensioni SIMD formano una gerarchia con avx512f
sopra avx2
sopra sse2
. L’abilitazione di un’estensione di livello superiore abilita anche le estensioni di livello inferiore.
La maggior parte dei computer Intel/AMD supporta anche l’estensione avx2
, vecchia di dieci anni. È possibile abilitarla impostando una variabile d’ambiente:
# Per il prompt dei comandi di Windowsset RUSTFLAGS=-C target-feature=+avx2# Per le shell di tipo Unix (come Bash)export RUSTFLAGS="-C target-feature=+avx2"
“Forza l’installazione” e esegui nuovamente simd-detect
e dovresti vedere che avx2
è abilitata.
# Forza l'installazione ogni volta per vedere le modifiche a 'enabled'cargo install cargo-simd-detect --forcecargo simd-detect
estensione larghezza disponibile abilitatasse2 128-bit/16-bytes true trueavx2 256-bit/32-bytes true trueavx512f 512-bit/64-bytes true false
In alternativa, è possibile attivare tutte le estensioni SIMD supportate dal proprio computer:
# Per il prompt dei comandi di Windowsset RUSTFLAGS=-C target-cpu=native# Per le shell di tipo Unix (come Bash)export RUSTFLAGS="-C target-cpu=native"
Sul mio computer, ciò abilita avx512f
, una nuova estensione SIMD supportata da alcuni computer Intel e alcuni computer AMD.
È possibile ripristinare le estensioni SIMD ai valori predefiniti (sse2
su Intel/AMD) con:
# Per il prompt dei comandi di Windowsset RUSTFLAGS=# Per le shell di tipo Unix (come Bash)unset RUSTFLAGS
Potresti chiederti perché target-cpu=native
non è l’impostazione predefinita di Rust. Il problema è che i binari creati utilizzando avx2
o avx512f
non verranno eseguiti su computer privi di tali estensioni SIMD. Quindi, se stai compilando solo per il tuo uso personale, utilizza target-cpu=native
. Se, invece, stai compilando per altri, scegli attentamente le tue estensioni SIMD e informa le persone del livello di estensione SIMD che stai assumendo.
Felizmente, qualsiasi livello di estensione SIMD che scegli, il supporto SIMD di Rust è così flessibile che puoi facilmente cambiare la tua decisione in seguito. Successivamente, impariamo i dettagli della programmazione con SIMD in Rust.
Regola 3: Impara core::simd
, ma in modo selettivo.
Per compilare con il nuovo modulo core::simd
di Rust, dovresti imparare i blocchi di costruzione selezionati. Ecco una scheda riassuntiva con le strutture, i metodi, ecc., che ho trovato più utili. Ogni elemento include un link alla sua documentazione.
Strutture
Simd
– un array speciale, allineato e di lunghezza fissa diSimdElement
. Ci riferiamo a una posizione nell’array e all’elemento memorizzato in quella posizione come “lane”. Per impostazione predefinita, copiamo le struttureSimd
anziché fare riferimento ad esse.Mask
– un array booleano speciale che mostra l’inclusione/esclusione su base per “lane”.
Elementi SIMD
- Tipo Floating-Point:
f32
,f64
- Tipo Integer:
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,isize
,usize
- — ma non
i128
,u128
Costruttori
Simd
Simd::from_array
– crea una strutturaSimd
copiando un array di lunghezza fissa.Simd::from_slice
– crea una strutturaSimd<T,LANE>
copiando i primiLANE
elementi di una slice.Simd::splat
– replica un singolo valore su tutte le lane di una strutturaSimd
.slice::as_simd
– senza copia, trasforma in modo sicuro una slice regolare in una slice allineata diSimd
(con eventuali non allineati).
Conversione
Simd
Simd::as_array
– senza copia, trasforma in modo sicuro una strutturaSimd
in un riferimento ad un array regolare.
Metodi e operatori
Simd
simd[i]
– estrae un valore dalla lane di unSimd
.simd + simd
– effettua l’addizione tra elementi di due struttureSimd
. Sono supportati anche gli operatori-
,*
,/
,%
, calcolo del resto, bitwise-and, -or, xor, -not, -shift.simd += simd
– aggiunge una strutturaSimd
attuale a un’altra, nel posto in cui si trova. Sono supportati anche gli altri operatori.Simd::simd_gt
– confronta due struttureSimd
, restituendo unaMask
che indica quali elementi del primo sono maggiori rispetto a quelli del secondo. Sono supportati anchesimd_lt
,simd_le
,simd_ge
,simd_lt
,simd_eq
,simd_ne
.Simd::rotate_elements_left
– ruota gli elementi di una strutturaSimd
a sinistra di una quantità specificata. Sono supportate ancherotate_elements_right
.simd_swizzle!(simd, indici)
– riorganizza gli elementi di una strutturaSimd
in base agli indici costanti specificati.simd == simd
– verifica l’uguaglianza tra due struttureSimd
, restituendo un risultato regolare di tipobool
.Simd::reduce_and
– effettua una riduzione con operazione AND bitwise su tutte le lane di una strutturaSimd
. Sono supportate anchereduce_or
,reduce_xor
,reduce_max
,reduce_min
,reduce_sum
(ma nonreduce_eq
).
Metodi e operatori
Mask
Mask::select
– seleziona elementi da due struttureSimd
in base a una maschera.Mask::all
– indica se la maschera è completamentetrue
.Mask::any
– indica se la maschera contiene almeno untrue
.
Tutto sulle lane
Simd::LANES
– una costante che indica il numero di elementi (lane) in una strutturaSimd
.SupportedLaneCount
– indica i valori consentiti diLANES
. Usato per i generics.simd.lanes
– metodo costante che indica il numero di lane di una strutturaSimd
.
Allineamento a basso livello, offset, ecc.
Quando possibile, utilizzare to_simd
invece.
mem::size_of
,mem::align_of
,mem::align_to
,intrinsics::offset
,pointer::read_unaligned
(non sicuro),pointer::write_unaligned
(non sicuro),mem::transmute
(non sicuro, const)
Altro, forse di interesse
deinterleave
,gather_or
,reverse
,scatter
Con questi elementi di base a disposizione, è ora il momento di costruire qualcosa.
Regola 4: Brainstorming di algoritmi candidati.
Cosa vuoi accelerare? Non saprai in anticipo quale approccio SIMD (se ne esiste uno) funzionerà meglio. Dovresti pertanto creare molti algoritmi che potrai poi analizzare (Regola 5) e testare (Regola 7).
Volevo accelerare range-set-blaze
, una crate per manipolare insiemi di interi “raggruppati”. Speravo che la creazione di is_consecutive
, una funzione per individuare blocchi di interi consecutivi, potesse essere utile.
Sfondo: La crate
range-set-blaze
lavora su interi “raggruppati”. “Raggruppati” qui significa che il numero di intervalli necessari per rappresentare i dati è piccolo rispetto al numero di interi di input. Ad esempio, questi 1002 interi di input
100, 101,
…,489, 499, 501, 502,
…,998, 999, 999, 100, 0
Diventano infine tre intervalli Rust:
0..=0, 100..=499, 501..=999
.(Internamente, la struct
RangeSetBlaze
rappresenta un insieme di interi come una lista ordinata di intervalli disgiunti memorizzati in un BTreeMap efficiente in termini di cache.)Sebbene gli interi di input possano essere non ordinati e ridondanti, ci aspettiamo che spesso siano “belle”. Il costruttore
from_iter
di RangeSetBlaze sfrutta già questa aspettativa raggruppando gli interi adiacenti. Ad esempio,from_iter
trasforma prima i 1002 interi di input in quattro intervalli
100..=499, 501..=999, 100..=100, 0..=0.
con un utilizzo di memoria minimo e costante, indipendente dalla dimensione dell’input. Successivamente li ordina e li unisce.
Mi chiedevo se un nuovo metodo
from_slice
potesse velocizzare la costruzione da input di tipo array individuando rapidamente (alcuni) interi consecutivi. Ad esempio, potrebbe – con un utilizzo minimo e costante della memoria – trasformare i 1002 interi di input in cinque intervalli Rust:
100..=499, 501..=999, 999..=999, 100..=100, 0..=0.
In tal caso,
from_iter
potrebbe quindi completare rapidamente l’elaborazione.
Iniziamo scrivendo is_consecutive
con il Rust tradizionale:
pub const LANES: usize = 16;pub fn is_consecutive_regular(chunk: &[u32; LANES]) -> bool { for i in 1..LANES { if chunk[i - 1].checked_add(1) != Some(chunk[i]) { return false; } } true}
L’algoritmo scorre semplicemente l’array in modo sequenziale, verificando che ogni valore sia uno in più rispetto al suo predecessore. Evita anche l’overflow.
Lo scorrimento degli elementi sembrava così facile che non ero sicuro che SIMD potesse fare di meglio. Ecco il mio primo tentativo:
Splat0
use std::simd::prelude::*;const COMPARISON_VALUE_SPLAT0: Simd<u32, LANES> = Simd::from_array([15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);pub fn is_consecutive_splat0(chunk: Simd<u32, LANES>) -> bool { if chunk[0].overflowing_add(LANES as u32 - 1) != (chunk[LANES - 1], false) { return false; } let added = chunk + COMPARISON_VALUE_SPLAT0; Simd::splat(added[0]) == added}
Ecco un riassunto dei suoi calcoli:
Inizialmente (inutilmente) verifica che il primo e l’ultimo elemento siano distanti 15 unità. Quindi crea added
aggiungendo 15 al primo elemento, 14 al successivo, ecc. Infine, per verificare se tutti gli elementi di added
sono uguali, crea un nuovo Simd
basato sul primo elemento di added
e lo confronta. Ricordiamo che splat
crea una struttura Simd
da un valore.
Splat1 & Splat2
Quando ho menzionato il problema is_consecutive
a Ben Lichtman, è giunto indipendentemente a questa soluzione, Splat1:
const COMPARISON_VALUE_SPLAT1: Simd<u32, LANES> = Simd::from_array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);pub fn is_consecutive_splat1(chunk: Simd<u32, LANES>) -> bool { let subtracted = chunk - COMPARISON_VALUE_SPLAT1; Simd::splat(chunk[0]) == subtracted}
Splat1 sottrae dal valore di confronto il chunk
e verifica se il risultato è lo stesso del primo elemento del chunk
, splattato.
Ha anche ideato una variante chiamata Splat2 che splatta il primo elemento di subtracted
anziché chunk
. Ciò sembrerebbe evitare un accesso di memoria.
Sono sicuro che ti stai chiedendo quale di queste soluzioni sia la migliore, ma prima di discuterne, diamo un’occhiata a due ulteriori candidati.
Swizzle
Swizzle è simile a Splat2, ma utilizza simd_swizzle!
invece di splat
. La macro simd_swizzle!
crea un nuovo Simd
riordinando le lane di un vecchio Simd
secondo un array di indici.
pub fn is_consecutive_sizzle(chunk: Simd<u32, LANES>) -> bool { let subtracted = chunk - COMPARISON_VALUE_SPLAT1; simd_swizzle!(subtracted, [0; LANES]) == subtracted}
Rotate
Questo è diverso. Avevo grandi speranze per questo.
const COMPARISON_VALUE_ROTATE: Simd<u32, LANES> = Simd::from_array([4294967281, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);pub fn is_consecutive_rotate(chunk: Simd<u32, LANES>) -> bool { let rotated = chunk.rotate_elements_right::<1>(); chunk - rotated == COMPARISON_VALUE_ROTATE}
L’idea è ruotare tutti gli elementi verso destra di una posizione. Quindi sottraiamo il chunk
originale da rotated
. Se l’input è consecutivo, il risultato dovrebbe essere “-15” seguito da tutti 1. (Utilizzando la sottrazione con wrap, -15 è 4294967281u32
.)
Ora che abbiamo dei candidati, cominciamo a valutarli.
Regola 5: Utilizza Godbolt e l’intelligenza artificiale per capire l’assembly del tuo codice, anche se non conosci il linguaggio assembly.
Valuteremo i candidati in due modi. Prima di tutto, in questa regola, esamineremo l’assembly generato dal nostro codice. In secondo luogo, nella Regola 7, testeremo la velocità del codice.
Non preoccuparti se non conosci il linguaggio assembly, puoi comunque trarne qualcosa osservandolo.
Il modo più semplice per vedere l’assembly generato è utilizzare il Compiler Explorer, noto anche come Godbolt. Funziona meglio con brevi porzioni di codice che non utilizzano librerie esterne. Ecco a che cosa assomiglia:
Riferendoti ai numeri nella figura precedente, segui questi passaggi per utilizzare Godbolt:
- Apri godbolt.org con il tuo browser.
- Aggiungi un nuovo editor sorgente.
- Seleziona Rust come linguaggio.
- Incolla il codice di interesse. Rendi pubbliche le funzioni di interesse (
pub fn
). Non includere una funzione main o funzioni non necessarie. Lo strumento non supporta librerie esterne. - Aggiungi un nuovo compilatore.
- Imposta la versione del compilatore su nightly.
- Imposta le opzioni (per ora) su
-C opt-level=3 -C target-feature=+avx512f.
- Se ci sono errori, guarda l’output.
- Se vuoi condividere o salvare lo stato dello strumento, clicca su “Condividi”
Dall’immagine precedente, puoi vedere che Splat2 e Sizzle sono identici, quindi possiamo escludere Sizzle dalla considerazione. Se apri una copia della mia sessione Godbolt, vedrai anche che la maggior parte delle funzioni viene compilata in approssimativamente lo stesso numero di operazioni di assembly. Le eccezioni sono Regular, che è molto più lungo, e Splat0, che include il controllo anticipato.
Nell’assembly, i registri a 512 bit iniziano con ZMM. I registri a 256 bit iniziano con YMM. I registri a 128 bit iniziano con XMM. Se vuoi comprendere meglio l’assembly generato, utilizza strumenti di intelligenza artificiale per generare annotazioni. Ad esempio, qui chiedo a Bing Chat riguardo a Splat2:
Prova diverse impostazioni del compilatore, inclusa -C target-feature=+avx2
e poi lasciando completamente fuori target-feature
.
Un minor numero di operazioni di assembly non significa necessariamente una velocità maggiore. Tuttavia, osservare l’assembly ci permette di verificare che il compilatore stia almeno cercando di utilizzare operazioni SIMD, inline const references, ecc. Inoltre, come nel caso di Splat1 e Swizzle, a volte può farci capire quando due candidati sono identici.
Potresti aver bisogno di funzionalità di disassemblaggio oltre a quelle offerte da Godbolt, ad esempio la capacità di lavorare con codice che utilizza librerie esterne. B3NNY mi ha consigliato lo strumento cargo
cargo-show-asm
. L’ho provato e ho trovato che è abbastanza facile da usare.
La libreria range-set-blaze
deve gestire tipi di interi oltre a u32
. Inoltre, dobbiamo scegliere un numero di LANES, ma non abbiamo motivo di pensare che 16 LANES sia sempre il migliore. Per affrontare queste esigenze, nella prossima regola generalizzeremo il codice.
Regola 6: Generizza a tutti i tipi e alle LANES con generici in-linea, (e quando questo non funziona) macros, e (quando questo non funziona) traits.
Per prima cosa, generalizziamo Splat1 con i generici.
#[inline]pub fn is_consecutive_splat1_gen<T, const N: usize>( chunk: Simd<T, N>, comparison_value: Simd<T, N>,) -> boolwhere T: SimdElement + PartialEq, Simd<T, N>: Sub<Simd<T, N>, Output = Simd<T, N>>, LaneCount<N>: SupportedLaneCount,{ let subtracted = chunk - comparison_value; Simd::splat(chunk[0]) == subtracted}
Prima di tutto, notate l’attributo #[inline]
. È importante per l’efficienza e lo useremo praticamente su ogni una di queste piccole funzioni.
La funzione definita sopra, is_consecutive_splat1_gen
, sembra ottima eccetto che ha bisogno di un secondo input, chiamato comparison_value
, che non abbiamo ancora definito.
Se non hai bisogno di una
comparison_value
generica costante, ti invidio. Puoi saltare alla regola successiva se vuoi. Allo stesso modo, se stai leggendo questo in futuro e creare unacomparison_value
generica costante è così facile come far svolgere i tuoi compiti domestici al tuo robot personale, allora ti invidio ancor di più.
Possiamo provare a creare una comparison_value_splat_gen
che sia generica e costante. Purtroppo, né From<usize>
né l’alternativa T::One
sono costanti, quindi questo non funziona:
// NON FUNZIONA PERCHÉ From<usize> non è constpub const fn comparison_value_splat_gen<T, const N: usize>() -> Simd<T, N>where T: SimdElement + Default + From<usize> + AddAssign, LaneCount<N>: SupportedLaneCount,{ let mut arr: [T; N] = [T::from(0usize); N]; let mut i_usize = 0; while i_usize < N { arr[i_usize] = T::from(i_usize); i_usize += 1; } Simd::from_array(arr)}
Le macros sono l’ultima spiaggia dei furfanti. Quindi, usiamo le macros:
#[macro_export]macro_rules! define_is_consecutive_splat1 { ($function:ident, $type:ty) => { #[inline] pub fn $function<const N: usize>(chunk: Simd<$type, N>) -> bool where LaneCount<N>: SupportedLaneCount, { define_comparison_value_splat!(comparison_value_splat, $type); let subtracted = chunk - comparison_value_splat(); Simd::splat(chunk[0]) == subtracted } };}#[macro_export]macro_rules! define_comparison_value_splat { ($function:ident, $type:ty) => { pub const fn $function<const N: usize>() -> Simd<$type, N> where LaneCount<N>: SupportedLaneCount, { let mut arr: [$type; N] = [0; N]; let mut i = 0; while i < N { arr[i] = i as $type; i += 1; } Simd::from_array(arr) } };}
Questo ci permette di eseguire su qualsiasi tipo di elemento particolare e su qualsiasi numero di LANES (Rust Playground):
define_is_consecutive_splat1!(is_consecutive_splat1_i32, i32);let a: Simd<i32, 16> = black_box(Simd::from_array(array::from_fn(|i| 100 + i as i32)));let ninety_nines: Simd<i32, 16> = black_box(Simd::from_array([99; 16]));assert!(is_consecutive_splat1_i32(a));assert!(!is_consecutive_splat1_i32(ninety_nines));
Purtroppo, questo non è ancora sufficiente per range-set-blaze
. Deve funzionare su tutti i tipi di elementi (non solo uno) e (idealmente) su tutte le LANE (non solo una).
Fortunatamente, c’è una soluzione alternativa, che dipende ancora una volta dai macro. Sfrutta anche il fatto che abbiamo solo bisogno di supportare un elenco finito di tipi, ovvero: i8
, i16
, i32
, i64
, isize
, u8
, u16
, u32
, u64
e usize
. Se hai bisogno di supportare anche (o al posto di) f32
e f64
, va bene.
Se, d’altra parte, hai bisogno di supportare
i128
eu128
, potresti essere sfortunato. Il modulocore::simd
non li supporta. Vedremo nella Regola 8 comerange-set-blaze
aggira questo a costo di prestazioni.
La soluzione alternativa definisce un nuovo tratto, qui chiamato IsConsecutive
. Utilizziamo quindi un macro (che chiama un macro, che chiama un macro) per implementare il tratto sui 10 tipi di interesse.
pub trait IsConsecutive {fn is_consecutive & lt;const N: usize & gt; (chunk: Simd & lt;Self, N & gt;) - & gt; bool dove Self: SimdElement, Simd & lt;Self, N & gt;: Sub & lt;Simd & lt;Self, N & gt;, Output = Simd & lt;Self, N & gt; & gt;, LaneCount & lt;N & gt;: SupportedLaneCount; } macro_rules! impl_is_consecutive {($ type: ty) =& gt; {impl IsConsecutive for $type { #[inline] // molto importante fn is_consecutive & lt;const N: usize & gt; (chunk: Simd & lt;Self, N & gt;) - & gt; bool where Self: SimdElement, Simd & lt;Self, N & gt;: Sub & lt;Simd & lt;Self, N & gt;, Output = Simd & lt;Self, N & gt; & gt;, LaneCount & lt;N & gt;: SupportedLaneCount, {define_is_consecutive_splat1!(is_consecutive_splat1, $ type); is_consecutive_splat1(chunk)} }}; } impl_is_consecutive! (i8); impl_is_consecutive! (i16); impl_is_consecutive! (i32); impl_is_consecutive! (i64); impl_is_consecutive! (isize); impl_is_consecutive! (u8); impl_is_consecutive! (u16); impl_is_consecutive! (u32); impl_is_consecutive! (u64); impl_is_consecutive! (usize);
Ora possiamo chiamare codice completamente generico (Rust Playground):
// Funziona su i32 e 16 corsielet un: Simd & lt; i32, 16 & gt; = black_box(Simd::from_array(array::from_fn(|i| 100 + i as i32)));let ninety_nines: Simd & lt; i32, 16 & gt; = black_box(Simd::from_array ([99; 16])); assert! (IsConsecutive::is_consecutive (a)); assert! (! IsConsecutive::is_consecutive (novantanove_novantanove)); // Funziona su i8 e 64 corsielet un: Simd & lt; i8, 64 & gt; = black_box(Simd::from_array(array::from_fn(|i| 10 + i as i8)));let ninety_nines: Simd & lt; i8, 64 & gt; = black_box(Simd::from_array ([99; 64])); assert! (IsConsecutive::is_consecutive (a)); assert! (! IsConsecutive::is_consecutive (novantanove_novantanove));
Con questa tecnica, possiamo creare più algoritmi candidati che sono completamente generici per tipo e LANES. Successivamente, è tempo di eseguire il benchmark e vedere quali algoritmi sono i più veloci.
Queste sono le prime sei regole per aggiungere il codice SIMD a Rust. Nella prossima parte 2, esamineremo le regole da 7 a 9. Queste regole copriranno come scegliere un algoritmo e impostare LANES. Inoltre, come integrare le operazioni SIMD nel codice esistente e (importantemente) come renderlo opzionale. La parte 2 si conclude con una discussione su quando / se si dovrebbe utilizzare SIMD e idee per migliorare l’esperienza SIMD di Rust. La parte 2 sarà pubblicata a breve. Spero di vederti lì.
Per favore, seguite Carl su VoAGI. Scrivo di programmazione scientifica in Rust e Python, apprendimento automatico e statistica. Di solito scrivo circa un articolo al mese.