Предположим, что необходимо вычислить обратный или обратный квадратный корень для упакованных данных с плавающей запятой. Оба могут быть легко выполнены:
__m128 recip_float4_ieee(__m128 x) { return _mm_div_ps(_mm_set1_ps(1.0f), x); }
__m128 rsqrt_float4_ieee(__m128 x) { return _mm_div_ps(_mm_set1_ps(1.0f), _mm_sqrt_ps(x)); }
Это работает отлично, но медленно: согласно руководство, они берут 14 и 28 циклов на Sandy Bridge (пропускная способность). Соответствующие версии AVX занимают почти одно и то же время на Haswell.
С другой стороны, вместо этого можно использовать следующие версии:
__m128 recip_float4_half(__m128 x) { return _mm_rcp_ps(x); }
__m128 rsqrt_float4_half(__m128 x) { return _mm_rsqrt_ps(x); }
Они берут только один или два цикла времени (пропускная способность), что дает большую производительность. Однако они ОЧЕНЬ приблизительны: они дают результат с относительной погрешностью менее 1,5 * 2 ^ -12. Учитывая, что машинный эпсилон чисел с плавающей запятой одинарной точности равен 2 ^ -24, можно сказать, что это приближение имеет примерно половину точности.
Кажется, что итерация Newton-Raphson может быть добавлена для получения результата с одинарной точностью (возможно, не так точно, как требуется стандарт IEEE), см. GCC, ICC, обсуждения на LLVM. Теоретически один и тот же метод можно использовать для значений двойной точности, создавая половину или одну или двойную точность.
Мне интересны рабочие реализации этого подхода как для типов с плавающей и двойной данными, так и для всех (половина, одиночные, двойные). Обработка особых случаев (деление на ноль, sqrt (-1), inf/nan и т.п.) не требуется. Кроме того, мне не ясно, какая из этих подпрограмм будет быстрее, чем тривиальные решения IEEE-компиляции, и которые будут медленнее.
Вот несколько незначительных ограничений на ответы:
- Используйте встроенные примеры кода. Сборка зависит от компилятора, поэтому менее полезна.
- Используйте аналогичные соглашения об именах для функций.
- Реализовать подпрограммы с использованием одного регистра SSE/AVX, содержащего плотно упакованные значения float/double в качестве входных данных. Если есть значительный прирост производительности, вы также можете отправлять процедуры, принимающие несколько регистров в качестве входных данных (два регистра могут быть жизнеспособными).
- Не отправляйте обе версии SSE/AVX, если они абсолютно равны до изменения _mm до _mm256 и наоборот.
Любые оценки эффективности, измерения, обсуждения приветствуются.
СУЩНОСТЬ
Вот версии для чисел с плавающей запятой с одной точностью с одной итерацией NR:
__m128 recip_float4_single(__m128 x) {
__m128 res = _mm_rcp_ps(x);
__m128 muls = _mm_mul_ps(x, _mm_mul_ps(res, res));
return res = _mm_sub_ps(_mm_add_ps(res, res), muls);
}
__m128 rsqrt_float4_single(__m128 x) {
__m128 three = _mm_set1_ps(3.0f), half = _mm_set1_ps(0.5f);
__m128 res = _mm_rsqrt_ps(x);
__m128 muls = _mm_mul_ps(_mm_mul_ps(x, res), res);
return res = _mm_mul_ps(_mm_mul_ps(half, res), _mm_sub_ps(three, muls));
}
Ответ, данный Peter Cordes, объясняет, как создавать другие версии, и содержит подробный теоретический анализ производительности.
Здесь вы можете найти все реализованные решения: recip_rsqrt_benchmark.
Полученные результаты пропускной способности на Ivy Bridge представлены ниже. Были проверены только однорежимные реализации SSE. Время, затраченное на задание, предоставляется в циклах за звонок. Первое число - для половины точности (без NR), второе - для одиночной точности (1 NR-итерация), третье - для двух итераций NR.
- recip on float занимает циклы 1, 4 против циклов 7.
- rsqrt на float занимает циклы 1, 6 против 14.
- recip on double принимает циклы 3, 6, 9 по сравнению с 14.
- rsqrt на двойных циклах цикла 3, 8, 13 против циклов 28.
Предупреждение: я должен был творчески объединить исходные результаты...