На основании дополнительных комментариев предполагается, что функция, которую вы хотите оптимизировать, выглядит следующим образом:
void euclidean(double x0, double y0, const double *x, const double* y,
const size_t *index, size_t index_size, double *result)
{
for (size_t i = 0; i < index_size; ++i) {
double dx = x0 - x[index[i]];
double dy = y0 - y[index[i]];
result[index[i]] = sqrt(dx*dx + dy * dy);
}
}
Поскольку ваша цель - Pentium, доступны только инструкции SSE2 SIMD. Вы можете попробовать следующую оптимизированную функцию (также доступна здесь ):
void euclidean_sse2(double x0, double y0, const double *x, const double* y,
const size_t *index, size_t index_size, double *result)
{
__m128d vx0 = _mm_set1_pd(x0);
__m128d vy0 = _mm_set1_pd(y0);
for (size_t i = 0; i + 1 < index_size; i += 2) { // process 2 at a time
size_t i0 = index[i];
size_t i1 = index[i+1];
__m128d vx = _mm_set_pd(x[i1], x[i0]); // load 2 scattered elements
__m128d vy = _mm_set_pd(y[i1], y[i0]); // load 2 scattered elements
__m128d vdx = _mm_sub_pd(vx, vx0);
__m128d vdy = _mm_sub_pd(vy, vy0);
__m128d vr = _mm_sqrt_pd(_mm_add_pd(_mm_mul_pd(vdx, vdx), _mm_mul_pd(vdy, vdy)));
_mm_store_sd(result + i0, vr);
_mm_storeh_pd(result + i1, vr);
}
if (index_size & 1) { // if index_size is odd, there is one more element to process
size_t i0 = index[index_size-1];
double dx = x0 - x[i0];
double dy = y0 - y[i0];
result[i0] = sqrt(dx*dx + dy * dy);
}
}
Здесь операции загрузки и сохранения не векторизованы, то есть мы загружаем x
и y
один за другим, и мы сохраняем в result
один за другим. Все остальные операции векторизованы.
При gcc вывод ассемблера тела основного цикла ниже.
// scalar load operations
mov r10, QWORD PTR [rax]
mov r9, QWORD PTR [rax+8]
add rax, 16
movsd xmm3, QWORD PTR [rdi+r10*8]
movsd xmm2, QWORD PTR [rsi+r10*8]
movhpd xmm3, QWORD PTR [rdi+r9*8]
movhpd xmm2, QWORD PTR [rsi+r9*8]
// vectorial operations
subpd xmm3, xmm4
subpd xmm2, xmm5
mulpd xmm3, xmm3
mulpd xmm2, xmm2
addpd xmm2, xmm3
sqrtpd xmm2, xmm2
// scalar store operations
movlpd QWORD PTR [r8+r10*8], xmm2
movhpd QWORD PTR [r8+r9*8], xmm2
Вы могли бы также выполнить некоторое развертывание цикла (это, вероятно, сделал бы векторизатор компилятора). Но в этом случае тело цикла довольно существенно и, вероятно, связано с вводом / выводом памяти, поэтому я не думаю, что это сильно поможет.
Если значение этих функций неясно, вы можете прочитать this .
Всестороннее обсуждение векторизации здесь . Короче говоря, современные процессоры предлагают векторные регистры в дополнение к обычным. В зависимости от архитектуры машины у нас есть 128-битные регистры (инструкции SSE), 256-битные регистры (инструкции AVX), 512-битные регистры (инструкции AVX512). Векторизация означает использование этих регистров в их полном объеме. Например, если у нас есть вектор double
{x0, x1, x2, …. xn}
, и мы хотим добавить его к константе k
, получая {x0+k, x1+k, x2+k, …. xn+k}
, в скалярном режиме операция разлагается на чтение x(i)
, добавление k
, сохранить результат. В векторном режиме с SSE это выглядело бы как чтение x[i]
и x[i+1]
одновременно, добавление k
одновременно, сохранение 2 результатов одновременно.
Если элементы не являются смежными в памяти, мы не можем загружать x [i] и x [i + 1] одновременно, а также не можем сохранять результаты одновременно, но, по крайней мере, мы можем выполнить два сложения одновременно.