SIMD найти первый элемент больше x

Я учусь использовать SIMD в C++, и это моя попытка реализовать версию SIMD «найти первый элемент, больший или равный X».

Мои вопросы:

  • Можно ли заменить reinterpret_cast каким-нибудь внутренним?
  • Лучше ли убедиться, что содержимое reinterpret_cast выровнено, начиная аналогичным образом до конца и используя логику, отличную от simd, для элементов до первого выровненного короткого фрагмента?
// reference function of desired behavior
inline auto find_first_greater_or_equal_simple(const std::vector<short>& v, short insert)
{
    for (auto it = v.begin(), end = v.end(); it != end; ++it)
    {
        if (*it >= insert) return it;
    }
    return v.end();
}

// simd version
inline auto find_first_greater_or_equal_simd(const std::vector<short>& v, short insert)
{
    const __m512i target = _mm512_set1_epi16(insert);

    auto it = v.begin();
    const auto end = v.end();
    const auto end_simd = it + 32 * ((end - it) / 32);

    __mmask32 cmpge_mask{};

    for (; it != end_simd &&
        !(cmpge_mask = _mm512_cmpge_epi16_mask(*reinterpret_cast<const __m512i*>(&*it), target));
        it += 32)
    {}

    if (cmpge_mask)
    {
        unsigned long local_idx;
        _BitScanForward(&local_idx, cmpge_mask); // todo: __builtin_ctz 
        return it + local_idx;
    }

    for (; it != end; ++it)
    {
        if (*it >= insert) return it;
    }

    return v.end();
}

Учитывая AVX-512, у вас определенно есть ИМТ2 для _tzcnt_u32, поэтому используйте его вместо встроенных функций, специфичных для компилятора, для BSF. Или используйте C++20 std::countr_zero

Peter Cordes 21.02.2024 02:59

@PeterCordes Компилятор может генерировать tzcnt для _BitScanForward, если это позволяет целевой процессор (и он это делает, когда включен AVX-512). gcc делает именно это, например, для __builtin_ctz.

Andrey Semashev 21.02.2024 12:06

@AndreySemachev: Нет, _BitScanForward является свойством bsf, в частности (felixcloutier.com/x86/bsf ). Как обычно, MSVC воспринимает это буквально, не используя tzcnt с -arch:AVX2 или -arch:AVX512 godbolt.org/z/K9T8Tc9rb (-arch:AVX2 MSVC действительно подразумевает BMI1+2, как вы можете видеть из этого, используя tzcnt для std::countr_zero. MSVC лечит «AVX2» так же, как GCC/Clang обрабатывает -march=x86-64-v3, но MSVC не оптимизирует внутренние функции.) Конечно, GCC или Clang оптимизируют __builtin_ctz до наилучшего доступного asm, но это не переносится на MSVC.

Peter Cordes 21.02.2024 17:39

@PeterCordes В спецификации _BitScanForward не упоминается bsf. Фактически, встроенная функция также поддерживается в ARM. Тот факт, что MSVC не генерирует tzcnt для него, когда это возможно, является вопросом качества и может быть сообщен разработчикам компилятора как пропущенная оптимизация.

Andrey Semashev 22.02.2024 12:23

@AndreySemachev: Ой, странно. Intel документирует это как встроенную функцию bsf. intel.com/content/www/us/en/docs/intrinsics-guide/… . К счастью, это все спорный вопрос, если доступен C++20 std::countr_zero или если вы уже используете функции x86-64-v3 и можете использовать _tzcnt_u32. Или, если вы знаете, что ваш ввод не равен нулю, вы можете использовать _tzcnt_u32, даже если он может работать как bsf, если вы используете MSVC или classic-ICC, которые позволяют вам использовать встроенные функции для расширений, которые вы не включили.

Peter Cordes 22.02.2024 18:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
206
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Обычно я просто использую математику указателей с внутренними функциями SIMD или &v[i], а не итераторами .begin()/.end(). Использование SIMD зависит от непрерывного хранения элементов, поэтому мы не получаем какой-либо общности для коллекций, в которых итератор не эквивалентен const int*.

Ваш reinterpret_castэквивалентен_mm512_load_si512(it), что является версией, требующей выравнивания. (В оптимизированных сборках компилятор складывает нагрузку в операнд источника памяти для vpcmpd, который не обеспечивает выравнивание.) Если ваш указатель не выровнен, используйте _mm512_loadu_si512.

Для векторов шириной менее 512 встроенные функции загрузки/сохранения __m128i/ __m256i имеют менее удобные определения, которые не принимают аргументы void*, поэтому вам понадобится _mm256_loadu_si256( (const __m256i*) ptr_expression ). Intel перешла на void* для встроенных функций, представленных примерно с 2015 года, которые включают в себя все новое с AVX-512, но не изменили задним числом старые встроенные функции, поэтому нам все еще нужны эти шумные приведения повсюду для работы с целочисленными векторами.


Да, особенно на процессорах Intel рекомендуется выравнивать указатели при использовании 512-битных векторов. В идеале вы можете просто использовать выровненный распределитель для вашего std::vector, чтобы данные всегда были выровнены. (Если вы всегда проверяете только начало std::vector, а не какую-то отправную точку в середине.) Или если ваши данные обычно выровнены, причем достаточно часто, чтобы не оправдать дополнительные затраты на выравнивание при запуске.

Но в этом случае можно очень дешево справиться с возможным несовпадением: проверьте первый вектор, затем выровняйте указатель. Первый выровненный вектор будет частично перекрывать первый вектор, если он не был выровнен, но это нормально; вам не нужно избегать двойной проверки одного и того же элемента, поскольку вы не суммируете или что-то в этом роде. Этот трюк также работает для циклов копирования и изменения, таких как dst[i] = f(src[i]), где вы никогда не вызываете его с dst==src для работы на месте.

Тот же прием можно использовать для обработки конца массива, если общий размер массива равен хотя бы одному вектору. Если небольшие массивы не являются редкостью в вашем предполагаемом варианте использования, рассмотрите для очистки 128-битные или 256-битные векторы, что потенциально позволит использовать этот трюк. Или дополните или выровняйте массивы, чтобы их можно было безопасно читать после конца, маскируя потенциальные совпадения (установленные биты маски) из прошлого, где массив должен был заканчиваться. (бжи для этого годится, _bzhi_u32)

Учитывая AVX-512, у вас определенно есть ИМТ2 для _tzcnt_u32, поэтому используйте его вместо встроенных функций, специфичных для компилятора, для BSF. Или используйте C++20 std::countr_zero

Другие вопросы по теме