_mm256_rem_epu64 встроенный не найден с GCC 10.3.0

Я пытаюсь переписать следующее умножение матриц uint64_t 2x2 с помощью AVX-512 инструкций, но GCC 10.3 не находит _mm256_rem_epu64 встроенных.

#include <cstdint>
#include <immintrin.h>

constexpr uint32_t LAST_9_DIGITS_DIVIDER = 1000000000;

void multiply(uint64_t f[2][2], uint64_t m[2][2])
{
  uint64_t x = (f[0][0] * m[0][0] + f[0][1] * m[1][0]) % LAST_9_DIGITS_DIVIDER;
  uint64_t y = (f[0][0] * m[0][1] + f[0][1] * m[1][1]) % LAST_9_DIGITS_DIVIDER;
  uint64_t z = (f[1][0] * m[0][0] + f[1][1] * m[1][0]) % LAST_9_DIGITS_DIVIDER;
  uint64_t w = (f[1][0] * m[0][1] + f[1][1] * m[1][1]) % LAST_9_DIGITS_DIVIDER;

  f[0][0] = x;
  f[0][1] = y;
  f[1][0] = z;
  f[1][1] = w;
}

void multiply_simd(uint64_t f[2][2], uint64_t m[2][2])
{
  __m256i v1 = _mm256_set_epi64x(f[0][0], f[0][0], f[1][0], f[1][0]);
  __m256i v2 = _mm256_set_epi64x(m[0][0], m[0][1], m[0][0], m[0][1]);
  __m256i v3 = _mm256_mullo_epi64(v1, v2);

  __m256i v4 = _mm256_set_epi64x(f[0][1], f[0][1], f[1][1], f[1][1]);
  __m256i v5 = _mm256_set_epi64x(m[1][0], m[1][1], m[1][0], m[1][1]);
  __m256i v6 = _mm256_mullo_epi64(v4, v5);

  __m256i v7 = _mm256_add_epi64(v3, v6);
  __m256i div = _mm256_set1_epi64x(LAST_9_DIGITS_DIVIDER);
  __m256i v8 = _mm256_rem_epu64(v7, div);
  _mm256_store_epi64(f, v8);
}

Можно ли как-то включить _mm256_rem_epu64 или если нет, то как-то иначе вычислить напоминание с SIMD-инструкциями?


person bobeff    schedule 07.07.2021    source источник
comment
Это функция SVML, а не встроенная для Инструкция процессора. Не используйте деление во время выполнения, используйте мультипликативную инверсию. (как в Какой самый быстрый способ создать текстовый файл размером 1 ГБ, содержащий случайные цифры? как я сделал для 16-битных целых чисел , Почему GCC использует умножение на странное число при реализации целочисленного деления? это общий прием)   -  person Peter Cordes    schedule 07.07.2021
comment
Я думал, что AVX-512 имеет 64-битное умножение high-half, но, возможно, это только с AVX512-IFMA52 (felixcloutier.com/x86/vpmadd52huq). Для 64-битного умножения я вижу только vpmullq, как вы используете. Возможно, вы можете использовать FP с двойной точностью и обратное FP или деление FP, если это достаточно точно.   -  person Peter Cordes    schedule 07.07.2021


Ответы (1)


Как упомянул Питер Кордес в комментариях, _mm256_rem_epu64 — это функция SVML. Большинство компиляторов не поддерживают SVML; Насколько я знаю, на самом деле это делает только ICC, но clang также можно настроить для его использования.

Единственная другая реализация SVML, о которой я знаю, находится в одном из моих проектов, SIMDe. В этом случае, поскольку вы используете GCC 10.3, реализация _mm256_rem_epu64 будет использовать векторные расширения, поэтому код из SIMDe будет в основном таким же, как что-то вроде:

#include <immintrin.h>
#include <stdint.h>

typedef uint64_t u64x4 __attribute__((__vector_size__(32)));

__m256i
foo_mm256_rem_epu64(__m256i a, __m256i b) {
    return (__m256i) (((u64x4) a) % ((u64x4) b));
}

В этом случае и GCC, и clang масштабируют операцию (см. Проводник компилятора), поэтому производительность снижается. быть довольно плохим, особенно учитывая насколько медленный div инструкция есть.

Тем не менее, поскольку вы используете константу времени компиляции, компилятор должен иметь возможность заменить деление умножением и сдвигом, поэтому производительность будет лучше, но мы можем выжать больше, используя libdivide.

Libdivide обычно вычисляет магическое значение во время выполнения, но libdivide_u64_t< /a> очень проста, и мы можем просто пропустить шаг libdivide_u64_gen и предоставить структуру во время компиляции:

__m256i div_by_1000000000(__m256i a) {
    static const struct libdivide_u64_t d = {
        UINT64_C(1360296554856532783),
        UINT8_C(93)
    };
    return libdivide_u64_do_vec256(a, &d);
}

Теперь, если вы можете использовать AVX-512VL + AVX-512DQ, есть функция 64-битного умножения (_mm256_mullo_epi64). Если вы можете использовать это, вероятно, это правильный путь:

__m256i rem_1000000000(__m256i a) {
    static const struct libdivide_u64_t d = {
        UINT64_C(1360296554856532783),
        UINT8_C(93)
    };
    return
        _mm256_sub_epi64(
            a,
            _mm256_mullo_epi64(
                libdivide_u64_do_vec256(a, &d),
                _mm256_set1_epi64x(1000000000)
            )
        );
}

(или в Compiler Explorer с LLVM-MCA)

Если у вас нет AVX-512DQ+VL, вы, вероятно, захотите снова использовать векторные расширения:

typedef uint64_t u64x4 __attribute__((__vector_size__(32)));

__m256i rem_1000000000(__m256i a) {
    static const struct libdivide_u64_t d = {
        UINT64_C(1360296554856532783),
        UINT8_C(93)
    };
    u64x4 one_billion = { 1000000000, 1000000000, 1000000000, 1000000000 };
    return (__m256i) (
        (
            (u64x4) a) -
            (((u64x4) libdivide_u64_do_vec256(a, &d)) * one_billion
        )
    );
}

(в обозревателе компиляторов)

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

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

person nemequ    schedule 07.07.2021