Быстрое взвешенное среднее & amp; Дисперсия 10 бункеров

Я хотел бы ускорить часть моего кода, но я не думаю, что есть лучший способ сделать следующий расчет:

float invSum = 1.0f / float(sum);

for (int i = 0; i < numBins; ++i)
{
histVec[i] *= invSum;
}

for (int i = 0; i < numBins; ++i)
{
float midPoint = (float)i*binSize + binOffset;
float f = histVec[i];
fmean += f * midPoint;
}

for (int i = 0; i < numBins; ++i)
{
float midPoint = (float)i*binSize + binOffset;
float f = histVec[i];
float diff = midPoint - fmean;
var += f * hwk::sqr(diff);
}

numBins в циклах for обычно 10, но этот бит кода вызывается очень часто (частота 80 кадров в секунду, вызываемая как минимум 8 раз на кадр)

Я пытался использовать некоторые методы SSE, но это лишь немного ускоряет этот код. Я думаю, что я мог бы избежать расчета дважды MidPoint, но я не уверен, как. Есть ли лучший способ вычислить fmean и var?

Вот код SSE:

// make hist contain a multiple of 4 valid values
for (int i = numBins; i < ((numBins + 3) & ~3); i++)
hist[i] = 0;

// find sum of bins in inHist
__m128i iSum4 = _mm_set1_epi32(0);
for (int i = 0; i < numBins; i += 4)
{
__m128i a = *((__m128i *) &inHist[i]);
iSum4 = _mm_add_epi32(iSum4, a);
}

int iSum = iSum4.m128i_i32[0] + iSum4.m128i_i32[1] + iSum4.m128i_i32[2] + iSum4.m128i_i32[3];

//float stdevB, meanB;

if (iSum == 0.0f)
{
stdev = 0.0;
mean = 0.0;
}
else
{
// Set histVec to normalised values in inHist
__m128 invSum = _mm_set1_ps(1.0f / float(iSum));
for (int i = 0; i < numBins; i += 4)
{
__m128i a = *((__m128i *) &inHist[i]);
__m128  b = _mm_cvtepi32_ps(a);
__m128  c = _mm_mul_ps(b, invSum);
_mm_store_ps(&histVec[i], c);
}

float binSize = 256.0f / (float)numBins;
float halfBinSize = binSize * 0.5f;
float binOffset = halfBinSize;

__m128 binSizeMask = _mm_set1_ps(binSize);
__m128 binOffsetMask = _mm_set1_ps(binOffset);
__m128 fmean4 = _mm_set1_ps(0.0f);
for (int i = 0; i < numBins; i += 4)
{
__m128i idx4 = _mm_set_epi32(i + 3, i + 2, i + 1, i);
__m128  idx_m128 = _mm_cvtepi32_ps(idx4);
__m128  histVec4 = _mm_load_ps(&histVec[i]);
__m128  midPoint4 = _mm_add_ps(_mm_mul_ps(idx_m128, binSizeMask), binOffsetMask);
fmean4 = _mm_add_ps(fmean4, _mm_mul_ps(histVec4, midPoint4));
}
fmean4 = _mm_hadd_ps(fmean4, fmean4); // 01 23 01 23
fmean4 = _mm_hadd_ps(fmean4, fmean4); // 0123 0123 0123 0123

float fmean = fmean4.m128_f32[0];

//fmean4 = _mm_set1_ps(fmean);
__m128 var4 = _mm_set1_ps(0.0f);
for (int i = 0; i < numBins; i+=4)
{
__m128i idx4 = _mm_set_epi32(i + 3, i + 2, i + 1, i);
__m128  idx_m128 = _mm_cvtepi32_ps(idx4);
__m128  histVec4 = _mm_load_ps(&histVec[i]);
__m128  midPoint4 = _mm_add_ps(_mm_mul_ps(idx_m128, binSizeMask), binOffsetMask);
__m128  diff4 = _mm_sub_ps(midPoint4, fmean4);
var4 = _mm_add_ps(var4, _mm_mul_ps(histVec4, _mm_mul_ps(diff4, diff4)));
}

var4 = _mm_hadd_ps(var4, var4); // 01 23 01 23
var4 = _mm_hadd_ps(var4, var4); // 0123 0123 0123 0123
float var = var4.m128_f32[0];

stdev = sqrt(var);
mean = fmean;
}

Возможно, я делаю что-то не так, так как у меня не так много улучшений, как я ожидал.
Есть ли в коде SSE что-то, что может замедлить процесс?

(примечание редактора: SSE-часть этого вопроса изначально задавалась как https://stackoverflow.com/questions/31837817/foor-loop-optimisation-sse-comparison, который был закрыт как дубликат.)

5

Решение

Я только что понял, что ваш массив данных начинается с массива int, поскольку в вашем коде не было объявлений. В версии SSE я вижу, что вы начинаете с целых чисел, а потом сохраняете их версию с плавающей запятой позже.

Сохранение целого числа позволит нам сделать цикл-счетчик-вектор простым ivec = _mm_add_epi32(ivec, _mm_set1_epi32(4)); В ответе Аки Суйхконена есть некоторые изменения, которые должны позволить ему намного лучше оптимизироваться. В частности, авто-векторизатор должен иметь возможность делать больше даже без -ffast-math, На самом деле, это довольно хорошо. Вы могли бы сделать лучше с внутренностями, особенно сохранение некоторого вектора 32-битных умножений и сокращение цепочки зависимостей.


Мой старый ответ, основанный на попытках оптимизировать ваш код, как написано, при условии ввода FP:

Вы можете объединить все 3 цикла в один, используя алгоритм @Jason связан с. Однако это может быть не выгодно, так как включает в себя разделение. Для небольшого количества бункеров, вероятно, просто зациклите несколько раз.

Начните с чтения руководств в http://agner.org/optimize/. Несколько приемов из его руководства по оптимизации сборки ускорят вашу попытку SSE (которую я отредактировал для вас в этом вопросе).

  • по возможности объединяйте свои циклы, чтобы вы делали больше с данными при каждой их загрузке / хранении.

  • несколько аккумуляторов, чтобы скрыть задержку циклических цепочек зависимостей. (Даже добавление FP занимает 3 цикла на последних процессорах Intel.) Это не относится к очень коротким массивам, подобным вашему.

  • вместо преобразования int-> float на каждой итерации используйте счетчик цикла float, а также счетчик цикла int. (добавить вектор _mm_set1_ps(4.0f) каждая итерация.) _mm_set... с переменными args это то, чего следует избегать в циклах, когда это возможно. Требуется несколько инструкций (особенно когда каждый аргумент setr должен быть рассчитан отдельно.)

НКУ -O3 удается автоматически векторизовать первый цикл, но не остальные. С -O3 -ffast-math, это авто-векторизация больше. -ffast-math позволяет выполнять операции FP в порядке, отличном от указанного в коде. например складывая массив в 4 элемента вектора, и объединяя только 4 аккумулятора в конце.

Указание gcc, что входной указатель выровнен на 16, позволяет gcc автоматически векторизовать с гораздо меньшими накладными расходами (без скалярных циклов для невыровненных частей).

// return mean
float fpstats(float histVec[], float sum, float binSize, float binOffset, long numBins, float *variance_p)
{
numBins += 3;
numBins &= ~3;  // round up to multiple of 4.  This is just a quick hack to make the code fast and simple.
histVec = (float*)__builtin_assume_aligned(histVec, 16);

float invSum = 1.0f / float(sum);
float var = 0, fmean = 0;

for (int i = 0; i < numBins; ++i)
{
histVec[i] *= invSum;
float midPoint = (float)i*binSize + binOffset;
float f = histVec[i];
fmean += f * midPoint;
}

for (int i = 0; i < numBins; ++i)
{
float midPoint = (float)i*binSize + binOffset;
float f = histVec[i];
float diff = midPoint - fmean;
//        var += f * hwk::sqr(diff);
var += f * (diff * diff);
}
*variance_p = var;
return fmean;
}

gcc генерирует какой-то странный код для второго цикла.

        # broadcasting fmean after the 1st loop
subss   %xmm0, %xmm2    # fmean, D.2466
shufps  $0, %xmm2, %xmm2        # vect_cst_.16
.L5: ## top of 2nd loop
movdqa  %xmm3, %xmm5    # vect_vec_iv_.8, vect_vec_iv_.8
cvtdq2ps        %xmm3, %xmm3    # vect_vec_iv_.8, vect__32.9
movq    %rcx, %rsi      # D.2465, D.2467
addq    $1, %rcx        #, D.2465
mulps   %xmm1, %xmm3    # vect_cst_.11, vect__33.10
salq    $4, %rsi        #, D.2467
paddd   %xmm7, %xmm5    # vect_cst_.7, vect_vec_iv_.8
addps   %xmm2, %xmm3    # vect_cst_.16, vect_diff_39.15
mulps   %xmm3, %xmm3    # vect_diff_39.15, vect_powmult_53.17
mulps   (%rdi,%rsi), %xmm3      # MEM[base: histVec_10, index: _107, offset: 0B], vect__41.18
addps   %xmm3, %xmm4    # vect__41.18, vect_var_42.19
cmpq    %rcx, %rax      # D.2465, bnd.26
ja      .L8     #,   ### <--- This is insane.
haddps  %xmm4, %xmm4    # vect_var_42.19, tmp160
haddps  %xmm4, %xmm4    # tmp160, vect_var_42.21
.L2:
movss   %xmm4, (%rdx)   # var, *variance_p_44(D)
ret
.p2align 4,,10
.p2align 3
.L8:
movdqa  %xmm5, %xmm3    # vect_vec_iv_.8, vect_vec_iv_.8
jmp     .L5     #

Таким образом, вместо того, чтобы просто возвращаться к вершине на каждой итерации, gcc решает перейти вперед, чтобы скопировать регистр, а затем безоговорочно jmp вернуться к вершине петли. Буфер цикла uop может удалить внешние издержки этой глупости, но gcc должен был структурировать цикл так, чтобы он не копировал xmm5-> xmm3 и затем xmm3-> xmm5 каждую итерацию, потому что это глупо. Он должен иметь условный переход просто на вершину цикла.

Также обратите внимание на метод gcc, используемый для получения плавающей версии счетчика цикла: начните с целочисленного вектора 1 2 3 4, и добавить set1_epi32(4), Используйте это как вход для упакованного int-> float cvtdq2ps, На Intel HW эта инструкция выполняется на порте FP-add и имеет задержку в 3 цикла, такую ​​же, как у упакованного FP-add. gcc prob. было бы лучше просто добавить вектор set1_ps(4.0)даже при том, что это создает цепочку зависимостей с 3 циклами, переносимую циклом, вместо 1 вектора цикла int add с 3-тактным преобразованием, отбрасываемым на каждой итерации.

маленький счетчик итераций

Вы говорите, что это часто будет использоваться ровно на 10 корзин? Специализированная версия всего для 10 бинов может значительно ускорить процесс, исключая все издержки цикла и сохраняя все в регистрах.

При таком небольшом размере проблемы вы можете иметь веса FP, просто находящиеся в памяти, вместо того, чтобы каждый раз пересчитывать их с целочисленным преобразованием с плавающей запятой.

Кроме того, 10 бинов будет означать много горизонтальных операций по сравнению с количеством вертикальных операций, так как у вас есть только данные на два с половиной вектора.

Если именно 10 действительно распространено, специализируйте версию для этого. Если до 16 лет является распространенным, специализировать версию для этого. (Они могут и должны поделиться const float weights[] = { 0.0f, 1.0f, 2.0f, ...}; массив).

Возможно, вы захотите использовать встроенные функции для специализированных небольших проблемных версий, а не автоматическую векторизацию.

Наличие нулевого отступа после окончания полезных данных в вашем массиве все еще может быть хорошей идеей в ваших специализированных версиях. Тем не менее, вы можете загрузить последние 2 числа и очистить верхние 64b векторного регистра с помощью movq инструкция. (__m128i _mm_cvtsi64_si128 (__int64 a)). В ролях это __m128 и ты в порядке.

5

Другие решения

Как отметил Петерчен, эти операции очень просты для современных процессоров для настольных ПК. Функция является линейной, то есть O (n). Каков типичный размер numBins? Если оно довольно большое (скажем, более 1000000), распараллеливание поможет. Это может быть просто с использованием библиотеки, такой как OpenMP. Если numBins начинает приближаться MAXINTВы можете рассмотреть GPGPU в качестве опции (CUDA / OpenCL).

Учитывая все вышесказанное, вы должны попробовать профилировать ваше приложение. Скорее всего, если есть ограничение производительности, это не относится к этому методу. Определение Майкла Абраша «высокопроизводительного кода» очень помогло мне определить, когда оптимизировать:

Прежде чем мы сможем создать высокопроизводительный код, мы должны понять, что такое высокая производительность. Цель (не всегда достигнутая) в создании высокопроизводительного программного обеспечения состоит в том, чтобы сделать программное обеспечение способным выполнять поставленные перед ним задачи настолько быстро, что оно мгновенно реагирует на запросы пользователя. Другими словами, высокопроизводительный код в идеале должен выполняться так быстро, что дальнейшее улучшение кода будет бессмысленным. Обратите внимание, что вышеприведенное определение совершенно не говорит о том, чтобы сделать программное обеспечение максимально быстрым.

Ссылка:
Черная книга по программированию графики

3

Общая функция для расчета

std = sqrt(SUM_i { hist[i]/sum * (midpoint_i - mean_midpoint)^2 })

Используя личность

Var (aX + b) = Var (X) * a^2

можно значительно снизить сложность всей операции

1) средняя точка ячейки не нуждается в смещении b
2) нет необходимости предварительно масштабировать элементы массива бинов с шириной бина

а также

3) нет необходимости нормализовать записи гистограммы с обратной суммой

Оптимизированный расчет выглядит следующим образом

float calcVariance(int histBin[], float binWidth)
{
int i;
int sum = 0;
int mid = 0;
int var = 0;
for (i = 0; i < 10; i++)
{
sum += histBin[i];
mid += i*histBin[i];
}

float inv_sum = 1.0f / (float)sum;
float mid_sum = mid * inv_sum;

for (i = 0; i < 10; i++)
{
int diff = i * sum - mid;  // because mid is prescaled by sum
var += histBin[i] * diff * diff;
}

return sqrt(float(var) / (float)(sum * sum * sum)) * binWidth;
}

Незначительные изменения требуются, если это float histBin[];

Также я добавляю размер дополнения HistBin кратным 4 для лучшей векторизации.

РЕДАКТИРОВАТЬ

Другой способ вычислить это с помощью числа с плавающей точкой во внутреннем цикле:

 float inv_sum = 1.0f / (float)sum;
float mid_sum = mid * inv_sum;
float var = 0.0f;

for (i = 0; i < 10; i++)
{
float diff = (float)i - mid_sum;
var += (float)histBin[i] * diff * diff;
}
return sqrt(var * inv_sum) * binWidth;
2

Выполняйте масштабирование только для глобальных результатов и сохраняйте целые числа как можно дольше.

Сгруппируйте все вычисления в один цикл, используя Σ(X-m)²/N = ΣX²/N - m²,

// Accumulate the histogram
int mean= 0, var= 0;
for (int i = 0; i < numBins; ++i)
{
mean+= i * histVec[i];
var+= i * i * histVec[i];
}

// Compute the reduced mean and variance
float fmean= (float(mean) / sum);
float fvar= float(var) / sum - fmean * fmean;

// Rescale
fmean= fmean * binSize + binOffset;
fvar= fvar * binSize * binSize;

Требуемый целочисленный тип будет зависеть от максимального значения в ячейках. Оптимизация цикла SSE может использовать _mm_madd_epi16 инструкция.

Если количество ячеек не превышает 10, рассмотрите возможность полного развертывания цикла. Предварительно вычислить i а также векторы в таблице.

В удачном случае, когда данные помещаются в 16 бит, а суммы в 32 бита, накопление выполняется с помощью чего-то вроде

    static short I[16]= { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0 };
static short I2[16]= { 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 0, 0, 0, 0, 0, 0 };

// First group
__m128i i= _mm_load_si128((__m128i*)&I[0]);
__m128i i2= _mm_load_si128((__m128i*)&I2[0]);
__m128i h= _mm_load_si128((__m128i*)&inHist[0]);

__m128i mean= _mm_madd_epi16(i, h);
__m128i var= _mm_madd_epi16(i2, h);

// Second group
i= _mm_load_si128((__m128i*)&I[8]);
i2= _mm_load_si128((__m128i*)&I2[8]);
h= _mm_load_si128((__m128i*)&inHist[8]);

mean= _mm_add_epi32(mean, _mm_madd_epi16(i, h));
var= _mm_add_epi32(var, _mm_madd_epi16(i2, h));

ВНИМАНИЕ: не проверено

1
По вопросам рекламы [email protected]