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

Un granchio che delega i calcoli ai piccoli granchi — Fonte: https://openai.com/dall-e-2/. Tutte le altre figure dell'autore.

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ì!

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 di HashSet.

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:

  1. Usa Rust nightly e core::simd, il modulo sperimentale SIMD standard di Rust.
  2. CCC: Controlla, Controlla e Scegli le capacità SIMD del tuo computer.
  3. Impara core::simd, ma in modo selettivo.
  4. Fai brainstorming di algoritmi candidati.
  5. Usa Godbolt e l’intelligenza artificiale per comprendere l’assembly del tuo codice, anche se non conosci il linguaggio assembly.
  6. 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 di SimdElement. Ci riferiamo a una posizione nell’array e all’elemento memorizzato in quella posizione come “lane”. Per impostazione predefinita, copiamo le strutture Simd 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 struttura Simd copiando un array di lunghezza fissa.
  • Simd::from_slice – crea una struttura Simd<T,LANE> copiando i primi LANE elementi di una slice.
  • Simd::splat – replica un singolo valore su tutte le lane di una struttura Simd.
  • slice::as_simd – senza copia, trasforma in modo sicuro una slice regolare in una slice allineata di Simd (con eventuali non allineati).

Conversione Simd

  • Simd::as_array – senza copia, trasforma in modo sicuro una struttura Simd in un riferimento ad un array regolare.

Metodi e operatori Simd

  • simd[i] – estrae un valore dalla lane di un Simd.
  • simd + simd – effettua l’addizione tra elementi di due strutture Simd. Sono supportati anche gli operatori -, *, /, %, calcolo del resto, bitwise-and, -or, xor, -not, -shift.
  • simd += simd – aggiunge una struttura Simd attuale a un’altra, nel posto in cui si trova. Sono supportati anche gli altri operatori.
  • Simd::simd_gt – confronta due strutture Simd, restituendo una Mask che indica quali elementi del primo sono maggiori rispetto a quelli del secondo. Sono supportati anche simd_lt, simd_le, simd_ge, simd_lt, simd_eq, simd_ne.
  • Simd::rotate_elements_left – ruota gli elementi di una struttura Simd a sinistra di una quantità specificata. Sono supportate anche rotate_elements_right.
  • simd_swizzle!(simd, indici) – riorganizza gli elementi di una struttura Simd in base agli indici costanti specificati.
  • simd == simd – verifica l’uguaglianza tra due strutture Simd, restituendo un risultato regolare di tipo bool.
  • Simd::reduce_and – effettua una riduzione con operazione AND bitwise su tutte le lane di una struttura Simd. Sono supportate anche reduce_or, reduce_xor, reduce_max, reduce_min, reduce_sum (ma non reduce_eq).

Metodi e operatori Mask

  • Mask::select – seleziona elementi da due strutture Simd in base a una maschera.
  • Mask::all – indica se la maschera è completamente true.
  • Mask::any – indica se la maschera contiene almeno un true.

Tutto sulle lane

  • Simd::LANES – una costante che indica il numero di elementi (lane) in una struttura Simd.
  • SupportedLaneCount – indica i valori consentiti di LANES. Usato per i generics.
  • simd.lanes – metodo costante che indica il numero di lane di una struttura Simd.

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:

Fonte: questa e tutte le immagini successive dell'autore.

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:

  1. Apri godbolt.org con il tuo browser.
  2. Aggiungi un nuovo editor sorgente.
  3. Seleziona Rust come linguaggio.
  4. 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.
  5. Aggiungi un nuovo compilatore.
  6. Imposta la versione del compilatore su nightly.
  7. Imposta le opzioni (per ora) su -C opt-level=3 -C target-feature=+avx512f.
  8. Se ci sono errori, guarda l’output.
  9. 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 una comparison_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 e u128 , potresti essere sfortunato. Il modulo core::simd non li supporta. Vedremo nella Regola 8 come range-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.