Эффективная реализация log2 (__ m256d) в AVX2

SVML __m256d _mm256_log2_pd (__m256d a) недоступен в других компиляторах, кроме Intel, и они говорят, что его производительность снижается на процессорах AMD. В Интернете есть некоторые реализации, указанные в AVX встроенные функции журнала (_mm256_log_ps) отсутствуют в g ​​++ - 4.8? и математика SIMD библиотеки для SSE и AVX, однако они кажутся больше SSE, чем AVX2. Также существует векторная библиотека Агнера Фога, однако это большая библиотека, в которой гораздо больше вещей, чем просто векторный log2, поэтому из реализации в нем трудно понять существенные части только для операции vector log2.

Так может кто-нибудь просто объяснить, как эффективно реализовать log2() операцию для вектора из 4 double чисел? Т.е. вроде того, что делает __m256d _mm256_log2_pd (__m256d a), но он доступен для других компиляторов и достаточно эффективен как для процессоров AMD, так и для процессоров Intel.

РЕДАКТИРОВАТЬ: В моем текущем конкретном случае числа представляют собой вероятности от 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 операций в секунду, и кажется точной до 8-й цифры. Производительность измеряется с помощью следующей функции:

#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);
}

По сравнению с результатами логарифма в C ++ и сборке, текущая реализация вектора в 4 раза больше быстрее std::log2() и в 2,5 раза быстрее std::log().

В частности, используется следующая формула аппроксимации:  Условия приближения - визуальные введите описание изображения здесь


person Serge Rogatch    schedule 19.08.2017    source источник
comment
Разве нельзя просто использовать функцию журнала в avx_mathfun и умножить результат на требуемую константу?   -  person Paul R    schedule 19.08.2017
comment
@PaulR, это для float, а не для double. Как минимум, я не знаю, как получить константы типа cephes_log_p0 для двойных чисел: github.com/reyoung/avx_mathfun/blob/master/avx_mathfun.h   -  person Serge Rogatch    schedule 19.08.2017
comment
Ах - не заметил этого - в этом случае я предлагаю поискать решение SSE (например, см. этот вопрос и ответы на него ) - должно быть легко расширить реализацию SSE на AVX.   -  person Paul R    schedule 19.08.2017
comment
@SergeRogatch Вы можете использовать свой любимый метод подбора полиномов для создания этих констант.   -  person    schedule 19.08.2017
comment
Кто они?   -  person EOF    schedule 19.08.2017
comment
@EOF, см. agner.org/optimize/blog/read. php? i = 209 & v = t, agner. org / optimize / blog / read.php? i = 115 & v = t. Кроме того, сама Intel в своем описании SVML говорит, что он оптимизирован для процессоров Intel, в то время как на самом деле происходит проверка поставщика процессора, а затем переход к неоптимальному коду, если это не Intel.   -  person Serge Rogatch    schedule 19.08.2017
comment
@SergeRogatch Учитывая, что этим записям в блоге несколько лет, и они демонстрируют довольно ограниченный эффект, я бы сказал, что протестированные библиотеки удовлетворяют требованию быть достаточно эффективными как на процессорах AMD, так и на процессорах Intel.   -  person EOF    schedule 19.08.2017
comment
Intel недавно добавила математические функции, оптимизированные для AVX2 и FMA, в glibc phoronix.com/   -  person Z boson    schedule 23.08.2017


Ответы (2)


Обычная стратегия основана на идентификаторе log(a*b) = log(a) + log(b), или в данном случае log2( 2^exponent * mantissa) ) = log2( 2^exponent ) + log2(mantissa). Или упрощая, exponent + log2(mantissa). Мантисса имеет очень ограниченный диапазон, от 1,0 до 2,0, поэтому полином для log2(mantissa) должен соответствовать только этому очень ограниченному диапазону. (Или, что то же самое, мантисса = от 0,5 до 1,0 и измените константу коррекции смещения экспоненты на 1).

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

Если важно, чтобы ваша функция оценивала log2(1.0) как точное значение 0.0, вы можете организовать это, фактически используя mantissa-1.0 в качестве полинома, а не постоянный коэффициент. 0.0 ^ n = 0.0. Это также значительно улучшает относительную ошибку для входных данных, близких к 1.0, даже если абсолютная ошибка все еще мала.


Насколько точным он должен быть и в каком диапазоне входных данных? Как обычно, существует компромисс между точностью и скоростью, но, к счастью, довольно легко двигаться по этой шкале, например, добавление еще одного полиномиального члена (и повторная подгонка коэффициентов) или отказ от некоторого исключения ошибок округления.

нацелена на очень высокую точность VCL Агнера _9 для реализации VCL от Агнера Фога. использовать уловки, чтобы избежать ошибки округления, избегая вещей, которые могут привести к добавлению малого и большого числа, когда это возможно. Это несколько затемняет основной дизайн.


Для более быстрого и приблизительного float log() см. Реализацию полинома на http://jrfonseca.blogspot.ca/2008/09/fast-sse2-pow-tables-or-polynomials.html. Он не учитывает МНОГО дополнительных приемов повышения точности, которые использует VCL, поэтому его легче понять. Он использует полиномиальную аппроксимацию мантиссы в диапазоне от 1,0 до 2,0.

(Это настоящий трюк для log() реализаций: вам нужен только многочлен, который работает в небольшом диапазоне.)

Он уже просто делает log2 вместо log, в отличие от VCL, где log-base-e встроен в константы и как он их использует. Его чтение, вероятно, станет хорошей отправной точкой для понимания exponent + polynomial(mantissa) реализаций log().

Даже его версия с наивысшей точностью не является полной float точностью, не говоря уже о double, но вы можете уместить многочлен с большим количеством членов. Или очевидно, что соотношение двух многочленов работает хорошо; это то, что VCL использует для double.

Я получил отличные результаты от переноса функции SSE2 из JRF в AVX2 + FMA (и особенно AVX512 с _mm512_getexp_ps и _mm512_getmant_ps), как только я ее тщательно настроил. (Это было частью коммерческого проекта, поэтому я не думаю, что смогу опубликовать код.) Быстрая приблизительная реализация для float была именно тем, что я хотел.

В моем варианте использования каждый jrf_fastlog() был независимым, поэтому выполнение OOO красиво скрыло задержку FMA, и даже не стоило использовать метод оценки полинома с более высокой ILP и меньшей задержкой, который Функция polynomial_5() VCL использует (" Схема Эстрина ", которая выполняет некоторые не-FMA умножения перед FMA, что приводит к большему количеству инструкций).


VCL Agner Fog теперь лицензируется Apache, поэтому любой проект может просто включать его напрямую. Если вам нужна высокая точность, вы должны просто использовать VCL напрямую. Это только заголовок, просто встроенные функции, поэтому он не приведет к раздуванию вашего двоичного файла.

Функции VCL log float и double находятся в vectormath_exp.h. Алгоритм состоит из двух основных частей:

  • извлеките биты экспоненты и преобразуйте это целое число обратно в число с плавающей запятой (после корректировки смещения, которое использует IEEE FP).

  • извлеките мантиссу и ИЛИ в некоторых битах экспоненты, чтобы получить вектор из double значений в диапазоне [0.5, 1.0). (Или (0.5, 1.0], я забыл).

    Далее отрегулируйте это с помощью if(mantissa <= SQRT2*0.5) { mantissa += mantissa; exponent++;}, а затем mantissa -= 1.0.

    Используйте полиномиальное приближение к log(x) с точностью около x = 1,0. (Для double log_d() VCL использует соотношение двух полиномов 5-го порядка. @harold говорит, что это часто хорошо для точности. Одно деление, смешанное с большим количеством FMA, обычно не снижает пропускной способности, но оно имеет большую задержку, чем FMA. Использование vrcpps + итерации Ньютона-Рафсона обычно медленнее, чем просто использование vdivps на современном оборудовании. Использование коэффициента также создает больше ILP за счет параллельной оценки двух полиномов более низкого порядка вместо одного полинома высокого порядка и может снизить общую задержку по сравнению с одной длинной цепочкой деплоя для высокого порядка полином (который также будет накапливать значительную ошибку округления в этой длинной цепочке).

Затем добавьте exponent + polynomial_approx_log(mantissa), чтобы получить окончательный результат log (). VCL делает это в несколько этапов, чтобы уменьшить ошибку округления. ln2_lo + ln2_hi = ln(2). Он разделен на малую и большую константу, чтобы уменьшить ошибку округления.

// res is the polynomial(adjusted_mantissa) result
// fe is the float exponent
// x is the adjusted_mantissa.  x2 = x*x;
res  = mul_add(fe, ln2_lo, res);             // res += fe * ln2_lo;
res += nmul_add(x2, 0.5, x);                 // res += x  - 0.5 * x2;
res  = mul_add(fe, ln2_hi, res);             // res += fe * ln2_hi;

Вы можете отказаться от двухэтапного ln2 материала и просто использовать VM_LN2, если вы не стремитесь к точности 0,5 или 1 ulp (или что-то еще, что эта функция фактически обеспечивает; IDK).

Я полагаю, что часть x - 0.5*x2 - это действительно дополнительный полиномиальный член. Это то, что я имел в виду под запеканием логарифмической базы e: вам понадобится коэффициент на этих условиях или избавиться от этой строки и заново подогнать полиномиальные коэффициенты для log2. Вы не можете просто умножить все коэффициенты полинома на константу.

После этого он проверяет отсутствие переполнения, переполнения или денормации и ветвления, если какой-либо элемент в векторе требует специальной обработки для получения правильного NaN или -Inf, а не любого мусора, который мы получили из полинома + экспонента. Если известно, что ваши значения конечны и положительны, вы можете закомментировать эту часть и получить значительное ускорение (даже проверка перед переходом требует нескольких инструкций).


Дальнейшее чтение:

  • http://gallium.inria.fr/blog/fast-vectorizable-math-approx/ кое-что о том, как оценивать относительную и абсолютную ошибку в полиномиальном приближении и выполнять минимаксную фиксацию коэффициентов вместо простого использования разложения в ряд Тейлора.

  • http://www.machinedlearnings.com/2011/06/fast-approximate-logarithm-exponential.html интересный подход: он набирает float на uint32_t, а преобразует это целое число в float. Поскольку двоичные 32 числа с плавающей запятой IEEE хранят показатель степени в более высоких битах, чем мантисса, результирующий float в основном представляет собой значение показателя, масштабированное на 1 << 23, но также содержащее информацию из мантиссы.

    Затем он использует выражение с парой коэффициентов, чтобы исправить ситуацию и получить приближение log(). Он включает деление на (constant + mantissa), чтобы исправить загрязнение мантиссы при преобразовании битового шаблона с плавающей запятой в float. Я обнаружил, что векторизованная версия этого была медленнее и менее точна с AVX2 на HSW и SKL, чем JRF fastlog с полиномами 4-го порядка. (Особенно при использовании его как части быстрого arcsinh, в котором также используется блок деления для vsqrtps.)

person Peter Cordes    schedule 20.08.2017
comment
Я уточнил диапазон и точность в вопросе. - person Serge Rogatch; 21.08.2017
comment
@SergeRogatch: если вам не нужна почти полная 53-битная точность, подойдет простой полином (или соотношение двух полиномов). Вы, вероятно, можете опустить все уловки порядка добавления, которые использует VCL. Выберите что-то вроде версии JRF float, но с соотношением двух полиномов 4-го или 5-го порядка. (И, вероятно, все еще с этим poly * (mantissa - 1.0) в конце, чтобы убедиться, что он обнуляется, когда должен). - person Peter Cordes; 21.08.2017

Наконец, вот мой лучший результат, который на Ryzen 1800X @ 3,6 ГГц дает около 0,8 миллиарда логарифмов в секунду (200 миллионов векторов по 4 логарифма в каждом) в одном потоке и является точным до нескольких последних бит в мантиссе. Спойлер: посмотрим напоследок, как увеличить производительность до 0,87 миллиарда логарифмов в секунду.

Особые случаи: отрицательные числа, отрицательная бесконечность и NaN с битом отрицательного знака обрабатываются так, как если бы они были очень близки к 0 (что приводит к появлению большого количества отрицательных логарифмических значений для мусора). Положительная бесконечность и NaN с положительным битом знака приводят к логарифму около 1024. Если вам не нравится, как обрабатываются особые случаи, можно добавить код, который проверяет их и делает то, что вам больше подходит. Это замедлит вычисления.

namespace {
  // The limit is 19 because we process only high 32 bits of doubles, and out of
  //   20 bits of mantissa there, 1 bit is used for rounding.
  constexpr uint8_t cnLog2TblBits = 10; // 1024 numbers times 8 bytes = 8KB.
  constexpr uint16_t cZeroExp = 1023;
  const __m256i gDoubleNotExp = _mm256_set1_epi64x(~(0x7ffULL << 52));
  const __m256d gDoubleExp0 = _mm256_castsi256_pd(_mm256_set1_epi64x(1023ULL << 52));
  const __m256i cAvxExp2YMask = _mm256_set1_epi64x(
    ~((1ULL << (52-cnLog2TblBits)) - 1) );
  const __m256d cPlusBit = _mm256_castsi256_pd(_mm256_set1_epi64x(
    1ULL << (52 - cnLog2TblBits - 1)));
  const __m256d gCommMul1 = _mm256_set1_pd(2.0 / 0.693147180559945309417); // 2.0/ln(2)
  const __m256i gHigh32Permute = _mm256_set_epi32(0, 0, 0, 0, 7, 5, 3, 1);
  const __m128i cSseMantTblMask = _mm_set1_epi32((1 << cnLog2TblBits) - 1);
  const __m128i gExpNorm0 = _mm_set1_epi32(1023);
  // plus |cnLog2TblBits|th highest mantissa bit
  double gPlusLog2Table[1 << cnLog2TblBits];
} // anonymous namespace

void InitLog2Table() {
  for(uint32_t i=0; i<(1<<cnLog2TblBits); i++) {
    const uint64_t iZp = (uint64_t(cZeroExp) << 52)
      | (uint64_t(i) << (52 - cnLog2TblBits)) | (1ULL << (52 - cnLog2TblBits - 1));
    const double zp = *reinterpret_cast<const double*>(&iZp);
    const double l2zp = std::log2(zp);
    gPlusLog2Table[i] = l2zp;
  }
}

__m256d __vectorcall Log2TblPlus(__m256d x) {
  const __m256d zClearExp = _mm256_and_pd(_mm256_castsi256_pd(gDoubleNotExp), x);
  const __m256d z = _mm256_or_pd(zClearExp, gDoubleExp0);

  const __m128i high32 = _mm256_castsi256_si128(_mm256_permutevar8x32_epi32(
    _mm256_castpd_si256(x), gHigh32Permute));
  // This requires that x is non-negative, because the sign bit is not cleared before
  //   computing the exponent.
  const __m128i exps32 = _mm_srai_epi32(high32, 20);
  const __m128i normExps = _mm_sub_epi32(exps32, gExpNorm0);

  // Compute y as approximately equal to log2(z)
  const __m128i indexes = _mm_and_si128(cSseMantTblMask,
    _mm_srai_epi32(high32, 20 - cnLog2TblBits));
  const __m256d y = _mm256_i32gather_pd(gPlusLog2Table, indexes,
    /*number of bytes per item*/ 8);
  // Compute A as z/exp2(y)
  const __m256d exp2_Y = _mm256_or_pd(
    cPlusBit, _mm256_and_pd(z, _mm256_castsi256_pd(cAvxExp2YMask)));

  // Calculate t=(A-1)/(A+1). Both numerator and denominator would be divided by exp2_Y
  const __m256d tNum = _mm256_sub_pd(z, exp2_Y);
  const __m256d tDen = _mm256_add_pd(z, exp2_Y);

  // Compute the first polynomial term from "More efficient series" of https://en.wikipedia.org/wiki/Logarithm#Power_series
  const __m256d t = _mm256_div_pd(tNum, tDen);

  const __m256d log2_z = _mm256_fmadd_pd(t, gCommMul1, y);

  // Leading integer part for the logarithm
  const __m256d leading = _mm256_cvtepi32_pd(normExps);

  const __m256d log2_x = _mm256_add_pd(log2_z, leading);
  return log2_x;
}

Он использует комбинацию подхода таблицы поиска и полинома 1-й степени, в основном описанного в Википедии (ссылка находится в комментариях к коду). Я могу позволить себе выделить здесь 8 КБ кэша L1 (что составляет половину от 16 КБ кеша L1, доступного на логическое ядро), потому что вычисление логарифмов на самом деле является узким местом для меня, и нет ничего, что требует кеш L1.

Однако, если вам нужно больше кеша L1 для других нужд, вы можете уменьшить объем кеша, используемый алгоритмом логарифмирования, уменьшив cnLog2TblBits до, например, 5 за счет уменьшения точности вычисления логарифма.

Или, чтобы сохранить высокую точность, вы можете увеличить количество полиномиальных членов, добавив:

namespace {
  // ...
  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 gCoeff5 = _mm256_set1_pd(1.0 / 11);
}

А затем измените хвост Log2TblPlus() после строки 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 t11 = _mm256_mul_pd(t9, t2); // t**11
  const __m256d terms012345 = _mm256_fmadd_pd(gCoeff5, t11, terms01234);

  const __m256d log2_z = _mm256_fmadd_pd(terms012345, gCommMul1, y);

Затем следует комментарий // Leading integer part for the logarithm и остальные без изменений.

Обычно вам не нужно так много терминов, даже для нескольких битов таблицы, я просто предоставил коэффициенты и вычисления для справки. Скорее всего, если cnLog2TblBits==5, вам ничего не понадобится, кроме terms012. Но я таких замеров не проводил, нужно поэкспериментировать, что вам подходит.

Очевидно, что чем меньше полиномиальных членов вы вычисляете, тем быстрее будут вычисления.


ИЗМЕНИТЬ: этот вопрос В какой ситуации инструкции по сбору AVX2 будут быстрее, чем индивидуальная загрузка данных? предполагает, что вы можете улучшить производительность, если

const __m256d y = _mm256_i32gather_pd(gPlusLog2Table, indexes,
  /*number of bytes per item*/ 8);

заменяется на

const __m256d y = _mm256_set_pd(gPlusLog2Table[indexes.m128i_u32[3]],
  gPlusLog2Table[indexes.m128i_u32[2]],
  gPlusLog2Table[indexes.m128i_u32[1]],
  gPlusLog2Table[indexes.m128i_u32[0]]);

Для моей реализации он экономит около 1,5 цикла, уменьшая общее количество циклов для вычисления 4 логарифмов с 18 до 16,5, таким образом, производительность возрастает до 0,87 миллиарда логарифмов в секунду. Я оставляю текущую реализацию как есть, потому что она более идиоматична и должна быть быстрее, когда процессоры начнут правильно выполнять gather операции (с объединением, как это делают графические процессоры).

РЕДАКТИРОВАТЬ2: на процессорах Ryzen (но не на Intel) вы можете получить немного большее ускорение (около 0,5 цикла), заменив

const __m128i high32 = _mm256_castsi256_si128(_mm256_permutevar8x32_epi32(
  _mm256_castpd_si256(x), gHigh32Permute));

с участием

  const __m128 hiLane = _mm_castpd_ps(_mm256_extractf128_pd(x, 1));
  const __m128 loLane = _mm_castpd_ps(_mm256_castpd256_pd128(x));
  const __m128i high32 = _mm_castps_si128(_mm_shuffle_ps(loLane, hiLane,
    _MM_SHUFFLE(3, 1, 3, 1)));
person Serge Rogatch    schedule 26.08.2017
comment
Skylake уже имеет эффективные сборки (одна vgatherdpd ymm на 4 цикла по сравнению с пропускной способностью Ryzen 12c). Я удивлен, что поиск в таблице был лучше, чем полином большего размера. Я предполагаю, что это только в микробенчмарке или в цикле, который в основном выполняет log() вычисления в течение очень долгого времени. И double действительно нужны полиномы побольше, так что, может быть, это не так уж и безумно. Кроме того, у Ryzen примерно половина пропускной способности FMA, чем у Intel. - person Peter Cordes; 27.08.2017
comment
Для процессоров Intel было бы более эффективно использовать векторы i64gather_pd и 256b вместо упаковки в __m128i для i32gather_pd. например используйте exps32 = _mm_srli_epi64(x, 20 + 32);. (Почему вы использовали арифметический сдвиг вместо логического? Вам нужна была широковещательная передача знаковых битов)? Однако извлечение high32 может быть полезно для Ryzen, потому что векторные инструкции 256b - это 2 мопа вместо 1. Таким образом, вы тратите 2 мопа на извлечение, чтобы сэкономить 4 мопа на __m128i инструкциях вместо __m256i - person Peter Cordes; 27.08.2017
comment
Использование глобальных констант const __m256d, вероятно, бесполезно. Вы могли подумать, что так будет лучше, но на самом деле вы получаете конструктор, который копирует из .rodata в хранилище для этих const __m256d переменных. т.е. у них есть непостоянные инициализаторы, потому что _mm_set не оптимизируется в глобальной области :( См. _GLOBAL__sub_I__Z13InitLog2Tablev в godbolt.org / g / x8aW62. Обычно лучше всего записывать векторные константы внутри функции, и позволить компилятору обрабатывать их так же, как он обращается со строковыми литералами, такими как "hello", в нескольких функциях. - person Peter Cordes; 27.08.2017
comment
OTOH, может быть, это то, что вы хотите, если он помещает константы в память рядом с вашей LUT для лучшей локальности. - person Peter Cordes; 27.08.2017
comment
@PeterCordes, мне все равно нужны 32-битные числа для _mm256_cvtepi32_pd(normExps) (потому что, похоже, нет его 64-битной версии), поэтому я подумал, что 128-битные векторные операции будут быстрее, и использовал их там, где это возможно. Я использовал арифметический сдвиг (с широковещательной передачей знаков), чтобы получить то, что мне больше подходит для отрицательного x: большой отрицательный логарифм, как если бы x был положительным и очень близким к 0. Является ли арифметический сдвиг медленнее логического? Где вы ищете задержки и пропускную способность? И да, мне нужна лучшая локализация для констант. Встроенные вызовы _set1_, похоже, дают такую ​​же производительность. - person Serge Rogatch; 27.08.2017
comment
Арифметика и логика - это одно и то же (конечно, согласно таблицам Агнера Фога). Но нет VPSRAQ, только размеры W / D для арифметики. В процессорах Intel скорость векторных операций 256b равна скорости 128b. Но вы правы, что 128b на Ryzen быстрее. В любом случае, вы правы, что _mm256_cvtepi32_pd в какой-то момент требует упаковки до 128b. С AVX512 вы можете держать его в полосе движения и использовать cvtepi64_pd. - person Peter Cordes; 27.08.2017
comment
Если сбор данных является частью критического пути, это уменьшит задержку, чтобы индексы были готовы раньше, за счет использования i64gather (без перемешивания, только с большим количеством сдвигов. Или, если вам действительно нужен арифметический сдвиг, вы можете выполнить арифметические операции ›› 20, а затем логический ›› 32) - person Peter Cordes; 27.08.2017
comment
re: константы: если вы тестируете в цикле, хороший компилятор поднимет нагрузку на большинство из них (в регистры вне цикла), поэтому не имеет значения, находятся они рядом с таблицей или нет. Если они не все подходят, некоторые из них должны оставаться горячими в кеше. - person Peter Cordes; 27.08.2017
comment
@PeterCordes, да, по крайней мере, в тестовом коде дизассемблирование показывает, что константы просто остаются в регистрах. Это производственный код, который, скорее всего, не сможет хранить их в реестрах. Рабочий код будет вычислять энтропии тысяч массивов, каждый из которых содержит миллионы элементов. Таким образом, он будет выполнять миллиарды H += P[i]*log2(P[i]) вычислений, поэтому эта реализация мне подходит, если LUT и константы помещаются в кеш L1. - person Serge Rogatch; 27.08.2017