У меня есть довольно большие (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, поэтому мне нужно оптимизировать код для него.
ICC Intel
сIPP library
для оптимизации уровня ЦП. просто говорю - person Roshan M   schedule 14.06.2021X
ограничен, вы можете попытаться аппроксимировать свой многочлен полиномом более низкой степени. - person chtz   schedule 14.06.2021X
, и у меня есть миллионы такихX
. Единственное, что эти миллионы приходят в разное время. т.е. Я не могу вычислять 10 X за раз, у меня есть только один X для моей функции. И это критичное место для производительности, эта оценка должна быть как можно быстрее. Также я не могу аппроксимировать этот полигон полигонами более низкой степени, потому что мне нужна именно точность этого полигона, без лишних потерь на аппроксимацию. Также эти полигоны уже разработаны именно для данного диапазона X. - person Arty   schedule 14.06.2021p(x) = (p0(x)*p1(x)+c0)*(p2(x)*p3(x)+c1) + c2
)? Затем вы можете оценить их параллельно и умножить их вместе. - person chtz   schedule 15.06.2021A(x) = B(x) * C(x) + D
во всех реальных значениях для некоторого D. I' Я не уверен в этом факте, но мне кажется, что это правда. - person Arty   schedule 15.06.2021A(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.20211 add 1 mul 1 add 1 mul ... Nth add Nth mul
? Также в целом лучше ли с точки зрения производительности размещать одинаковые независимые инструкции рядом друг с другом или лучше смешивать / чередовать разные виды инструкций? - person Arty   schedule 15.06.2021(x + a ) * b
равноx*b + a*b
=fma(x, b, a*b)
, поэтому у вас все еще есть две константы, просто разные значения. - person Peter Cordes   schedule 15.06.2021add
и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