Вопрос

Я хочу узнать больше об использовании СШЭ.

Какие существуют способы обучения, помимо очевидного чтения Руководства разработчиков программного обеспечения для архитектур Intel® 64 и IA-32?

В основном мне интересно работать с Встроенные функции GCC X86.

Это было полезно?

Решение

Во-первых, я не рекомендую использовать встроенные функции - они не переносимы (между компиляторами одной и той же арки).

Использовать внутренние качества, ССАГПЗ делает замечательную работу оптимизация встроенных функций SSE в еще более оптимизированный код.Вы всегда можете взглянуть на сборку и увидеть, как использовать весь потенциал SSE.

Внутренние функции просты — как и обычные вызовы функций:

#include <immintrin.h>  // portable to all x86 compilers

int main()
{
    __m128 vector1 = _mm_set_ps(4.0, 3.0, 2.0, 1.0); // high element first, opposite of C array order.  Use _mm_setr_ps if you want "little endian" element order in the source.
    __m128 vector2 = _mm_set_ps(7.0, 8.0, 9.0, 0.0);

    __m128 sum = _mm_add_ps(vector1, vector2); // result = vector1 + vector 2

    vector1 = _mm_shuffle_ps(vector1, vector1, _MM_SHUFFLE(0,1,2,3));
    // vector1 is now (1, 2, 3, 4) (above shuffle reversed it)
    return 0;
}

Использовать _mm_load_ps или _mm_loadu_ps для загрузки данных из массивов.

Конечно, вариантов гораздо больше, SSE действительно мощный и, на мой взгляд, относительно простой в освоении.

Смотрите также https://stackoverflow.com/tags/sse/info некоторые ссылки на руководства.

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

Поскольку вы запросили ресурсы:

Практическое руководство по использованию SSE с C++.:Хороший концептуальный обзор эффективного использования SSE с примерами.

Листинг MSDN внутренних компонентов компилятора:Комплексный справочник для всех ваших внутренних потребностей.Это MSDN, но почти все перечисленные здесь встроенные функции также поддерживаются GCC и ICC.

Страница Кристофера Райта на SSE:Краткий справочник по значениям кодов операций SSE.Я думаю, что руководства Intel могут выполнять ту же функцию, но это быстрее.

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

Я нахожу доктора.Руководства Агнера Фога по исследованиям и оптимизации очень ценны!У него также есть несколько библиотек и инструментов тестирования, которые я еще не пробовал.http://www.agner.org/optimize/

Шаг 1:написать какую-нибудь сборку вручную

Я рекомендую вам сначала попробовать написать собственную сборку вручную, чтобы точно видеть и контролировать, что происходит, когда вы начнете обучение.

Тогда возникает вопрос, как наблюдать за тем, что происходит в программе, и ответы такие:

  • ГББ
  • используйте стандартную библиотеку C для print и assert вещи

Самостоятельное использование стандартной библиотеки C требует некоторой работы, но ничего особенного.Например, я хорошо проделал эту работу для вас в Linux в следующих файлах моей тестовой установки:

Используя эти помощники, я затем начинаю экспериментировать с основами, такими как:

  • загружать и сохранять данные в/из памяти в регистры SSE
  • добавлять целые числа и числа с плавающей запятой разных размеров
  • утверждать, что результаты соответствуют моим ожиданиям

addpd.S

#include <lkmc.h>

LKMC_PROLOGUE
.data
    .align 16
    addps_input0: .float 1.5, 2.5,  3.5,  4.5
    addps_input1: .float 5.5, 6.5,  7.5,  8.5
    addps_expect: .float 7.0, 9.0, 11.0, 13.0
    addpd_input0: .double 1.5, 2.5
    addpd_input1: .double 5.5, 6.5
    addpd_expect: .double 7.0, 9.0
.bss
    .align 16
    output:       .skip 16
.text
    /* 4x 32-bit */
    movaps addps_input0, %xmm0
    movaps addps_input1, %xmm1
    addps %xmm1, %xmm0
    movaps %xmm0, output
    LKMC_ASSERT_MEMCMP(output, addps_expect, $0x10)

    /* 2x 64-bit */
    movaps addpd_input0, %xmm0
    movaps addpd_input1, %xmm1
    addpd %xmm1, %xmm0
    movaps %xmm0, output
    LKMC_ASSERT_MEMCMP(output, addpd_expect, $0x10)
LKMC_EPILOGUE

GitHub апстрим.

паддк.С

#include <lkmc.h>

LKMC_PROLOGUE
.data
    .align 16
    input0:       .long 0xF1F1F1F1, 0xF2F2F2F2, 0xF3F3F3F3, 0xF4F4F4F4
    input1:       .long 0x12121212, 0x13131313, 0x14141414, 0x15151515
    paddb_expect: .long 0x03030303, 0x05050505, 0x07070707, 0x09090909
    paddw_expect: .long 0x04030403, 0x06050605, 0x08070807, 0x0A090A09
    paddd_expect: .long 0x04040403, 0x06060605, 0x08080807, 0x0A0A0A09
    paddq_expect: .long 0x04040403, 0x06060606, 0x08080807, 0x0A0A0A0A
.bss
    .align 16
    output:       .skip 16
.text
    movaps input1, %xmm1

    /* 16x 8bit */
    movaps input0, %xmm0
    paddb %xmm1, %xmm0
    movaps %xmm0, output
    LKMC_ASSERT_MEMCMP(output, paddb_expect, $0x10)

    /* 8x 16-bit */
    movaps input0, %xmm0
    paddw %xmm1, %xmm0
    movaps %xmm0, output
    LKMC_ASSERT_MEMCMP(output, paddw_expect, $0x10)

    /* 4x 32-bit */
    movaps input0, %xmm0
    paddd %xmm1, %xmm0
    movaps %xmm0, output
    LKMC_ASSERT_MEMCMP(output, paddd_expect, $0x10)

    /* 2x 64-bit */
    movaps input0, %xmm0
    paddq %xmm1, %xmm0
    movaps %xmm0, output
    LKMC_ASSERT_MEMCMP(output, paddq_expect, $0x10)

LKMC_EPILOGUE

GitHub апстрим.

Шаг 2:напиши немного внутренностей

Однако для производственного кода вы, скорее всего, захотите использовать уже существующие встроенные функции вместо необработанной сборки, как указано в: https://stackoverflow.com/a/1390802/895245

Итак, теперь я пытаюсь преобразовать предыдущие примеры в более или менее эквивалентный код C со встроенными функциями.

addpq.c

#include <assert.h>
#include <string.h>

#include <x86intrin.h>

float global_input0[] __attribute__((aligned(16))) = {1.5f, 2.5f, 3.5f, 4.5f};
float global_input1[] __attribute__((aligned(16))) = {5.5f, 6.5f, 7.5f, 8.5f};
float global_output[4] __attribute__((aligned(16)));
float global_expected[] __attribute__((aligned(16))) = {7.0f, 9.0f, 11.0f, 13.0f};

int main(void) {
    /* 32-bit add (addps). */
    {
        __m128 input0 = _mm_set_ps(1.5f, 2.5f, 3.5f, 4.5f);
        __m128 input1 = _mm_set_ps(5.5f, 6.5f, 7.5f, 8.5f);
        __m128 output = _mm_add_ps(input0, input1);
        /* _mm_extract_ps returns int instead of float:
        * * https://stackoverflow.com/questions/5526658/intel-sse-why-does-mm-extract-ps-return-int-instead-of-float
        * * https://stackoverflow.com/questions/3130169/how-to-convert-a-hex-float-to-a-float-in-c-c-using-mm-extract-ps-sse-gcc-inst
        * so we must use instead: _MM_EXTRACT_FLOAT
        */
        float f;
        _MM_EXTRACT_FLOAT(f, output, 3);
        assert(f == 7.0f);
        _MM_EXTRACT_FLOAT(f, output, 2);
        assert(f == 9.0f);
        _MM_EXTRACT_FLOAT(f, output, 1);
        assert(f == 11.0f);
        _MM_EXTRACT_FLOAT(f, output, 0);
        assert(f == 13.0f);

        /* And we also have _mm_cvtss_f32 + _mm_shuffle_ps, */
        assert(_mm_cvtss_f32(output) == 13.0f);
        assert(_mm_cvtss_f32(_mm_shuffle_ps(output, output, 1)) == 11.0f);
        assert(_mm_cvtss_f32(_mm_shuffle_ps(output, output, 2)) ==  9.0f);
        assert(_mm_cvtss_f32(_mm_shuffle_ps(output, output, 3)) ==  7.0f);
    }

    /* Now from memory. */
    {
        __m128 *input0 = (__m128 *)global_input0;
        __m128 *input1 = (__m128 *)global_input1;
        _mm_store_ps(global_output, _mm_add_ps(*input0, *input1));
        assert(!memcmp(global_output, global_expected, sizeof(global_output)));
    }

    /* 64-bit add (addpd). */
    {
        __m128d input0 = _mm_set_pd(1.5, 2.5);
        __m128d input1 = _mm_set_pd(5.5, 6.5);
        __m128d output = _mm_add_pd(input0, input1);
        /* OK, and this is how we get the doubles out:
        * with _mm_cvtsd_f64 + _mm_unpackhi_pd
        * https://stackoverflow.com/questions/19359372/mm-cvtsd-f64-analogon-for-higher-order-floating-point
        */
        assert(_mm_cvtsd_f64(output) == 9.0);
        assert(_mm_cvtsd_f64(_mm_unpackhi_pd(output, output)) == 7.0);
    }

    return 0;
}

GitHub апстрим.

Paddq.c

#include <assert.h>
#include <inttypes.h>
#include <string.h>

#include <x86intrin.h>

uint32_t global_input0[] __attribute__((aligned(16))) = {1, 2, 3, 4};
uint32_t global_input1[] __attribute__((aligned(16))) = {5, 6, 7, 8};
uint32_t global_output[4] __attribute__((aligned(16)));
uint32_t global_expected[] __attribute__((aligned(16))) = {6, 8, 10, 12};

int main(void) {

    /* 32-bit add hello world. */
    {
        __m128i input0 = _mm_set_epi32(1, 2, 3, 4);
        __m128i input1 = _mm_set_epi32(5, 6, 7, 8);
        __m128i output = _mm_add_epi32(input0, input1);
        /* _mm_extract_epi32 mentioned at:
        * https://stackoverflow.com/questions/12495467/how-to-store-the-contents-of-a-m128d-simd-vector-as-doubles-without-accessing/56404421#56404421 */
        assert(_mm_extract_epi32(output, 3) == 6);
        assert(_mm_extract_epi32(output, 2) == 8);
        assert(_mm_extract_epi32(output, 1) == 10);
        assert(_mm_extract_epi32(output, 0) == 12);
    }

    /* Now from memory. */
    {
        __m128i *input0 = (__m128i *)global_input0;
        __m128i *input1 = (__m128i *)global_input1;
        _mm_store_si128((__m128i *)global_output, _mm_add_epi32(*input0, *input1));
        assert(!memcmp(global_output, global_expected, sizeof(global_output)));
    }

    /* Now a bunch of other sizes. */
    {
        __m128i input0 = _mm_set_epi32(0xF1F1F1F1, 0xF2F2F2F2, 0xF3F3F3F3, 0xF4F4F4F4);
        __m128i input1 = _mm_set_epi32(0x12121212, 0x13131313, 0x14141414, 0x15151515);
        __m128i output;

        /* 8-bit integers (paddb) */
        output = _mm_add_epi8(input0, input1);
        assert(_mm_extract_epi32(output, 3) == 0x03030303);
        assert(_mm_extract_epi32(output, 2) == 0x05050505);
        assert(_mm_extract_epi32(output, 1) == 0x07070707);
        assert(_mm_extract_epi32(output, 0) == 0x09090909);

        /* 32-bit integers (paddw) */
        output = _mm_add_epi16(input0, input1);
        assert(_mm_extract_epi32(output, 3) == 0x04030403);
        assert(_mm_extract_epi32(output, 2) == 0x06050605);
        assert(_mm_extract_epi32(output, 1) == 0x08070807);
        assert(_mm_extract_epi32(output, 0) == 0x0A090A09);

        /* 32-bit integers (paddd) */
        output = _mm_add_epi32(input0, input1);
        assert(_mm_extract_epi32(output, 3) == 0x04040403);
        assert(_mm_extract_epi32(output, 2) == 0x06060605);
        assert(_mm_extract_epi32(output, 1) == 0x08080807);
        assert(_mm_extract_epi32(output, 0) == 0x0A0A0A09);

        /* 64-bit integers (paddq) */
        output = _mm_add_epi64(input0, input1);
        assert(_mm_extract_epi32(output, 3) == 0x04040404);
        assert(_mm_extract_epi32(output, 2) == 0x06060605);
        assert(_mm_extract_epi32(output, 1) == 0x08080808);
        assert(_mm_extract_epi32(output, 0) == 0x0A0A0A09);
    }

    return 0;

GitHub апстрим.

Шаг 3:пойти и оптимизировать код и протестировать его

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

Для этого вам, вероятно, потребуется немного узнать о микроархитектуре x86, о которой я сам не знаю.Привязка ЦП к вводу-выводу, вероятно, будет одной из вещей, которые возникнут: Что означают термины «привязка к процессору» и «привязка к вводу-выводу»?

Как упоминалось в: https://stackoverflow.com/a/12172046/895245 это почти неизбежно потребует чтения документации Агнера Фога, которая кажется лучше, чем все, что опубликовала сама Intel.

Однако мы надеемся, что шаги 1 и 2 послужат основой для того, чтобы хотя бы поэкспериментировать с функциональными аспектами, не связанными с производительностью, и быстро увидеть, что делают инструкции.

ДЕЛАТЬ:приведите здесь минимально интересный пример такой оптимизации.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top