Самый быстрый способ вычисления квадрата расстояния

Мой код в значительной степени зависит от вычисления расстояний между двумя точками в трехмерном пространстве. Чтобы избежать дорогого квадратного корня, я использую квадрат расстояния. Но все же это занимает значительную часть вычислительного времени, и я хотел бы заменить мою простую функцию чем-то еще быстрее. Теперь у меня есть:

double distance_squared(double *a, double *b)
{
  double dx = a[0] - b[0];
  double dy = a[1] - b[1];
  double dz = a[2] - b[2];

  return dx*dx + dy*dy + dz*dz;
}

Я также попытался использовать макрос, чтобы избежать вызова функции, но это мало помогает.

#define DISTANCE_SQUARED(a, b) ((a)[0]-(b)[0])*((a)[0]-(b)[0]) + ((a)[1]-(b)[1])*((a)[1]-(b)[1]) + ((a)[2]-(b)[2])*((a)[2]-(b)[2])

Я думал об использовании инструкций SIMD, но не смог найти хороший пример или полный список инструкций (в идеале, некоторые умножить + добавить на два вектора).

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

Каким будет самый быстрый способ вычисления квадрата расстояния?

Ответы

Ответ 1

Хороший компилятор оптимизирует это, как и вы когда-либо справляетесь. Хороший компилятор будет использовать инструкции SIMD, если он сочтет, что они будут полезными. Убедитесь, что вы включили все такие возможные оптимизации для своего компилятора. К сожалению, векторы размерности 3 не склонны хорошо сидеть с SIMD-модулями.

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

Ответ 2

Первым очевидным было бы использовать ключевое слово restrict.

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

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

Далее, если вы можете жить с этим, используйте float вместо double и добавьте 4 поплавков, даже если один из них не используется, это более "естественный" макет данных для большинства процессоров (это несколько специфические для платформы, но 4 поплавки - это хорошая догадка для большинства платформ - 3 удвоения, а также 1,5 SIMD-регистра на "типичных" процессорах, нигде не оптимальны).

(Для рукописной реализации SIMD (что сложнее, чем вы думаете) сначала и до того, как все должны быть выровнены. Затем посмотрите, какие задержки у вас есть на целевом компьютере и сделайте самые длинные первые Например, в pre-Prescott Intel имеет смысл сначала перетасовать каждый компонент в регистр, а затем размножаться вместе с ним, даже если он использует 3 умножения вместо одного, поскольку тасования имеют длительную задержку. На более поздних моделях тасование принимает один цикл, так что это будет полная анти-оптимизация.
Что еще раз показывает, что оставить его компилятору не такая уж плохая идея.)

Ответ 3

Код SIMD для этого (с использованием SSE3):

movaps xmm0,a
movaps xmm1,b
subps xmm0,xmm1
mulps xmm0,xmm0
haddps xmm0,xmm0
haddps xmm0,xmm0

но для этого вам нужны четыре вектора значений (x, y, z, 0). Если у вас есть только три значения, вам нужно немного поиграть, чтобы получить требуемый формат, который отменит любое преимущество выше.

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

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

movaps xmm0,a1
                  movaps xmm2,a2
movaps xmm1,b1
                  movaps xmm3,b2
subps xmm0,xmm1
                  subps xmm2,xmm3
mulps xmm0,xmm0
                  mulps xmm2,xmm2
haddps xmm0,xmm0
                  haddps xmm2,xmm2
haddps xmm0,xmm0
                  haddps xmm2,xmm2

Ответ 4

Если вы хотите что-то оптимизировать, сначала введите код профиля и проверьте выход ассемблера.

После компиляции с gcc -O3 (4.6.1) у нас будет хороший дизассемблированный вывод с SIMD:

movsd   (%rdi), %xmm0
movsd   8(%rdi), %xmm2
subsd   (%rsi), %xmm0
movsd   16(%rdi), %xmm1
subsd   8(%rsi), %xmm2
subsd   16(%rsi), %xmm1
mulsd   %xmm0, %xmm0
mulsd   %xmm2, %xmm2
mulsd   %xmm1, %xmm1
addsd   %xmm2, %xmm0
addsd   %xmm1, %xmm0

Ответ 5

Этот тип проблемы часто встречается в моделях MD. Обычно количество вычислений уменьшается путем отсечки и списков соседей, поэтому число для вычисления уменьшается. Фактический расчет квадратов расстояний, однако, точно выполнен (с оптимизацией компилятора и плавающей точкой фиксированного типа [3]), как указано в вашем вопросе.

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

Ответ 6

Возможно, прохождение 6 удваивается напрямую, поскольку аргументы могут сделать это быстрее (потому что это может избежать разыменования массива):

inline double distsquare_coord(double xa, double ya, double za, 
                               double xb, double yb, double zb) 
{ 
  double dx = xa-yb; double dy=ya-yb; double dz=za-zb;
  return dx*dx + dy*dy + dz*dz; 
}

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

Ответ 7

Если вы можете переставить свои данные для обработки двух пар входных векторов сразу, вы можете использовать этот код (только для SSE2)

// @brief Computes two squared distances between two pairs of 3D vectors
// @param a
//  Pointer to the first pair of 3D vectors.
//  The two vectors must be stored with stride 24, i.e. (a + 3) should point to the first component of the second vector in the pair.
//  Must be aligned by 16 (2 doubles).
// @param b
//  Pointer to the second pairs of 3D vectors.
//  The two vectors must be stored with stride 24, i.e. (a + 3) should point to the first component of the second vector in the pair.
//  Must be aligned by 16 (2 doubles).
// @param c
//  Pointer to the output 2 element array.
//  Must be aligned by 16 (2 doubles).
//  The two distances between a and b vectors will be written to c[0] and c[1] respectively.
void (const double * __restrict__ a, const double * __restrict__ b, double * __restrict c) {
    // diff0 = ( a0.y - b0.y, a0.x - b0.x ) = ( d0.y, d0.x )
    __m128d diff0 = _mm_sub_pd(_mm_load_pd(a), _mm_load_pd(b));
    // diff1 = ( a1.x - b1.x, a0.z - b0.z ) = ( d1.x, d0.z )
    __m128d diff1 = _mm_sub_pd(_mm_load_pd(a + 2), _mm_load_pd(b + 2));
    // diff2 = ( a1.z - b1.z, a1.y - b1.y ) = ( d1.z, d1.y )
    __m128d diff2 = _mm_sub_pd(_mm_load_pd(a + 4), _mm_load_pd(b + 4));

    // prod0 = ( d0.y * d0.y, d0.x * d0.x )
    __m128d prod0 = _mm_mul_pd(diff0, diff0);
    // prod1 = ( d1.x * d1.x, d0.z * d0.z )
    __m128d prod1 = _mm_mul_pd(diff1, diff1);
    // prod2 = ( d1.z * d1.z, d1.y * d1.y )
    __m128d prod2 = _mm_mul_pd(diff1, diff1);

    // _mm_unpacklo_pd(prod0, prod2) = ( d1.y * d1.y, d0.x * d0.x )
    // psum = ( d1.x * d1.x + d1.y * d1.y, d0.x * d0.x + d0.z * d0.z )
    __m128d psum = _mm_add_pd(_mm_unpacklo_pd(prod0, prod2), prod1);

    // _mm_unpackhi_pd(prod0, prod2) = ( d1.z * d1.z, d0.y * d0.y )
    // dotprod = ( d1.x * d1.x + d1.y * d1.y + d1.z * d1.z, d0.x * d0.x + d0.y * d0.y + d0.z * d0.z )
    __m128d dotprod = _mm_add_pd(_mm_unpackhi_pd(prod0, prod2), psum);

    __mm_store_pd(c, dotprod);
}