SSE-копия, AVX-копия и std:: копирование

Я пытаюсь улучшить производительность операции копирования через SSE и AVX:

    #include <immintrin.h>

    const int sz = 1024;
    float *mas = (float *)_mm_malloc(sz*sizeof(float), 16);
    float *tar = (float *)_mm_malloc(sz*sizeof(float), 16);
    float a=0;
    std::generate(mas, mas+sz, [&](){return ++a;});

    const int nn = 1000;//Number of iteration in tester loops    
    std::chrono::time_point<std::chrono::system_clock> start1, end1, start2, end2, start3, end3; 

    //std::copy testing
    start1 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
        std::copy(mas, mas+sz, tar);
    end1 = std::chrono::system_clock::now();
    float elapsed1 = std::chrono::duration_cast<std::chrono::microseconds>(end1-start1).count();

    //SSE-copy testing
    start2 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
    {
        auto _mas = mas;
        auto _tar = tar;
        for(; _mas!=mas+sz; _mas+=4, _tar+=4)
        {
           __m128 buffer = _mm_load_ps(_mas);
           _mm_store_ps(_tar, buffer);
        }
    }
    end2 = std::chrono::system_clock::now();
    float elapsed2 = std::chrono::duration_cast<std::chrono::microseconds>(end2-start2).count();

    //AVX-copy testing
    start3 = std::chrono::system_clock::now();
    for(int i=0; i<nn; ++i)
    {
        auto _mas = mas;
        auto _tar = tar;
        for(; _mas!=mas+sz; _mas+=8, _tar+=8)
        {
           __m256 buffer = _mm256_load_ps(_mas);
           _mm256_store_ps(_tar, buffer);
        }
    }
    end3 = std::chrono::system_clock::now();
    float elapsed3 = std::chrono::duration_cast<std::chrono::microseconds>(end3-start3).count();

    std::cout<<"serial - "<<elapsed1<<", SSE - "<<elapsed2<<", AVX - "<<elapsed3<<"\nSSE gain: "<<elapsed1/elapsed2<<"\nAVX gain: "<<elapsed1/elapsed3;

    _mm_free(mas);
    _mm_free(tar);

Это работает. Однако, в то время как число итераций в тесте-петлях - nn - увеличивается, коэффициент усиления simd-копии уменьшается:

nn = 10: SSE-gain = 3, AVX-gain = 6;

nn = 100: SSE-gain = 0,75, AVX-gain = 1,5;

nn = 1000: SSE-gain = 0,55, AVX-gain = 1,1;

Может ли кто-нибудь объяснить, в чем причина упомянутого эффекта снижения производительности, и целесообразно ли вручную нарисовать операцию копирования?

Ответ 1

Проблема заключается в том, что ваш тест выполняет плохую работу, чтобы перенести некоторые факторы на аппаратное обеспечение, которое затрудняет бенчмаркинг. Чтобы проверить это, я сделал свой собственный тестовый пример. Что-то вроде этого:

for blah blah:
    sleep(500ms)
    std::copy
    sse
    axv

выход:

SSE: 1.11753x faster than std::copy
AVX: 1.81342x faster than std::copy

Итак, в этом случае AVX является связкой быстрее, чем std::copy. Что происходит, когда я перехожу к тестовому примеру..

for blah blah:
    sleep(500ms)
    sse
    axv
    std::copy

Обратите внимание, что абсолютно ничего не изменилось, кроме порядка тестов.

SSE: 0.797673x faster than std::copy
AVX: 0.809399x faster than std::copy

Ого! как это возможно? Процессор занимает некоторое время, чтобы увеличить скорость до максимальной скорости, поэтому тесты, которые выполняются позже, имеют преимущество. У этого вопроса есть 3 ответа, включая "принятый" ответ. Но только тот, у кого было наименьшее количество upvotes, было на правильном пути.

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

Ответ 2

Это очень интересный вопрос, но Я считаю, что ответы не верны, потому что сам вопрос настолько вводит в заблуждение.

Название должно быть изменено на "Как достичь пропускной способности ввода-вывода теоретической памяти?"

Независимо от того, какой набор инструкций используется, процессор намного быстрее, чем оперативная память, при которой чистая память блока памяти ограничена на 100%. И это объясняет, почему разница между SSE и производительностью AVX незначительна.

Для небольших буферов, горячих в кеше L1D, AVX может копировать значительно быстрее, чем SSE на процессорах, таких как Haswell, где 256b загрузок/хранилищ действительно используют путь данных 256b к кэшу L1D вместо разделения на две операции 128b.

Как ни странно, древняя инструкция X86 rep stosq работает намного лучше, чем SSE и AVX с точки зрения копирования памяти!

В статье здесь объясняется, как насытить пропускную способность памяти очень хорошо, и у нее есть богатые ссылки для дальнейшего изучения.

См. также Улучшенный REP MOVSB ​​для memcpy здесь, на SO, где ответ @BeeOnRope обсуждает хранилища NT (и хранилища без RFO, выполненные с помощью rep stosb/stosq), по сравнению с обычным магазинов и как частотная пропускная способность одноядерной памяти часто ограничена max concurrency/latency, а не самим контроллером памяти.

Ответ 3

Написание быстрого SSE не так просто, как использование SSE-операций вместо их непараллельных эквивалентов. В этом случае я подозреваю, что ваш компилятор не может с пользой развернуть пару load/store, а ваше время доминирует в киосках, вызванных использованием вывода одной операции с низкой пропускной способностью (нагрузкой) в самой следующей инструкции (хранилище).

Вы можете проверить эту идею, вручную развернув одну метку:

//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
    auto _mas = mas;
    auto _tar = tar;
    for(; _mas!=mas+sz; _mas+=8, _tar+=8)
    {
       __m128 buffer1 = _mm_load_ps(_mas);
       __m128 buffer2 = _mm_load_ps(_mas+4);
       _mm_store_ps(_tar, buffer1);
       _mm_store_ps(_tar+4, buffer2);
    }
}

Обычно при использовании встроенных функций я разбираю вывод и слежу за тем, чтобы не происходило сумасшедшего (вы можете попробовать это, чтобы проверить, как и как разворачивается исходный цикл). Для более сложных циклов правильным инструментом является Анализатор кода архитектуры Intel (IACA). Это инструмент статического анализа, который может рассказать вам такие вещи, как "у вас есть конвейеры".

Ответ 4

Я думаю, это потому, что измерение неточно для коротких коротких операций.

При измерении производительности на процессоре Intel

  • Отключите "Turbo Boost" и "SpeedStep". Вы можете это сделать в BIOS системы.

  • Изменить приоритет процесса/потока на высокий или в реальном времени. Это будет поддерживать поток.

  • Установите маску процессора процесса только на одно ядро. Маска CPU с более высоким приоритетом минимизирует переключение контекста.

  • использовать встроенную функцию __rdtsc(). Серия Intel Core возвращает счетчик встроенных часов процессора с __rdtsc(). Вы получите 3400000000 отсчетов в секунду от CPU 3.4Ghz. И __rdtsc() сбрасывает все запланированные операции в ЦП, чтобы более точно измерить время.

Это мой тестовый код для тестирования кодов SSE/AVX.

    int GetMSB(DWORD_PTR dwordPtr)
    {
        if(dwordPtr)
        {
            int result = 1;
    #if defined(_WIN64)
            if(dwordPtr & 0xFFFFFFFF00000000) { result += 32; dwordPtr &= 0xFFFFFFFF00000000; }
            if(dwordPtr & 0xFFFF0000FFFF0000) { result += 16; dwordPtr &= 0xFFFF0000FFFF0000; }
            if(dwordPtr & 0xFF00FF00FF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00FF00FF00; }
            if(dwordPtr & 0xF0F0F0F0F0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0F0F0F0F0; }
            if(dwordPtr & 0xCCCCCCCCCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCCCCCCCCCC; }
            if(dwordPtr & 0xAAAAAAAAAAAAAAAA) { result += 1; }
    #else
            if(dwordPtr & 0xFFFF0000) { result += 16; dwordPtr &= 0xFFFF0000; }
            if(dwordPtr & 0xFF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00; }
            if(dwordPtr & 0xF0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0; }
            if(dwordPtr & 0xCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCC; }
            if(dwordPtr & 0xAAAAAAAA) { result += 1; }
    #endif
            return result;
        }
        else
        {
            return 0;
        }
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
        // Set Core Affinity
        DWORD_PTR processMask, systemMask;
        GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask);
        SetProcessAffinityMask(GetCurrentProcess(), 1 << (GetMSB(processMask) - 1) );

        // Set Process Priority. you can use REALTIME_PRIORITY_CLASS.
        SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);

        DWORD64 start, end;
        start = __rdtsc();
    // your code here.
        end = __rdtsc();
        printf("%I64d\n", end - start);
        return 0;
    }

Ответ 5

Я думаю, что ваша главная проблема/узкое место - это _mm_malloc.

Я настоятельно рекомендую использовать std::vector в качестве основной структуры данных, если вы обеспокоены локальностью в С++.

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

Также обратите внимание, что тот факт, что AVX новее, чем SSE, не ускоряет AVX, независимо от того, что вы планируете использовать, количество циклов, выполняемых функцией, вероятно, более важно, чем аргумент "avx vs sse", например, см. этот ответ.

Попробуйте использовать POD int array[] или std::vector.