SVML __m256d _mm256_log2_pd (__m256d a)
недоступен для других компиляторов, чем Intel, и говорят, что его производительность ограничена на процессорах AMD. Есть несколько реализаций в Интернете, упомянутых в AVN log intrinsics (_mm256_log_ps), отсутствующих в g++ - 4.8? и SIMD-математических библиотеках для SSE и AVX, однако они, похоже, больше SSE, чем AVX2. Там также векторная библиотека Agner Fog, однако это большая библиотека, имеющая гораздо больше материала, всего лишь вектор log2, поэтому от реализации в ней трудно выясните основные части только для операции векторного log2.
Так может кто-нибудь объяснить, как эффективно реализовать операцию log2()
для вектора из 4 double
чисел? То есть как то, что делает __m256d _mm256_log2_pd (__m256d a)
, но доступно для других компиляторов и достаточно эффективно для процессоров AMD и Intel.
EDIT: в моем текущем конкретном случае числа - это вероятности между 0 и 1, а логарифм используется для вычисления энтропии: отрицание суммы по всем i
из P[i]*log(P[i])
. Диапазон показателей с плавающей запятой для P[i]
велик, поэтому числа могут быть близкими к 0. Я не уверен в точности, поэтому рассмотрим любое решение, начинающееся с 30 бит мантиссы, особенно предпочтительным является настраиваемое решение.
EDIT2: вот моя реализация до сих пор, основанная на "Более эффективная серия" из https://en.wikipedia.org/wiki/Logarithm#Power_series. как это может быть улучшено? (желательно улучшение производительности и точности)
namespace {
const __m256i gDoubleExpMask = _mm256_set1_epi64x(0x7ffULL << 52);
const __m256i gDoubleExp0 = _mm256_set1_epi64x(1023ULL << 52);
const __m256i gTo32bitExp = _mm256_set_epi32(0, 0, 0, 0, 6, 4, 2, 0);
const __m128i gExpNormalizer = _mm_set1_epi32(1023);
//TODO: some 128-bit variable or two 64-bit variables here?
const __m256d gCommMul = _mm256_set1_pd(2.0 / 0.693147180559945309417); // 2.0/ln(2)
const __m256d gCoeff1 = _mm256_set1_pd(1.0 / 3);
const __m256d gCoeff2 = _mm256_set1_pd(1.0 / 5);
const __m256d gCoeff3 = _mm256_set1_pd(1.0 / 7);
const __m256d gCoeff4 = _mm256_set1_pd(1.0 / 9);
const __m256d gVect1 = _mm256_set1_pd(1.0);
}
__m256d __vectorcall Log2(__m256d x) {
const __m256i exps64 = _mm256_srli_epi64(_mm256_and_si256(gDoubleExpMask, _mm256_castpd_si256(x)), 52);
const __m256i exps32_avx = _mm256_permutevar8x32_epi32(exps64, gTo32bitExp);
const __m128i exps32_sse = _mm256_castsi256_si128(exps32_avx);
const __m128i normExps = _mm_sub_epi32(exps32_sse, gExpNormalizer);
const __m256d expsPD = _mm256_cvtepi32_pd(normExps);
const __m256d y = _mm256_or_pd(_mm256_castsi256_pd(gDoubleExp0),
_mm256_andnot_pd(_mm256_castsi256_pd(gDoubleExpMask), x));
// Calculate t=(y-1)/(y+1) and t**2
const __m256d tNum = _mm256_sub_pd(y, gVect1);
const __m256d tDen = _mm256_add_pd(y, gVect1);
const __m256d t = _mm256_div_pd(tNum, tDen);
const __m256d t2 = _mm256_mul_pd(t, t); // t**2
const __m256d t3 = _mm256_mul_pd(t, t2); // t**3
const __m256d terms01 = _mm256_fmadd_pd(gCoeff1, t3, t);
const __m256d t5 = _mm256_mul_pd(t3, t2); // t**5
const __m256d terms012 = _mm256_fmadd_pd(gCoeff2, t5, terms01);
const __m256d t7 = _mm256_mul_pd(t5, t2); // t**7
const __m256d terms0123 = _mm256_fmadd_pd(gCoeff3, t7, terms012);
const __m256d t9 = _mm256_mul_pd(t7, t2); // t**9
const __m256d terms01234 = _mm256_fmadd_pd(gCoeff4, t9, terms0123);
const __m256d log2_y = _mm256_mul_pd(terms01234, gCommMul);
const __m256d log2_x = _mm256_add_pd(log2_y, expsPD);
return log2_x;
}
Пока моя реализация дает 405 268 490 операций в секунду, и она кажется точной до восьмой цифры. Производительность измеряется с помощью следующей функции:
#include <chrono>
#include <cmath>
#include <cstdio>
#include <immintrin.h>
// ... Log2() implementation here
const int64_t cnLogs = 100 * 1000 * 1000;
void BenchmarkLog2Vect() {
__m256d sums = _mm256_setzero_pd();
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 1; i <= cnLogs; i += 4) {
const __m256d x = _mm256_set_pd(double(i+3), double(i+2), double(i+1), double(i));
const __m256d logs = Log2(x);
sums = _mm256_add_pd(sums, logs);
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
double sum = sums.m256d_f64[0] + sums.m256d_f64[1] + sums.m256d_f64[2] + sums.m256d_f64[3];
printf("Vect Log2: %.3lf Ops/sec calculated %.3lf\n", cnLogs / nSec, sum);
}
По сравнению с результатами Логарифма в С++ и сборке, реализация текущего вектора в 4 раза быстрее, чем std::log2()
и в 2,5 раза быстрее, чем std::log()
.