Девять правил для ускорения SIMD вашего кода на Rust (Часть 1)
Девять правил, чтобы ускорить SIMD вашего кода на Rust (Часть 1)
Общие уроки улучшения восприятия данных в ящике range-set-blaze
на 7 раз
Благодаря Бену Лихтману (B3NNY) из Сиэттльской встречи Rust я нашел нужное направление к SIMD.
SIMD (одиночная инструкция, множественные данные) операции являются особенностью процессоров Intel/AMD и ARM с начала 2000-х годов. Эти операции позволяют, например, добавлять массив из восьми i32
в другой массив из восьми i32
одной операцией процессора на одном ядре. Использование SIMD операций значительно ускоряет определенные задачи. Если вы не используете SIMD, вы, возможно, не полностью используете возможности вашего процессора.
Это “Еще одна статья про Rust и SIMD”? Да и нет. Да, я применил SIMD к проблеме программирования и потом почувствовал обязанность написать статью об этом. Нет, я надеюсь, что эта статья также достаточно глубоко рассматривает вопрос и может помочь вам в вашем проекте. Она объясняет новые возможности SIMD и настройки в Rust nightly. В ней приведены подсказки по SIMD в Rust. В ней показано, как сделать ваш код SIMD-совместимым, не выходя из безопасного Rust. В ней вы узнаете о таких инструментах, как Godbolt и Criterion. Наконец, она представляет новые команды cargo, которые упрощают процесс.
Класс range-set-blaze
использует свой метод RangeSetBlaze::from_iter
для восприятия потенциально длинных последовательностей целых чисел. Когда числа “скучные”, он может делать это в 30 раз быстрее, чем стандартный HashSet::from_iter
в Rust. Можем ли мы сделать еще лучше, используя SIMD операции? Да!
- Реализация LoRA с нуля’.
- Земля не плоская, и ваша диаграмма Вороного не должна быть такой же
- Сегментация любого 3D для облаков точек полное руководство (SAM 3D)
См. правило 2 предыдущей статьи для определения “скучных” чисел. Кроме того, что происходит, если числа не являются “скучными”?
RangeSetBlaze
работает в 2-3 раза медленнее чемHashSet
.
На скучных числах, метод RangeSetBlaze::from_slice
, основанный на SIMD операциях, в 7 раз быстрее, чем RangeSetBlaze::from_iter.
Это делает его более чем в 200 раз быстрее, чем HashSet::from_iter
. (Когда числа не являются скучными, он всё равно на 2-3 раза медленнее, чем HashSet
.)
В процессе реализации этого ускорения я выяснил девять правил, которые могут помочь вам ускорить ваши проекты с помощью SIMD операций.
Правила такие:
- Используйте ночную сборку Rust и
core::simd
, экспериментальный модуль SIMD в Rust. - CCC: Проверьте, контролируйте и выберите возможности SIMD вашего компьютера.
- Изучайте
core::simd
, но выборочно. - Придумывайте возможные алгоритмы.
- Используйте Godbolt и AI для понимания сборки вашего кода, даже если вы не знаете ассемблерный язык.
- Обобщайте для всех типов и ЛЕЙНОВ с помощью встроенных обобщений, (и когда это не работает) макросов и (когда это не работает) трейтов.
См. предстоящую вторую часть для этих правил:
7. Используйте бенчмаркинг Criterion, чтобы выбрать алгоритм и узнать, что ЛЕЙНЫ должны быть (почти) всегда равны 32 или 64.
8. Внедрите ваш алгоритм SIMD в проект с помощью as_simd
, специального кода для i128
/u128
и дополнительного контекстного бенчмаркинга.
9. Извлеките ваш лучший алгоритм SIMD из проекта (на данный момент) с помощью опциональной функциональности грузовика.
Кстати: Чтобы избежать неопределенности, я называю их “правилами”, но, конечно, это всего лишь предложения.
Правило 1: Используйте ночную версию Rust и core::simd
, экспериментальный стандартный модуль SIMD в Rust.
Прежде чем мы попытаемся использовать операции SIMD в большом проекте, давайте убедимся, что они работают вообще. Вот шаги:
Сначала создайте проект с названием simd_hello
:
cargo new simd_hello
cd simd_hello
Измените файл src/main.rs
содержимым (песочница Rust):
// Сообщите ночной версии Rust, чтобы включить 'portable_simd'
#![feature(portable_simd)]
use core::simd::prelude::*;
// Константные структуры Simd
const LANES: usize = 32;
const THIRTEENS: Simd = Simd::::from_array([13; LANES]);
const TWENTYSIXS: Simd = Simd::::from_array([26; LANES]);
const ZEES: Simd = Simd::::from_array([b'Z'; LANES]);
fn main() {
// Создайте структуру Simd из среза из LANES байтов
let mut data = Simd::::from_slice(b"URYYBJBEYQVQBUBCRVGFNYYTBVATJRYY");
data += THIRTEENS; // прибавляем 13 к каждому байту
// сравниваем каждый байт с 'Z', где байт больше 'Z', вычитаем 26
let mask = data.simd_gt(ZEES); // сравниваем каждый байт с 'Z'
data = mask.select(data - TWENTYSIXS, data);
let output = String::from_utf8_lossy(data.as_array());
assert_eq!(output, "HELLOWORLDIDOHOPEITSALLGOINGWELL");
println!("{}", output);
}
Затем — полная возможность SIMD требует ночной версии Rust. Предполагая, что у вас установлен Rust, установите ночную версию (rustup install nightly
). Убедитесь, что у вас последняя ночная версия (rustup update nightly
). Наконец, установите этот проект на использование ночной версии (rustup override set nightly
).
Теперь вы можете запустить программу с помощью cargo run
. Программа применяет ROT13 дешифровку к 32 байтам прописных букв. С помощью SIMD программа может одновременно расшифровать все 32 байта.
Давайте рассмотрим каждый раздел программы, чтобы понять, как он работает. Он начинается с:
#![feature(portable_simd)]
use core::simd::prelude::*;
Ночная версия Rust предлагает свои дополнительные возможности (или “функциональности”) только по запросу. Выражение #![feature(portable_simd)]
запрашивает у ночной версии Rust доступ к новому экспериментальному модулю core::simd
. Затем с помощью выражения use
импортируются наиболее важные типы и трейты этого модуля.
В следующем разделе кода мы определяем полезные константы:
const LANES: usize = 32;
const THIRTEENS: Simd = Simd::::from_array([13; LANES]);
const TWENTYSIXS: Simd = Simd::::from_array([26; LANES]);
const ZEES: Simd = Simd::::from_array([b'Z'; LANES]);
Структура Simd
является особого рода массивом в Rust. (Она, например, всегда выравнивается по памяти.) Константа LANES
указывает длину массива Simd
. Конструктор from_array
копирует обычный массив Rust для создания Simd
. В данном случае, поскольку нам нужны const
Simd
, массивы, которые мы создаем, также должны быть const
.
Следующие две строки копируют наш зашифрованный текст в data
и затем добавляют 13 к каждой букве.
let mut data = Simd::<u8, LANES>::from_slice(b"URYYBJBEYQVQBUBCRVGFNYYTBVATJRYY");data += THIRTEENS;
Что произойдет, если вы допустите ошибку и ваш зашифрованный текст не будет иметь длину LANES
(32)? К сожалению, компилятор вам не скажет об этом. Вместо этого, при запуске программы, from_slice
вызовет аварийное завершение. А что, если зашифрованный текст содержит не заглавные буквы? В этой программе мы проигнорируем эту возможность.
Оператор +=
выполняет поэлементное сложение между Simd
data
и Simd
THIRTEENS
. Результат помещается в data
. Напомним, что отладочные сборки обычного сложения в Rust проверяют на переполнение. Но не в случае SIMD. Rust определяет арифметические операторы SIMD так, чтобы они всегда циклически оборачивались. Значения типа u8
оборачиваются после 255.
К сожалению, декодирование Rot13 также требует оборачивания, но уже после символа ‘Z’, а не после 255. Вот один из подходов к кодированию необходимого оборачивания Rot13. Он вычитает 26 из значений, превышающих символ ‘Z’.
let mask = data.simd_gt(ZEES);data = mask.select(data - TWENTYSIXS, data);
Это означает найти поэлементные места, превышающие символ ‘Z’. Затем вычтите 26 из всех значений. В местах интереса используйте вычтенные значения. В остальных местах используйте исходные значения. Не кажется ли избыточным вычитать из всех значений, а затем использовать только некоторые? В случае SIMD это не занимает лишнего времени компьютера и избегает переходов. Такая стратегия эффективна и обычна.
Программа завершается следующим образом:
let output = String::from_utf8_lossy(data.as_array());assert_eq!(output, "HELLOWORLDIDOHOPEITSALLGOINGWELL");println!("{}", output);
Обратите внимание на метод .as_array()
. Он безопасно приводит структуру Simd
к обычному массиву Rust без копирования.
Удивительно для меня, эта программа работает нормально на компьютерах без SIMD-расширений. Rust nightly компилирует код в обычные (не-SIMD) инструкции. Но нам не просто необходимо работать “нормально”, мы хотим работать быстрее. Для этого нам нужно включить SIMD-мощность нашего компьютера.
Правило 2: CCC: Проверьте, контролируйте и выберите возможности SIMD вашего компьютера.
Чтобы SIMD-программы работали быстрее на вашем компьютере, сначала вам нужно узнать, какие SIMD-расширения поддерживает ваш компьютер. Если у вас есть компьютер Intel/AMD, вы можете использовать мою команду cargo-расширение simd-detect
.
Запустите с помощью:
rustup override set nightlycargo install cargo-simd-detect --forcecargo simd-detect
На моем компьютере это выдает:
extension width available enabledsse2 128-bit/16-bytes true trueavx2 256-bit/32-bytes true falseavx512f 512-bit/64-bytes true false
Это говорит о том, что мой компьютер поддерживает расширения SIMD sse2
, avx2
и avx512f
. Из них, по умолчанию, Rust включает всеобщее расширение sse2
, действующее уже двадцать лет.
Расширения SIMD образуют иерархию с avx512f
выше avx2
выше sse2
. Включение расширения более высокого уровня также включает расширения более низкого уровня.
Большинство компьютеров Intel/AMD также поддерживают десятилетнее расширение avx2
. Вы можете включить его, установив переменную окружения:
# Для командной строки Windowsset RUSTFLAGS=-C target-feature=+avx2# Для подобных Unix-подобным оболочкам (например, Bash)export RUSTFLAGS="-C target-feature=+avx2"
Выполните «принудительную установку» и снова запустите simd-detect
, и вы должны увидеть, что avx2
включен.
# Принудительная установка каждый раз, чтобы увидеть изменения 'enabled'cargo install cargo-simd-detect --forcecargo simd-detect
extension width available enabledsse2 128-бит/16-байт true trueavx2 256-бит/32-байта true trueavx512f 512-бит/64-байта true false
Кроме того, вы можете включить все расширения SIMD, которые поддерживает ваш компьютер:
# Для командной строки Windowsset RUSTFLAGS=-C target-cpu=native# Для подобных Unix-подобным оболочкам (например, Bash)export RUSTFLAGS="-C target-cpu=native"
На моем компьютере это позволяет включить avx512f
, новое расширение SIMD, поддерживаемое некоторыми компьютерами Intel и несколькими компьютерами AMD.
Вы можете вернуть расширения SIMD обратно к своим значениям по умолчанию (sse2
на Intel/AMD) с помощью:
# Для командной строки Windowsset RUSTFLAGS=# Для подобных Unix-подобным оболочкам (например, Bash)unset RUSTFLAGS
Возможно, вы задаетесь вопросом, почему target-cpu=native
не является значением по умолчанию в Rust. Проблема в том, что бинарные файлы, созданные с помощью avx2
или avx512f
, не будут работать на компьютерах без этих расширений SIMD. Поэтому, если вы компилируете только для собственного использования, используйте target-cpu=native
. Однако, если вы компилируете для других людей, тщательно выбирайте свои расширения SIMD и сообщайте людям, какой уровень расширений SIMD вы предполагаете.
К счастью, независимо от выбранного вами уровня расширений SIMD, поддержка SIMD в Rust настолько гибкая, что вы можете легко изменить свое решение позже. Давайте далее узнаем детали программирования с использованием SIMD в Rust.
Правило 3: Изучите core::simd
, но выборочно.
Чтобы собирать с помощью нового модуля core::simd
в Rust, вам следует изучить выбранные строительные блоки. Вот шпаргалка со структурами, методами и т.д., которые я нашел наиболее полезными. Каждый элемент содержит ссылку на его документацию.
Структуры
Simd
– специальный выровненный массив фиксированной длины типаSimdElement
. Мы называем позицию в массиве и элемент, хранящийся на этой позиции, “лейн”. По умолчанию мы копируем структурыSimd
, а не ссылаемся на них.Mask
– специальный массив Boolean, отображающий включение/исключение по лейново.
SimdElements
- Типы с плавающей запятой:
f32
,f64
- Типы целых чисел:
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,isize
,usize
- — но не
i128
,u128
Simd
конструкторы
Simd::from_array
– создает структуруSimd
, копируя массив фиксированной длины.Simd::from_slice
– создает структуруSimd<T,LANE>
, копируя первыеLANE
элементов среза.Simd::splat
– создает структуруSimd
, повторяя одно значение во всех элементах.slice::as_simd
– без копирования безопасно преобразует обычный срез в выровненный срезSimd
(и остаток невыровненный).
Simd
преобразование
Simd::as_array
– без копирования безопасно преобразует структуруSimd
в обычную ссылку на массив.
Simd
методы и операторы
simd[i]
– извлекает значение из элемента срезаSimd
.simd + simd
– выполняет поэлементное сложение двух структурSimd
. Также поддерживаются операторы-
,*
,/
,%
, остаток, побитовое И, ИЛИ, исключающее ИЛИ, отрицание, сдвиг.simd += simd
– добавляет другую структуруSimd
к текущей структуре, на месте. Поддерживаются и другие операторы.Simd::simd_gt
– сравнивает две структурыSimd
, возвращая маскуMask
, указывающую, какие элементы первой структуры больше, чем у второй. Также поддерживаютсяsimd_lt
,simd_le
,simd_ge
,simd_lt
,simd_eq
,simd_ne
.Simd::rotate_elements_left
– поворачивает элементы структурыSimd
влево на указанное количество элементов. Также естьrotate_elements_right
.simd_swizzle!(simd, indexes)
– переставляет элементы структурыSimd
на основе заданных индексов.simd == simd
– проверяет равенство двух структурSimd
, возвращая обычный результатbool
.Simd::reduce_and
– выполняет побитовое И сокращение по все элементам структурыSimd
. Также поддерживаются:reduce_or
,reduce_xor
,reduce_max
,reduce_min
,reduce_sum
(но безreduce_eq
).
Mask
методы и операторы
Mask::select
– выбирает элементы из двух структурSimd
на основе маски.Mask::all
– указывает, все ли элементы маски равныtrue
.Mask::any
– указывает, содержит ли маска хотя бы одно значениеtrue
.
Все о дорожках
Simd::LANES
– константа, указывающая количество элементов (дорожек) в структуреSimd
.SupportedLaneCount
– указывает допустимые значения дляLANES
. Используется в обобщениях.simd.lanes
– константный метод, который указывает количество дорожек в структуреSimd
.
Выравнивание, смещения и т. д. на низком уровне
При возможности используйте to_simd
.
mem::size_of
,mem::align_of
,mem::align_to
,intrinsics::offset
,pointer::read_unaligned
(небезопасно),pointer::write_unaligned
(небезопасно),mem::transmute
(небезопасно, константа)
Больше, возможно, будет интересно
deinterleave
,gather_or
,reverse
,scatter
С этими строительными блоками в руках пришло время что-то построить.
Правило 4: Генерация алгоритмов-кандидатов
Что вы хотите ускорить? Вы заранее не будете знать, какой подход SIMD (если вообще какой-то) будет работать лучше. Вам следует создать много алгоритмов, которые вы затем можете анализировать (Правило 5) и тестировать (Правило 7).
Я хотел ускорить range-set-blaze
, крейт для манипуляции наборами “кучных” целых чисел. Я надеялся, что создание is_consecutive
, функции для обнаружения блоков последовательных чисел, будет полезно.
Фон: Крейт
range-set-blaze
работает с “кучными” целыми числами. “Кучный” здесь означает, что количество диапазонов, необходимых для представления данных, мало по сравнению с количеством входных чисел. Например, эти 1002 входных числа
100, 101,
…,489, 499, 501, 502,
…,998, 999, 999, 100, 0
В конечном итоге превращаются в три диапазона Rust:
0..=0, 100..=499, 501..=999
.(Внутренне, структура
RangeSetBlaze
представляет набор целых чисел в виде отсортированного списка непересекающихся диапазонов, хранящихся с эффективностью использования кэша BTreeMap).Хотя входные числа могут быть неотсортированными и избыточными, мы ожидаем, что они часто будут “хорошими”. Конструктор
from_iter
RangeSetBlaze уже использует эту предпосылку, группируя смежные числа. Например,from_iter
сначала преобразует 1002 входных числа в четыре диапазона
100..=499, 501..=999, 100..=100, 0..=0.
с использованием минимального постоянного объема памяти, независимо от размера ввода. Затем эти уменьшенные диапазоны сортируются и объединяются.
Я задался вопросом, не мог ли бы новый метод
from_slice
ускорить построение из массивообразных входных данных, быстро находя (некоторые) последовательные числа. Например, можно ли было бы – с использованием минимального постоянного объема памяти – преобразовать эти 1002 входных чисел в пять диапазонов Rust:
100..=499, 501..=999, 999..=999, 100..=100, 0..=0.
Если это так, то
from_iter
мог бы быстро завершить обработку.
Давайте начнем с написания is_consecutive
с обычным Rust:
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}
Алгоритм просто проходит по массиву последовательно, проверяя, что каждое значение на единицу больше предыдущего. Он также избегает переполнения.
Поскольку проход по элементам казался настолько простым, я не был уверен, может ли SIMD сделать что-то лучше. Вот моя первая попытка:
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}
Вот обзор его расчетов:
Сначала он (ненужно) проверяет, что первый и последний элементы находятся на расстоянии 15 друг от друга. Затем он создает added
, добавляя 15 к нулевому элементу, 14 к следующему и т. д. Наконец, чтобы увидеть, являются ли все элементы в added
одинаковыми, он создает новый Simd
на основе нулевого элемента в added
и затем сравнивает. Напомним, что splat
создает структуру Simd
из одного значения.
Splat1 & Splat2
Когда я упомянул проблему is_consecutive
Бену Лихтману, он самостоятельно придумал это, 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 вычитает значение сравнения из chunk
и проверяет, является ли результат таким же, как первый элемент chunk
, разбрасывается.
Он также придумал вариацию под названием Splat2, которая разбрасывает первый элемент subtracted
вместо chunk
. Это, видимо, позволяет избежать одного доступа к памяти.
Я уверен, что вы задаетесь вопросом, которой из них лучше, но прежде чем мы обсудим это, давайте рассмотрим еще два варианта.
Swizzle
Swizzle похож на Splat2, но использует simd_swizzle!
вместо splat
. Макрос simd_swizzle!
создает новый Simd
, переставляя дорожки старого Simd
в соответствии с массивом индексов.
pub fn is_consecutive_sizzle(chunk: Simd<u32, LANES>) -> bool { let subtracted = chunk - COMPARISON_VALUE_SPLAT1; simd_swizzle!(subtracted, [0; LANES]) == subtracted}
Rotate
Это другое. У меня были большие надежды на него.
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}
Идея заключается в том, чтобы повернуть все элементы на один вправо. Затем мы вычитаем исходный chunk
из rotated
. Если входные данные последовательны, результат должен быть “-15”, за которым следуют все 1-цы. (Используя циклическое вычитание, -15 – это 4294967281u32
.)
Теперь, когда у нас есть кандидаты, давайте начнем их оценивать.
Правило 5: Используйте Godbolt и ИИ для понимания ассемблера вашего кода, даже если вы не знаете язык ассемблера.
Мы будем оценивать кандидатов двумя способами. Во-первых, в этом правиле мы рассмотрим ассемблерный код, сгенерированный из нашего кода. Во-вторых, в правиле 7 мы измерим скорость работы кода.
Не беспокойтесь, если вы не знаете язык ассемблера, вы все равно можете получить что-то, заглянув в него.
Самый простой способ увидеть сгенерированный ассемблерный код – это использовать Compiler Explorer, также известный как Godbolt. Он работает лучше всего с небольшими фрагментами кода, которые не используют внешние крейты. Выглядит это так:
Ссылаясь на числа на рисунке выше, выполните следующие действия, чтобы использовать Godbolt:
- Откройте godbolt.org в вашем веб-браузере.
- Добавьте новый исходный редактор.
- Выберите Rust в качестве вашего языка.
- Вставьте интересующий вас код. Сделайте интересующие вас функции публичными (
pub fn
). Не включайте main или ненужные функции. Инструмент не поддерживает внешние крейты. - Добавьте новый компилятор.
- Установите версию компилятора на nightly.
- Установите параметры (пока) как
-C opt-level=3 -C target-feature=+avx512f.
- Если возникнут ошибки, посмотрите вывод.
- Если вы хотите поделиться или сохранить состояние инструмента, нажмите “Share”
Изображение выше показывает, что Splat2 и Sizzle полностью идентичны, поэтому мы можем исключить Sizzle из рассмотрения. Если вы откроете копию моей сессии Godbolt, вы также увидите, что большинство функций компилируются приблизительно в одно и то же количество ассемблерных операций. Исключения составляют Regular – который намного длиннее – и Splat0 – который включает раннюю проверку.
В ассемблере регистры размером 512 бит начинаются с ZMM. Регистры размером 256 бит начинаются с YMM. Регистры размером 128 бит начинаются с XMM. Если вы хотите лучше понять сгенерированный ассемблерный код, используйте инструменты ИИ для создания аннотаций. Например, здесь я спрашиваю у чата Bing о Splat2:
Попробуйте разные настройки компилятора, включая -C target-feature=+avx2
, а затем полное отключение параметра target-feature
.
Меньшее количество ассемблерных операций не всегда означает быстродействие. Однако просмотр ассемблерного кода позволяет нам проверить, что компилятор хотя бы пытается использовать операции SIMD, встраивать константные ссылки и т.д. Также, как в случае с Splat1 и Swizzle, иногда это позволяет нам узнать, когда два кандидата идентичны.
Возможно, вам понадобятся возможности дисассемблирования, которые превосходят возможности Godbolt, например, возможность работать с кодом, использующим внешние крейты. B3NNY рекомендовал мне инструмент
cargo-show-asm
из пакета Cargo. Я попробовал его и нашел его достаточно простым в использовании.
Крейт range-set-blaze
должен работать с целочисленными типами, превышающими u32
. Кроме того, нам нужно выбрать количество LANES, но у нас нет оснований считать, что 16 LANES всегда лучший вариант. Чтобы удовлетворить эти требования, в следующем правиле мы обобщим код.
Правило 6: Обобщайте для всех типов и LANE с помощью инлайн-обобщений (и когда это не работает), макросов и (когда это не работает), трейтов.
Давайте сначала обобщим Splat1 с помощью обобщений.
#[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}
Прежде всего обратите внимание на атрибут #[inline]
. Он важен для эффективности и мы будем использовать его практически в каждой из этих маленьких функций.
Вышеопределенная функция is_consecutive_splat1_gen
выглядит отлично, за исключением того, что ей нужен второй вход, называемый comparison_value
, которое мы еще не определили.
Если вам не нужен обобщенный постоянный параметр
comparison_value
, я вас завидую. Вы можете пропустить следующее правило, если хотите. Точно так же, если вы это читаете в будущем и создание обобщенного постоянного параметраcomparison_value
настолько легко, как дать вашему личному роботу выполнить все домашние дела, то я вдвойне вас завидую.
Мы можем попробовать создать обобщенное и постоянное значение comparison_value_splat_gen
. К сожалению, ни From<usize>
, ни альтернативный T::One
не являются постоянными, поэтому это не работает:
// НЕ РАБОТАЕТ, ПОТОМУ ЧТО From<usize> не является постояннымpub 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)}
Макросы – последнее убежище негодяев. Итак, давайте используем макросы:
#[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) } };}
Это позволяет запускать на определенном типе элементов и на всех количествах LANE (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));
К сожалению, это все равно недостаточно для range-set-blaze
. Он должен работать с элементами всех типов (не только с одним) и (желательно) всех LANES (не только с одним).
К счастью, есть обходное решение, которое снова зависит от макросов. Оно также использует факт, что нам нужно поддерживать ограниченный список типов, а именно: i8
, i16
, i32
, i64
, isize
, u8
, u16
, u32
, u64
и usize
. Если вам также нужна поддержка f32
и f64
, это тоже хорошо.
С другой стороны, если вам нужна поддержка
i128
иu128
, вам может не повезти. Модульcore::simd
их не поддерживает. Мы увидим в Правиле 8, какrange-set-blaze
обходит это с потерей производительности.
Обходное решение определяет новый трейт, называемый IsConsecutive
. Затем мы используем макрос (который вызывает макрос, который вызывает макрос) для реализации этого трейта для 10 интересующих типов.
pub trait IsConsecutive { fn is_consecutive<const N: usize>(chunk: Simd<Self, N>) -> bool where Self: SimdElement, Simd<Self, N>: Sub<Simd<Self, N>, Output = Simd<Self, N>>, LaneCount<N>: SupportedLaneCount;}macro_rules! impl_is_consecutive { ($type:ty) => { impl IsConsecutive for $type { #[inline] // очень важно fn is_consecutive<const N: usize>(chunk: Simd<Self, N>) -> bool where Self: SimdElement, Simd<Self, N>: Sub<Simd<Self, N>, Output = Simd<Self, N>>, LaneCount<N>: 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);
Теперь мы можем вызывать полностью обобщенный код (Rust Playground):
// Работает на i32 и 16 laneslet 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!(IsConsecutive::is_consecutive(a));assert!(!IsConsecutive::is_consecutive(ninety_nines));// Работает на i8 и 64 laneslet a: Simd<i8, 64> = black_box(Simd::from_array(array::from_fn(|i| 10 + i as i8)));let ninety_nines: Simd<i8, 64> = black_box(Simd::from_array([99; 64]));assert!(IsConsecutive::is_consecutive(a));assert!(!IsConsecutive::is_consecutive(ninety_nines));
С помощью этой техники мы можем создавать несколько потенциальных алгоритмов, которые полностью обобщены по типу и LANES. Дальше пришло время выполнить бенчмарки и посмотреть, какие алгоритмы являются самыми быстрыми.
Это первые шесть правил добавления SIMD-кода в Rust. В предстоящей части 2 мы рассмотрим правила с 7 по 9. Эти правила касаются выбора алгоритма и настройки LANES. Мы также рассмотрим, как интегрировать SIMD-операции в существующий код и (что важно) сделать это опциональным. В завершение части 2 мы обсудим, когда и/или почему следует использовать SIMD, а также предложим идеи для улучшения опыта работы с SIMD в Rust. Часть 2 будет опубликована скоро. Надеюсь на ваше участие.
Пожалуйста, подпишитесь на Карла в VoAGI. Я пишу о научном программировании на Rust и Python, машинном обучении и статистике. Обычно я пишу около одной статьи в месяц.