Почему этот простой тест C ++ SIMD работает медленнее при использовании инструкций SIMD? - PullRequest
0 голосов
/ 13 октября 2019

Я думаю о написании векторной математической библиотеки SIMD, поэтому в качестве быстрого теста я написал программу, которая делает 100 миллионов (4-х плавающих) векторных умножений по элементам и добавляет их к совокупному итогу. Для моего классического варианта без SIMD я просто создал структуру с 4 числами с плавающей запятой и написал свою собственную функцию умножения «multiplyTwo», которая умножает две такие структуры по элементам и возвращает другую структуру. Для моего варианта SIMD я использовал «immintrin.h» вместе с __m128, _mm_set_ps и _mm_mul_ps. Я работаю на процессоре i7-8565U (виски Lake) и компилирую с: g++ main.cpp -mavx -o test.exe, чтобы включить инструкции расширения AVX в GCC.

Странно то, что SIMD-версия занимает около 1,4 секунды, а не SIMD-версия - всего 1 секунду. Мне кажется, что я делаю что-то не так, так как я думал, что SIMD-версия должна работать в 4 раза быстрее. Любая помощь приветствуется, код ниже. Я разместил не-SIMD-код в комментариях, код в его текущей форме - SIMD-версия.

#include "immintrin.h" // for AVX 
#include <iostream>

struct NonSIMDVec {
    float x, y, z, w;
};

NonSIMDVec multiplyTwo(const NonSIMDVec& a, const NonSIMDVec& b);

int main() {
    union { __m128 result; float res[4]; };
    // union { NonSIMDVec result; float res[4]; };

    float total = 0; 
    for(unsigned i = 0; i < 100000000; ++i) {
        __m128 a4 = _mm_set_ps(0.0000002f, 1.23f, 2.0f, (float)i);
        __m128 b4 = _mm_set_ps((float)i, 1.3f, 2.0f, 0.000001f);
        // NonSIMDVec a4 = {0.0000002f, 1.23f, 2.0f, (float)i}; 
        // NonSIMDVec b4 = {(float)i, 1.3f, 2.0f, 0.000001f};

        result = _mm_mul_ps(a4, b4); 
        // result = multiplyTwo(a4, b4);

        total += res[0];
        total += res[1];
        total += res[2];
        total += res[3];
    }

    std::cout << total << '\n';
}

NonSIMDVec multiplyTwo(const NonSIMDVec& a, const NonSIMDVec& b)
{ return {a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w}; }

1 Ответ

3 голосов
/ 13 октября 2019

С отключенной оптимизацией (по умолчанию gcc -O0), встроенные функции часто бывают ужасными. Антиоптимизированный -O0 code-gen для встроенных функций обычно причиняет больший ущерб (даже больше, чем для скалярных), а некоторые из функционально-подобных встроенных функций приводят к дополнительным затратам на хранение / перезагрузку. Кроме того, дополнительная задержка переадресации в магазине -O0 имеет тенденцию причинять больше вреда, потому что при работе с 1 вектором вместо 4 скаляров меньше ILP.

Использование gcc -march=native -O3

Но даже с включенной оптимизацией ваш код по-прежнему написан для снижения производительности SIMD путем горизонтального сложения каждого вектора внутри цикла. См. Как вычислить произведение векторной точки с использованием встроенных функций SSE в C , чтобы узнать, как не сделать это: используйте _mm_add_ps для накопления вектора __m128 total и только горизонтальной суммируйте его снаружицикл.

Вы ограничиваете цикл на задержке добавления FP, выполняя скалярное total += внутри цикла. Эта цепочка зависимостей, переносимая циклами, означает, что ваш цикл не может работать быстрее, чем 1 float за 4 цикла в вашей микроархитектуре, полученной из Skylake, где задержка addss равна 4 циклам. (https://agner.org/optimize/)

Даже лучше, чем __m128 total, используйте 4 или 8 векторов, чтобы скрыть задержку добавления FP, поэтому ваш цикл SIMD может стать узким местом при пропускной способности multi / add (или FMA) вместо задержки.


Как только вы исправите это, то, как @harold указывает на то, как вы используете _mm_set_ps внутри цикла, вы получите довольно плохой ассемблер от компилятора. Это не хороший выбор внутри цикла, когда операндыне являются константами или, по крайней мере, инвариантными к циклам.

Ваш пример здесь явно искусственный, обычно вы загружаете векторы SIMD из памяти. Но если вам нужно обновить счетчик цикла в __m128 vector, вы можете использовать tmp = _mm_add_ps(tmp, _mm_set_ps(1.0, 0, 0, 0)). Или развернуть с добавлением 1.0, 2.0, 3.0 и 4.0, чтобы зависимость, переносимая в цикле, составляла только + = 4.0 в одном элементе.

x + 0.0 - этоИдентификационная операция даже для FP (кроме, может быть, со знаком нуля), так что вы можете сделать это с другими элементами, не изменяя их.

Или для нижнего элемента вектора, вы можете использовать _mm_add_ss (скаляр) длятолько измените его.

...