27.11.2015

SSE3 и SSE4

Оптимизация под набор команд SSE3 и SSE4 не дала вообще никакого прироста производительности. Все три варианты выполняются за стабильно одинаковое количество тактов. А скалярная версия под FPU чуть-учть быстрее.

То ли я чего не так делаю, то ли все дело в процессоре... Но в общем пока я пришел к выводу, что оптимизация операций с плавающей точкой на основе SSE не дает особого выигрыша в производительности. Лучше оптимизировать скалярный код.

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

Еще можно попробовать использовать команды SSE для вычислений в скалярном режиме. Возможно, это даст какой-то выигрыш.

4 комментария:

  1. Спасибо за интересный эксперимент. Повычислял и я скалярное произведение.

    Дано: 1000 пар 4-х компонентных векторов (float), нужно посчитать скалярное произведение каждой пары (результат 1000 float-ов) 1 миллион раз:

    clock_t start = clock();
    for (int i = 0; i < 1000000; i++)
    {
    for (int j = 0; j < 1000; j++) {
    c[j] += dot_product(a[j], b[j]); //dot_product - inline function
    }
    }
    double el_time = (double)(clock() - start);

    Процессор: i7-4870HQ @ 2.50GHz (MBP 15 Mid 2014)
    Компилятор: Apple LLVM version 7.0.0 (clang-700.1.76)

    Инструкции AVX не использовал, asm напрямую тоже, только “dpps” через intrinsic clang.

    Компилятор порадовал и огорчил одновременно: в режиме O3 (максимальная оптимизация) генерирует понятный и ожидаемый код используя xmmX регистры, “огорчил” тем, что я так и не смог заставить его не использовать их, -mnosse ключика уже нет.

    Тесты:

    1. Вектор: массив из 4-х float, произведение считаем как a[0] * b[0] + ...

    Время: 1662.27 ms (100%)
    asm (генерированный): загрузить 2 вектора в xmm0 и xmm1 и дальше 16-ю sse инструкциями посчитать скалярное произведение активно используя sse маски и вспомогательные регистры xmm2-xmm4

    2. #1, но я вручную раскрутил цикл по 4, время ~ тоже

    3. Используем dpps инструкцию SSE4:

    Время: 2297.00 ms (+38%)
    asm: загрузить один вектор в xmm0, дальше dpps со 2-м вектором из памяти и к результату добавляет скалярное произведение из предыдущей итерации (я суммирую каждую итерацию, чтобы компилятор не выкинул результат). Компилятор решил раскрутить цикл по 2, но почему-то использует одни и теже регистры!

    4. Поможем компилятору #3 и вручную раскрутим цикл по 4:

    Время: 1613.17 ms (-3%), перезапускал несколько раз всегда быстрее чем #2, но всего на 3-4%
    asm: загрузить 4 вектора в xmm0-xmm3, далее 4-ре dpps с векторами в памяти, дальше 3-мя sse инструкциями упаковали 4-ре 128-bit скалярных произведения (результаты dpps) в один 128-bit регистр и прибавили к результату прошлой итерации.

    5. Дальше я ломал голову как заставить clang не использовать 128-bit регистры, ничего не нашел, тогда решил “обмануть” его и разбить вектора на 2 * 4 массива float-в, дабы он не грузил весь вектор одной sse инструкцией.

    Время: 410.16 ms! (в 4 раза быстрее чем #1)
    Смотрим генерированный asm: компилятор меня перехитрил, он по прежнему грузит в xmmX, но теперь вычисляет 4-ре скалярных произведения за 11 простых sse инструкций! Вот так :)

    Судя по статьям в инете, там действительно все не так однозначно. Если я правильно понял то основной плюс SSE & AVX - это загрузка кучи данных за раз. Процессор то быстро считать умеет, а тот же кэш все еще жуткий тормоз, чем больше из него возьмем тем лучше...

    ОтветитьУдалить
    Ответы
    1. Ну, похоже и этого плюса нет. FPU у меня в скалярном режиме считает чуть быстрее.
      Да и данные не всегда лежат в одной строчке, иногда их приходится собирать.
      Выигрыш, по идее, должен быть в том, что четыре операции с плавающей точкой выполняются как одна.

      Но! Обычные скалярные инструкции, если они независимы, тоже на современных процессорах могут выполняются одновременно.

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

      Правда, есть очень интересный момент с целочисленными операциями. Возможно, здесь SSE дает прикурить.

      Удалить
    2. Может из-за сбора и упаковки в 128-битный регистр не получается получить выигрыш? Основное время уходит на общение с кэшем и памятью?

      Вроде современный процессор и sse инструкций выполняет 2 за один так (одно сложение и одно умножение), покрайней мере у меня раскручивание цикла ускоряло работу.

      Удалить
    3. Не знаю. Я эксперименты свои проводил на готовом векторе, там уже собирать нечего, просто загрузить из памяти в регистр.

      В документации по оптимизации рекомендуют для максимальной производительности для некоторых SSE инструкций (не помню уже, для каких) после выполнения такой инструкции до обращения к ресурсам SSE выполнить не менее 10 обычных инструкций (также забыл, только общего назначения или можно и FPU использовать). Сейчас уже не найду то место документации, конечно.

      Удалить