«Безопасная» SIMD-арифметика на выровненных векторах нечетного размера? - PullRequest
7 голосов
/ 08 октября 2019

Допустим, у меня есть какая-то 16-байтовая выровненная структура, которая просто оборачивает массив 3xFloat32:

#[repr(C, align(16))]
pub struct Vector(pub [f32; 3]);

Теперь я хочу разделить два его экземпляра, например:

use core::arch::x86_64;

let a = Vector([1f32, 2f32, 3f32]);
let b = Vector([4f32, 5f32, 6f32]);
let mut q = Vector([0f32, 0f32, 0ff32]);

unsafe {
    let a1 = x86_64::_mm_load_ps(a.0.as_ptr());
    let b1 = x86_64::_mm_load_ps(b.0.as_ptr());
    let q1 = x86_64::_mm_div_ps(a1, b1);
    x86_64::_mm_store_ps(q.0.as_mut_ptr(), q1);
}

Делает, но есть проблема: 4-й элемент содержит мусор, который, помимо прочего, может сигнализировать NaN. И если флаги определенных исключений не маскируются, SIGFPE будет запущен. Я хочу как-то избежать этого, не подавляя сигнал полностью. Т.е. я хочу либо заставить замолчать только на 4-й паре элементов, либо поставить туда какие-то вменяемые значения. Каков самый лучший и быстрый способ сделать это? Или, может быть, есть лучший подход в целом?

Ответы [ 2 ]

5 голосов
/ 08 октября 2019

Как правило, никто не снимает маскировку с исключений FP, в противном случае вам понадобятся перестановки, например, для дублирования одного из элементов, чтобы верхний элемент делил то же самое деление, что и один из других элементов. Или есть другая известная безопасная вещь.

Возможно, вам удастся избежать только перетасовки делителя, если вы можете предположить, что в этом элементе дивиденд не равен NaN.

С AVX512 вы можете подавитьисключения для элемента, использующего маскирование нуля, но до этого момента такой функции не было. Также AVX512 позволяет вам переопределить режим округления + Подавить все исключения (SAE) без маскировки, чтобы вы могли сделать ближайший, даже явный, для получения SAE. Но это подавляет исключения для всех элементов.


Серьезно, не включайте исключения FP. Компиляторы едва ли / не знают, как оптимизировать безопасным способом, если количество исключений является видимым побочным эффектом. например, GCC -ftrapping-math включен по умолчанию, но он не работает.

Я бы не подумал, что LLVM лучше;строгий FP по умолчанию, вероятно, все еще выполняет оптимизацию, которая могла бы дать один SIGFPE, где источник поднял бы 2 или 4. Может быть, даже оптимизации, которые поднимают 0, когда источник поднял бы 1, или наоборот, как GCC, сломанный и почти бесполезный по умолчанию. 1013 *

Включение исключений FP может быть полезно для отладки, однако, если вы ожидаете, что никогда не будет каких-либо исключений определенного типа. Но вы, вероятно, можете справиться со случайными ложными срабатываниями из инструкции SIMD, проигнорировав те с этим адресом источника.


Если есть компромисс между производительностью и корректностью исключений, большинство пользователей библиотекискорее, это максимизирует производительность.

Даже очистка, а затем проверка липких маскированных флагов FP с fenv выполняется редко и требует использования контролируемых обстоятельств. У меня не было бы никаких ожиданий для вызова библиотечной функции, особенно если бы она не использовала SIMD.


Избегайте субнормалей в элементе мусора

Вы можетеполучить замедления от субнормалей (также известных как денормали), если в MXCSR не установлены FTZ и DAZ. (т. е. обычный случай, если вы не скомпилировали (эквивалент Rust) -ffast-math.)

Создание NaN или + -Inf не требует дополнительного времени для типичного оборудования x86 с SSE/ AVX инструкции. (Интересный факт: NaN тоже медленный, с наследием математики x87 даже на современных HW). Так что можно _mm_or_ps с результатом cmpps создать NAN в некоторых элементах вектора, например, перед математической операцией. Или _mm_and_ps, чтобы создать несколько нулей в делителе перед делением.

Но будьте осторожны с тем, какой мусор находится в вашем заполнении, потому что это может привести к ложным субнормалам. 0.0 и NaN (все), как правило, всегда безопасны.


Обычно избегайте горизонтальных вещей с SIMD. SIMD vec! = Геометрия vec.

Использование только 3 из 4 элементов вектора SIMD обычно является плохой идеей , поскольку обычно это означает, что вы используете один вектор SIMD для храненияодин вектор геометрии, вместо трех векторов с 4 x координатами, 4 y координатами и 4 z координатами.

Перестановки / горизонтальные вещи в основном стоят дополнительных инструкций (за исключением широковещательных загрузок скаляраэто уже было в памяти), но вам часто нужно много тасовок, если вы используете SIMD таким образом. Есть случаи, когда вы не можете векторизовать множество вещей, но вы можете получить ускорение с помощью SIMD.

Если вы просто используете этот частичный вектор для оставшихся элементов операции нечетного размера, тогда здорово , один частичный вектор намного лучше, чем 3 скалярных итерации. Но большинство людей, спрашивающих об использовании только 3 из 4 векторных элементов, спрашивают, потому что они используют SIMD неправильно, например, добавление геометрического вектора в качестве вектора SIMD все еще дешево, но точечный продукт нуждается в тасованиях. См. https://deplinenoise.wordpress.com/2015/03/06/slides-simd-at-insomniac-games-gdc-2015/ для некоторых хороших вещей о том, как правильно использовать SIMD (SoA против AoS и т. Д.). Если вы уже знаете об этом и просто используете 3-элементные векторы для случая нечетного угла, а не для большей части работы, то это нормально.

Заполнение кратным ширине вектора обычно отлично подходит для нечетногоразмеры, но другой вариант для некоторых алгоритмов - это конечный вектор без выравнивания, который заканчивается в конце ваших данных. Хранилище с частичным перекрытием - это хорошо, если только это не алгоритм на месте, и вам не нужно беспокоиться о том, чтобы не выполнить элемент дважды. (Или о стойках пересылки магазина даже для идемпотентных операций, таких как маскирование и зажим AND).


Получение нулей бесплатно

Если у вас осталось только 2 float элементов,movsd load будет загружать + нулевое расширение в регистр XMM. Вы могли бы также заставить компилятор сделать это вместо movaps.

В противном случае, если перемешать 3 скаляра, insertps может обнулять элементы. Или вы могли знать нулевые старшие части регистров xmm из movss загрузок из памяти. Таким образом, использование 0.0 в качестве части инициализатора вектора из скаляра (например, C ++ _mm_set_ps()) может быть бесплатным для компилятора.

С AVX вы можете рассмотреть возможность использованиязамаскированная нагрузка, если вы беспокоитесь о том, что заполнение вызывает ненормальноеhttps://www.felixcloutier.com/x86/vmaskmov. Но это несколько медленнее, чем vmovaps. А замаскированные магазины намного дороже на AMD, даже на Ryzen.

4 голосов
/ 08 октября 2019

В Rust, как в C, sizeof всегда кратно alignof: необходимость, поскольку sizeof используется как шаг в массивах, а элементы массива должны быть правильно выровнены.

В результате, даже если для вашего struct вы используете только 12 байт, его sizeof равно 16 байтам с 4 байтами "заполнения".

Поэтому я бы предложилочень прагматичное исправление: собственные прокладки. Вместо того, чтобы сделать внутреннее содержимое struct видимым, дайте ему конструктор и метод доступа ... и добавьте его в 16 байтов со значением 1.0.

#[repr(C, align(16))]
pub struct Vector([f32; 4]);

impl Vector {
    pub fn new(v: [f32; 3]) -> Vector {
        Vector([v[0], v[1], v[2], 1.0])
    }

    pub fn pad(&mut self, pad: f32) { self.0[3] = pad; }

    pub fn as_ptr(&self) -> *const f32 { self.0.as_ptr() }
}

Затем вы можете выполнять операции суверенность, что байт мусора не используется.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...