Умножение 64-битных целых чисел с использованием производительности AVX2 и AVX-512

avatar
dave_thenerd
9 августа 2021 в 03:23
301
0
0

Я пытаюсь определить, является ли встроенная последовательность _mm512_mullox_epi64 (основа AVX-512) значительно более медленной, чем аппаратно-реализованная встроенная последовательность _mm512_mullo_epi64 (AVX-512 Double-Word и Quad-Word ISA).

_mm512_mullo_epi64 вызовет исключение «Недопустимая инструкция» на оборудовании с AVX-512, но без расширений набора инструкций DWQW.

У меня нет процессора с поддержкой AVX-512, и попытка провести бенчмаркинг с помощью godbolt дает очень противоречивые результаты. Мой код также не компилируется с использованием быстрой скамьи, поскольку в настоящее время вы не можете передавать параметры компилятора, такие как -mavx512dq

.

Мне также интересно узнать, есть ли хороший вариант для использования AVX2, поскольку нет встроенной функции умножения 64-битных целых чисел на AVX2.

Использование _mm256_mul_pd с приведением часто приводит к неправильным результатам, когда произведение находится в пределах границ int64_t, но за пределами 64-битного двойного числа.

Вот мой тестовый код, если вам интересно:

 #include "immintrin.h"
 #include <cstdint>
 #include <array>
 #include <algorithm>
 #include <numeric>
 #include <iostream>
 #include <chrono>

 std::array<int64_t, 1000000> arr1;
 std::array<int64_t, 1000000> arr2;
 std::array<int64_t, 1000000> arr3;

 class Timer
{
public:
    Timer()
    {
        start = std::chrono::high_resolution_clock::now();
    }//End of constructor

    Timer(Timer const&) = delete;
    Timer& operator=(Timer const&) = delete;
    Timer(Timer&&) = delete;
    Timer& operator=(Timer&&) = delete;

    ~Timer()
    {
        end = std::chrono::high_resolution_clock::now();
        std::chrono::high_resolution_clock::duration d = end - start;
        std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(d).count() <<      "ns\n";
    }//End of destructor

private:
    std::chrono::high_resolution_clock::time_point start;
    std::chrono::high_resolution_clock::time_point end;
};//End of class Timer

 template<uint64_t SIZE1, uint64_t SIZE2, uint64_t SIZE3>
 void mul_f(const std::array<int64_t, SIZE1>& src, const std::array<int64_t, SIZE2>& src2,           std::array<int64_t, SIZE3>& dest)
 {
__m512i _src1;
__m512i _src2;
__m512i _dest;

for(uint64_t i = 0; i < SIZE3; i+=8)
{
    if((i + 8) > SIZE3)
    {
        break;
    }

    _src1 = _mm512_load_epi64(&src[i]);
    _src2 = _mm512_load_epi64(&src2[i]);

    _dest = _mm512_mullox_epi64(_src1, _src2);

    _mm512_store_epi64(&dest[i], _dest);        
     }
 }

 template<uint64_t SIZE1, uint64_t SIZE2, uint64_t SIZE3>
 void mul_dq(const std::array<int64_t, SIZE1>& src, const std::array<int64_t, SIZE2>& src2, std::array<int64_t, SIZE3>& dest)
 {
__m512i _src1;
__m512i _src2;
__m512i _dest;

for(uint64_t i = 0; i < SIZE3; i+=8)
{
    if((i + 8) > SIZE3)
    {
        break;
    }

    _src1 = _mm512_load_epi64(&src[i]);
    _src2 = _mm512_load_epi64(&src2[i]);

    _dest = _mm512_mullo_epi64(_src1, _src2);

    _mm512_store_epi64(&dest[i], _dest);        
}
 }

 template<uint64_t SIZE1, uint64_t SIZE2, uint64_t SIZE3>
 void mul_avx2(const std::array<int64_t, SIZE1>& src, const std::array<int64_t, SIZE2>& src2, std::array<int64_t, SIZE3>& dest)
 {
__m256i _src1;
__m256i _src2;
__m256i _dest;

for(uint64_t i = 0; i < SIZE3; i+=4)
{
    if((i + 4) > SIZE3)
    {
        break;
    }

    _src1 = _mm256_load_si256((__m256i*)&src[i]);
    _src2 = _mm256_load_si256((__m256i*)&src2[i]);

    int64_t d[4] = {};
    for (size_t x = 0; x != 4; ++x)
    {
 #ifdef _WIN32
        d[x] = _src1.m256i_i64[x] * _src2.m256i_i64[x];
 #else
        d[x] = _src1[x] * _src2[x];
 #endif
    }//End for

    _dest = _mm256_load_si256((__m256i*) &d);

    _mm256_store_si256((__m256i*)&dest[i], _dest);        
}
 }



 int main()
 {
std::iota(arr1.begin(), arr1.end(), 5);
std::iota(arr2.begin(), arr2.end(), 2);

{   
    Timer();
     mul_f(arr1, arr2, arr3);
}

{
    Timer();
    mul_dq(arr1, arr2, arr3);
}

{
    Timer();
    mul_avx2(arr1, arr2, arr3);
}

return static_cast<int>(arr3[0]);
 }

Заранее благодарим за помощь.

Источник
Peter Cordes
9 августа 2021 в 03:39
2

аппаратное обеспечение с AVX-512, но без DWQW — для справки, единственными процессорами AVX-512 (пока что) без AVX-512DQ являются снятые с производства карты Xeon Phi. (en.wikipedia.org/wiki/AVX-512#CPUs_with_AVX-512). Я предполагаю, что если AMD когда-либо реализует AVX-512, они будут включать AVX-512DQ, как это делает Intel в своих основных процессорах. Вы можете прогнозировать производительность с помощью инструментов статического анализа, таких как LLVM-MCA, или с помощью uops.info/table.html. (Размер 64-битного элемента vpmullq занимает 3 мкп на инструкцию в текущей версии Intel, по сравнению с 2 для vpmulld или 1 для vpmuludq.)

Nate Eldredge
9 августа 2021 в 03:42
3

mullox реализован с использованием 32-битного умножения и длинного умножения "школьного типа", поэтому кажется, что это будет значительно медленнее: godbolt.org/z/We88796Kn

Peter Cordes
9 августа 2021 в 03:50
1

Также связано: вы можете делать вещи с повышенной точностью, используя множители мантиссы с точностью double с умной битовой манипуляцией битовых шаблонов FP и FMA. Но если 64-битной точности достаточно, а одного double (52/53-бит) недостаточно, то да, 64-битное целое может быть подходящим вариантом, даже если это неудобно и медленно умножается.

Peter Cordes
9 августа 2021 в 03:51
2

Самый быстрый способ умножения массива int64_t? имеет векторизованную вручную версию AVX2, которая должна быть значительно лучше, чем извлечение в скаляр, особенно между другими векторными операциями.

dave_thenerd
14 августа 2021 в 06:14
0

Хорошо, спасибо, ребята! Я не уверен, буду ли я обновлять свой код для использования _mm512_mullo_epi64, но Xeon Phi все еще довольно новый, и если бы это был я, я бы хотел, чтобы мой процессор стоимостью более 2000 долларов не устарел через 5 лет.

Ответы (0)