Эффективно оценивайте большие полиномы с помощью SIMD

У меня есть довольно большие (20-40 градусов) медленно сходящиеся (иногда) многочлены с плавающей запятой. Я хотел бы оптимизировать их оценку с помощью SIMD (SSE2, AVX1, AVX-512). Мне нужны решения как с плавающей запятой-32, так и с двойным-64.

Значения коэффициентов являются константами, заданными заранее, а значение X для оценки поли задается в качестве аргумента функции.

Важное примечание. У меня есть только один вход X для моей функции. Поэтому я не могу выполнять вертикальную оптимизацию, вычисляя полигон для 8-16 Xs одновременно. Это означает, что мне нужна горизонтальная оптимизация в рамках оценки для одного X.

Я создал связанный вопрос, который помогает мне вычислить степени X (например, X^1, X^2, ..., X^8), необходимые для оценки SIMD.

Очевидно, что SIMD следует использовать только после некоторого порога полиномиальной степени, для довольно малых полигонов можно использовать метод Хорнера (или Эстрина), основанный на методе Хорнера (или Эстрина) как здесь. Также ширина SIMD (128, 256 или 512) должна выбираться в зависимости от степени полиномии.

Ниже я реализовал вариант AVX-256-Float32, используя своего рода модифицированный метод Хорнера, подходящий для SIMD (умножение на x^8 вместо x^1). Спасибо @PeterCordes за быструю горизонтальную сумму tutorial. Нажмите на ссылку «попробовать онлайн», она содержит более крупный код, который также имеет справочную простую оценку для сравнения и измерения времени:

Попробуйте онлайн!

template <size_t S, size_t I, typename MT = __m256, size_t Cnt>
inline MT EvalPoly8xF32Helper(MT xhi,
        std::array<float, Cnt> const & A, MT r = _mm256_undefined_ps()) {
    size_t constexpr K = 8;
    if constexpr(I + K >= S)
        r = _mm256_load_ps(&A[I]);
    else {
        #ifdef __FMA__
            r = _mm256_fmadd_ps(r, xhi, _mm256_load_ps(&A[I]));
        #else
            r = _mm256_add_ps(_mm256_mul_ps(r, xhi),
                _mm256_load_ps(&A[I]));
        #endif
    }
    if constexpr(I < K)
        return r;
    else
        return EvalPoly8xF32Helper<S, I - K>(xhi, A, r);
}

inline float _mm_fast_hsum_ps(__m128 v) {
    __m128 shuf = _mm_movehdup_ps(v);
    __m128 sums = _mm_add_ps(v, shuf);
    shuf        = _mm_movehl_ps(shuf, sums);
    sums        = _mm_add_ss(sums, shuf);
    return        _mm_cvtss_f32(sums);
}

template <size_t S, size_t Cnt>
inline float EvalPoly8xF32(
        float x, std::array<float, Cnt> const & A) {
    auto constexpr K = 8;
    auto const x2 = x * x, x4 = x2 * x2, x8 = x4 * x4, x3 = x2 * x;
    auto const powsx = _mm256_setr_ps(
        1, x, x2, x3, x4, x4 * x, x4 * x2, x4 * x3);
    auto r0 = EvalPoly8xF32Helper<S, (S - 1) / K * K>(
        _mm256_set1_ps(x8), A);
    r0 = _mm256_mul_ps(r0, powsx);
    return _mm_fast_hsum_ps(_mm_add_ps(
        _mm256_castps256_ps128(r0), _mm256_extractf128_ps(r0, 1)));
}

Как видно, SIMD-версия дает достаточно большое ускорение по сравнению с эталонной простой реализацией. Для AVX1-256-float32 и степени 32 это дает ускорение примерно в 4.5x раз (для степени 16 дает 1.8x ускорение, что тоже хорошо)! Очевидно, что даже простое использование FMA инструкций внутри эталонных реализаций уже заметно улучшит скорость вычислений.

У меня вопрос, можете ли вы предложить более быстрый метод оценки полинома, или даже какой-то готовый код или библиотеку, или какие-либо оптимизации моего кода.

Чаще всего будет использоваться целевой ЦП: Intel Xeon Gold 6230 с AVX-512, поэтому мне нужно оптимизировать код для него.


person Arty    schedule 14.06.2021    source источник
comment
Вы можете попробовать сильно оптимизировать компилятор ICC Intel с IPP library для оптимизации уровня ЦП. просто говорю   -  person Roshan M    schedule 14.06.2021
comment
Если вы просто хотите оценить один полином, уверены ли вы, что это действительно критично для производительности? Кроме того, для многочленов такого размера вам, вероятно, придется позаботиться о порядке вычисления, так как вы можете получить эффект числовой отмены. Если возможный диапазон X ограничен, вы можете попытаться аппроксимировать свой многочлен полиномом более низкой степени.   -  person chtz    schedule 14.06.2021
comment
@chtz Мне нужно оценить один и тот же полигон (одинаковые коэффициенты) для разных X, и у меня есть миллионы таких X. Единственное, что эти миллионы приходят в разное время. т.е. Я не могу вычислять 10 X за раз, у меня есть только один X для моей функции. И это критичное место для производительности, эта оценка должна быть как можно быстрее. Также я не могу аппроксимировать этот полигон полигонами более низкой степени, потому что мне нужна именно точность этого полигона, без лишних потерь на аппроксимацию. Также эти полигоны уже разработаны именно для данного диапазона X.   -  person Arty    schedule 14.06.2021
comment
Можете ли вы разложить свой большой полином на произведение 4 или 8 полиномов более низкой степени (возможно, с дополнительными постоянными смещениями p(x) = (p0(x)*p1(x)+c0)*(p2(x)*p3(x)+c1) + c2)? Затем вы можете оценить их параллельно и умножить их вместе.   -  person chtz    schedule 15.06.2021
comment
@chtz Я могу учитывать только более мелкие полигоны, только если это факторинг каким-то образом возможен для любого возможного полигона. Можете ли вы предложить ссылку на такой алгоритм тогда? Потому что я строю оптимальные полигоны для заданного диапазона. Это означает, что они не являются произвольными полигонами. Оптимальный полигон существует только один, и он такой, что максимальная ошибка по диапазону минимальна среди всех полигонов. Мы можем ослабить требование Оптимальности, если можно построить полигон более высокой степени, достигая обоих свойств: 1) иметь достаточно маленькую максимальную ошибку для заданного диапазона 2) можно легко факторизовать. Можете ли вы предложить алгоритм нахождения такого полигона?   -  person Arty    schedule 15.06.2021
comment
Не всегда возможно разложить многочлен, используя только действительные коэффициенты. Всегда можно разложить на сложные коэффициенты, например wolfram. Это может оказаться бесполезным, хотя в этом примере показаны только два термина с комплексными коэффициентами. Если достаточное количество терминов чисто реальны, это может быть победа. (Но, возможно, только если бы вы могли JIT генерировать код на основе факторинга; вам, вероятно, нужна стратегия, специфичная для того, сколько реальных и сложных коэффициентов. @chtz)   -  person Peter Cordes    schedule 15.06.2021
comment
@PeterCordes Кстати, можете ли вы предложить какую-нибудь полезную ссылку на учебник о том, как я могу JIT-кодировать свой код? На самом деле, если у меня есть произвольный код C++ в качестве функции, я хочу иметь возможность скомпилировать его во время запуска программы EXE. Я понимаю, что мне нужно связать библиотеку CLang с моим бинарником. Но кроме привязки что еще нужно? Какие функции libclang я должен вызывать для компиляции кода из ОЗУ? Как мне тогда динамически связать результирующую функцию с моим кодом в ОЗУ? Все эти вопросы освещены в каком-нибудь хорошем онлайн-уроке, если вы знаете?   -  person Arty    schedule 15.06.2021
comment
@PeterCordes Я точно не помню, но мне кажется, что ЛЮБОЙ полигон может быть преобразован в реальный полигон, если вы добавите к нему специальную константу, что означает, что любой полигон может быть представлен как A(x) = B(x) * C(x) + D во всех реальных значениях для некоторого D. I' Я не уверен в этом факте, но мне кажется, что это правда.   -  person Arty    schedule 15.06.2021
comment
Да, если вы вообще примете такой подход, вы захотите использовать для этого libclang. Вы создаете одну функцию, которую будете использовать много раз, поэтому ее полный оптимизатор был бы хорош (вместо более быстрых оптимизаторов, которые ухудшают машинный код, таких как HotSpot или MS C# JIT). Я не использовал libclang, но я d думаю, что он может вернуть вам указатель на функцию, которую вы можете вызвать. Вам не нужно ничего связывать, просто разыменуйте указатель функции, который указывает на исполняемую страницу памяти, содержащую машинный код.   -  person Peter Cordes    schedule 15.06.2021
comment
@Arty: re: факторинг в A(x) = B(x) * C(x) + D - возможно, но это, вероятно, не гарантирует, что B и C будут иметь одинаковую степень. Разложение полинома с 16 степенями на 14 x 2 или 15 x 1 было бы бесполезным. Однако, возможно, стоит изучить, поскольку параллельное вычисление двух или более полиномов (в отдельных элементах SIMD одного и того же вектора) позволило бы использовать простое правило Хорнера/схему Эстрина, оставляя горизонтальную очистку как shuffle/mul вместо shuffle/add. Прошло много времени с тех пор, как я посещал уроки математики для студентов, поэтому я уверен, что есть много трюков, которых я не знаю.   -  person Peter Cordes    schedule 15.06.2021
comment
@PeterCordes Вы всегда можете разложить многочлен с действительными коэффициентами на действительные многочлены степени 1 или 2 (для каждого комплексного корня сопряженное тоже является корнем). Однако у меня нет обзора численно стабильных алгоритмов для этого.   -  person chtz    schedule 15.06.2021
comment
@chtz: Ах, да, числовая стабильность может быть серьезной проблемой, например. поражение цели использования двойного и наличия такого количества терминов. Но если факторизованные коэффициенты не приводят к катастрофическому сокращению, параллельное выполнение многих полиномов низкой степени очень хорошо с SIMD FMA, тогда вам остается сокращать их с помощью дерева умножения операций.   -  person Peter Cordes    schedule 15.06.2021
comment
@chtz Было бы здорово найти численные методы нахождения всех корней многоугольника. я таких не знаю. Вещественные и комплексные корни.   -  person Arty    schedule 15.06.2021
comment
@PeterCordes Я читаю uops.info, можете ли вы сказать, что означают UOP в таблице?   -  person Arty    schedule 15.06.2021
comment
микрооперации. См. agner.org/optimize (руководство по микроархитектуре), realworldtech.com/sandy-bridge и, возможно, руководство Intel по оптимизации (хотя руководство Intel огромно и обширно, но есть раздел о внутреннем устройстве Skylake. Сначала прочтите руководство Агнера Фога, в нем рассматриваются основные понятия, чтобы понять, как инструкции x86 декодируются в uops и проходят через конвейер.)   -  person Peter Cordes    schedule 15.06.2021
comment
@Arty Предлагаю начать читать здесь en.wikipedia.org/wiki/ -- Я не думаю, что для этого существует один оптимальный алгоритм, так как производительность, скорее всего, будет зависеть от состояния полиномов.   -  person chtz    schedule 15.06.2021
comment
@PeterCordes Если я разложу свой полигон на квадратичный полигон, не могли бы вы предложить, как быстрее всего это вычислить? Очевидно, что одиночный квадратичный полигон вычисляется всего за одно сложение и одну операцию FMA. Но что дальше? Я получаю тогда N число, которое мне нужно умножить как можно быстрее? Каков самый быстрый способ умножить N чисел, используя любые SIMD-операции? Вероятно, существуют разные алгоритмы/код для разных пороговых значений N. Есть ли у вас готовая ссылка на какой-нибудь учебник (или код) о том, как максимально быстро умножать N чисел?   -  person Arty    schedule 15.06.2021
comment
@chtz Кстати, большое спасибо за факторинг полиномиального предложения. Это мне очень помогло и, надеюсь, сделает мой код намного быстрее. Я уже начал кодировать это решение применения факторинга для ускорения оценки полигонов. Результаты опубликую здесь через некоторое время.   -  person Arty    schedule 15.06.2021
comment
@Arty: mulps работает точно так же, как addps, а также является коммутативным и (примерно для FP) ассоциативным. Как я уже сказал, дерево умножений до одного вектора, а затем горизонтальное уменьшение этого одного вектора до скаляра с точно такими же перетасовками, что и для Самый быстрый способ сделать горизонтальную векторную сумму SSE (или другое сокращение). Точно так же, как если бы вам нужна была сумма массива. Если у вас нет 8 полных векторов данных, использование более широких векторов — это просто компромисс между большим количеством перетасовок на критическом пути и приближением к насыщению 2 mul/такт.   -  person Peter Cordes    schedule 15.06.2021
comment
@PeterCordes Если мне нужно сделать N независимых умножений и N независимых сложений (не для этой задачи), какова наилучшая стратегия порядка инструкций? Нужно ли ставить все N умножений и после них N сложений? Или их лучше чередовать, 1 add 1 mul 1 add 1 mul ... Nth add Nth mul? Также в целом лучше ли с точки зрения производительности размещать одинаковые независимые инструкции рядом друг с другом или лучше смешивать / чередовать разные виды инструкций?   -  person Arty    schedule 15.06.2021
comment
Все современные процессоры x86 имеют нестандартный exec, но вы можете упростить его жизнь с помощью конвейерной обработки программного обеспечения, поэтому сначала появляется независимая работа. то есть чередовать отдельные цепочки зависимостей. Как вы можете видеть из uops.info, на вашем процессоре mul и add буквально идентичны в том, как процессор их обрабатывает. На самом деле они оба работают на одном и том же устройстве FMA. Если вы можете предварительно обработать свои коэффициенты, чтобы использовать FMA вместо add+mul, это сократит задержку и пропускную способность. например (x + a ) * b равно x*b + a*b = fma(x, b, a*b), поэтому у вас все еще есть две константы, просто разные значения.   -  person Peter Cordes    schedule 15.06.2021
comment
@PeterCordes На самом деле я хотел знать это для какой-то другой задачи, в которой add и mul не зависят друг от друга. Так что в этом случае поможет улучшить производительность запись mul mul mul add add add или mul add mul add mul add, здесь все 6 операций независимы друг от друга. Итак, если все 6 независимы, есть ли смысл чередовать операции разного типа? Или лучше сгруппировать одинаковые операции вместе? Я знаю, что случай с 6 операциями не имеет значения, но как насчет N=16, 32 или 64 операций? Я хочу, чтобы с помощью кода ЦП выполнял выполнение не по порядку наиболее эффективным способом.   -  person Arty    schedule 15.06.2021
comment
Нет, не будет, как я уже сказал, единственное, что имеет значение в SKX, это их шаблон зависимости. Единственная разница между mulps и addps заключается в том, что они делают с данными внутри исполнительного модуля FMA, который их запускает, и в этот момент нет взаимодействия с другими инструкциями. (Тем не менее, более ранние ЦП до Skylake имеют отдельный модуль SIMD FP-add только на одном порту, поэтому в этом случае лучше запланировать добавления раньше, чтобы инструкции умножения могли видеть, что порт уже имеет uops в очереди, и получать запланирован на другой порт).   -  person Peter Cordes    schedule 15.06.2021