Девять правил для ускорения SIMD вашего кода на Rust (Часть 1)

Девять правил, чтобы ускорить SIMD вашего кода на Rust (Часть 1)

Общие уроки улучшения восприятия данных в ящике range-set-blaze на 7 раз

Краб делегирующий вычисления маленьким крабам - Источник: https://openai.com/dall-e-2/. Все остальные рисунки от автора.

Благодаря Бену Лихтману (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 операции? Да!

См. правило 2 предыдущей статьи для определения “скучных” чисел. Кроме того, что происходит, если числа не являются “скучными”? RangeSetBlaze работает в 2-3 раза медленнее чем HashSet.

На скучных числах, метод RangeSetBlaze::from_slice, основанный на SIMD операциях, в 7 раз быстрее, чем RangeSetBlaze::from_iter. Это делает его более чем в 200 раз быстрее, чем HashSet::from_iter. (Когда числа не являются скучными, он всё равно на 2-3 раза медленнее, чем HashSet.)

В процессе реализации этого ускорения я выяснил девять правил, которые могут помочь вам ускорить ваши проекты с помощью SIMD операций.

Правила такие:

  1. Используйте ночную сборку Rust и core::simd, экспериментальный модуль SIMD в Rust.
  2. CCC: Проверьте, контролируйте и выберите возможности SIMD вашего компьютера.
  3. Изучайте core::simd, но выборочно.
  4. Придумывайте возможные алгоритмы.
  5. Используйте Godbolt и AI для понимания сборки вашего кода, даже если вы не знаете ассемблерный язык.
  6. Обобщайте для всех типов и ЛЕЙНОВ с помощью встроенных обобщений, (и когда это не работает) макросов и (когда это не работает) трейтов.

См. предстоящую вторую часть для этих правил:

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:

  1. Откройте godbolt.org в вашем веб-браузере.
  2. Добавьте новый исходный редактор.
  3. Выберите Rust в качестве вашего языка.
  4. Вставьте интересующий вас код. Сделайте интересующие вас функции публичными (pub fn). Не включайте main или ненужные функции. Инструмент не поддерживает внешние крейты.
  5. Добавьте новый компилятор.
  6. Установите версию компилятора на nightly.
  7. Установите параметры (пока) как -C opt-level=3 -C target-feature=+avx512f.
  8. Если возникнут ошибки, посмотрите вывод.
  9. Если вы хотите поделиться или сохранить состояние инструмента, нажмите “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, машинном обучении и статистике. Обычно я пишу около одной статьи в месяц.