_mm256_rem_epu64 intrinsik tidak ditemukan dengan GCC 10.3.0

Saya mencoba menulis ulang perkalian matriks uint64_t 2x2 berikut dengan instruksi AVX-512, tetapi GCC 10.3 tidak menemukan _mm256_rem_epu64 intrinsik.

#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);
}

Apakah mungkin untuk mengaktifkan _mm256_rem_epu64 atau jika tidak, cara lain untuk menghitung pengingat dengan instruksi SIMD?


person bobeff    schedule 07.07.2021    source sumber
comment
Itu fungsi SVML, bukan intrinsik untuk a Instruksi CPU. Jangan gunakan pembagian runtime, gunakan invers perkalian. (seperti dalam Apa cara tercepat untuk menghasilkan file teks 1 GB yang berisi angka acak? seperti yang saya lakukan untuk bilangan bulat 16-bit . Mengapa GCC menggunakan perkalian dengan bilangan ganjil dalam penerapan pembagian bilangan bulat? adalah trik umum)   -  person Peter Cordes    schedule 07.07.2021
comment
Saya pikir AVX-512 memiliki perkalian setengah tinggi 64-bit, tapi mungkin itu hanya dengan AVX512-IFMA52 (felixcloutier.com/x86/vpmadd52huq). Untuk perkalian 64-bit, saya hanya melihat vpmullq seperti yang Anda gunakan. Mungkin Anda bisa menggunakan FP presisi ganda dan invers FP, atau pembagian FP, jika itu cukup tepat.   -  person Peter Cordes    schedule 07.07.2021


Jawaban (1)


Seperti yang disebutkan Peter Cordes di komentar, _mm256_rem_epu64 adalah fungsi SVML. Kebanyakan kompiler tidak mendukung SVML; AFAIK sebenarnya hanya ICC yang melakukannya, tetapi clang dapat dikonfigurasi untuk menggunakannya juga.

Satu-satunya implementasi SVML lain yang saya ketahui ada di salah satu proyek saya, SIMDe. Dalam hal ini, karena Anda menggunakan GCC 10.3, implementasi _mm256_rem_epu64 akan menggunakan ekstensi vektor, jadi kode dari SIMDe pada dasarnya akan sama dengan sesuatu seperti:

#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));
}

Dalam hal ini, GCC dan clang akan menskalakan operasi (lihat Compiler Explorer), sehingga kinerja akan meningkat menjadi sangat buruk, terutama mengingat betapa lambat div instruksi adalah.

Oleh karena itu, karena Anda menggunakan konstanta waktu kompilasi, kompiler harus dapat mengganti pembagian dengan perkalian dan pergeseran, sehingga performa akan lebih baik, namun kita dapat melakukan lebih banyak lagi dengan menggunakan libdivide.

Libdivide biasanya menghitung nilai ajaib saat runtime, tetapi libdivide_u64_t< /a> strukturnya sangat sederhana dan kita bisa melewati langkah libdivide_u64_gen dan menyediakan struct pada waktu kompilasi:

__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);
}

Nah kalau bisa pakai AVX-512VL + AVX-512DQ ada fungsi perkalian 64 bit (_mm256_mullo_epi64). Jika Anda dapat menggunakannya mungkin itu cara yang tepat:

__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)
            )
        );
}

(atau di Compiler Explorer, dengan LLVM-MCA)

Jika Anda tidak memiliki AVX-512DQ+VL, Anda mungkin ingin kembali menggunakan ekstensi vektor:

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
        )
    );
}

(di Compiler Explorer)

Semua ini belum teruji, namun dengan asumsi saya tidak melakukan kesalahan bodoh apa pun, hal ini seharusnya relatif cepat.

Jika Anda benar-benar ingin menghilangkan ketergantungan libdivide, Anda dapat melakukan operasi tersebut sendiri, tetapi saya tidak melihat alasan bagus untuk tidak menggunakan libdivide jadi saya akan membiarkannya sebagai latihan untuk orang lain.

person nemequ    schedule 07.07.2021