Эффективное сравнение с плавающей точкой (Cortex-A8)
Существует большой (~100 000) массив переменных с плавающей запятой, и существует порог (также с плавающей запятой).
Проблема в том, что мне приходится сравнивать каждую переменную из массива с порогом, но передача флагов NEON занимает очень много времени (~20 циклов в соответствии с профилировщиком).
Есть ли эффективный способ сравнить эти значения?
ПРИМЕЧАНИЕ. Поскольку ошибка округления не имеет значения, я попробовал следующее:
float arr[10000];
float threshold;
....
int a = arr[20]; // e.g.
int t = threshold;
if (t > a) {....}
Но в этом случае я получил следующую последовательность команд процессора:
vldr.32 s0, [r0]
vcvt.s32.f32 s0, s0
vmov r0, s0 <--- takes 20 cycles as `vmrs APSR_nzcv, fpscr` in case of
cmp r0, r1 floating point comparison
Поскольку преобразование происходит в NEON, не имеет значения, сравниваю ли я целые числа описанным способом или с плавающей точкой.
4 ответа
Если числа с плавающей запятой - это 32-разрядные IEEE-754, а целые числа тоже 32-разрядные, и если нет + бесконечность, -infinity и NaN
Значения, мы можем сравнить числа с плавающей точкой как небольшую хитрость:
#include <stdio.h>
#include <limits.h>
#include <assert.h>
#define C_ASSERT(expr) extern char CAssertExtern[(expr)?1:-1]
C_ASSERT(sizeof(int) == sizeof(float));
C_ASSERT(sizeof(int) * CHAR_BIT == 32);
int isGreater(float* f1, float* f2)
{
int i1, i2, t1, t2;
i1 = *(int*)f1;
i2 = *(int*)f2;
t1 = i1 >> 31;
i1 = (i1 ^ t1) + (t1 & 0x80000001);
t2 = i2 >> 31;
i2 = (i2 ^ t2) + (t2 & 0x80000001);
return i1 > i2;
}
int main(void)
{
float arr[9] = { -3, -2, -1.5, -1, 0, 1, 1.5, 2, 3 };
float thr;
int i;
// Make sure floats are 32-bit IEE754 and
// reinterpreted as integers as we want/expect
{
static const float testf = 8873283.0f;
unsigned testi = *(unsigned*)&testf;
assert(testi == 0x4B076543);
}
thr = -1.5;
for (i = 0; i < 9; i++)
{
printf("%f %s %f\n", arr[i], "<=\0> " + 3*isGreater(&arr[i], &thr), thr);
}
thr = 1.5;
for (i = 0; i < 9; i++)
{
printf("%f %s %f\n", arr[i], "<=\0> " + 3*isGreater(&arr[i], &thr), thr);
}
return 0;
}
Выход:
-3.000000 <= -1.500000
-2.000000 <= -1.500000
-1.500000 <= -1.500000
-1.000000 > -1.500000
0.000000 > -1.500000
1.000000 > -1.500000
1.500000 > -1.500000
2.000000 > -1.500000
3.000000 > -1.500000
-3.000000 <= 1.500000
-2.000000 <= 1.500000
-1.500000 <= 1.500000
-1.000000 <= 1.500000
0.000000 <= 1.500000
1.000000 <= 1.500000
1.500000 <= 1.500000
2.000000 > 1.500000
3.000000 > 1.500000
Конечно, имеет смысл предварительно рассчитать это окончательное целочисленное значение в isGreater()
это используется в операторе сравнения, если ваш порог не изменяется.
Если вы боитесь неопределенного поведения в C/C++ в приведенном выше коде, вы можете переписать код в сборке.
Если ваши данные являются числами с плавающей точкой, то вы должны сравнить их с числами, например
float arr[10000];
float threshold;
....
float a = arr[20]; // e.g.
if (threshold > a) {....}
в противном случае у вас будут дорогие преобразования с плавающей точкой.
Ваш пример показывает, насколько плохими могут быть сгенерированные компилятором коды:
Он загружает значение с NEON, чтобы преобразовать его в int, затем выполняет передачу NEON->ARM, которая вызывает сброс конвейера, что приводит к потере 11~14 циклов.
Наилучшим решением было бы написание функции полностью в ручной сборке.
Тем не менее, есть простой трюк, который позволяет проводить быстрые сравнения без преобразования типов и усечения:
Порог положительный (точно так же быстро, как и сравнение):
void example(float * pSrc, float threshold, unsigned int count)
{
typedef union {
int ival,
unsigned int uval,
float fval
} unitype;
unitype v, t;
if (count==0) return;
t.fval = threshold;
do {
v.fval = *pSrc++;
if (v.ival < t.ival) {
// your code here
}
else {
// your code here (optional)
}
} while (--count);
}
Порог отрицательный (1 цикл больше на значение, чем сравнение int):
void example(float * pSrc, float threshold, unsigned int count)
{
typedef union {
int ival,
unsigned int uval,
float fval
} unitype;
unitype v, t, temp;
if (count==0) return;
t.fval = threshold;
t.uval &= 0x7fffffff;
do {
v.fval = *pSrc++;
temp.uval = v.uval ^ 0x80000000;
if (temp.ival >= t.ival) {
// your code here
}
else {
// your code here (optional)
}
} while (--count);
}
Я думаю, что это будет намного быстрее, чем принятый выше. Опять я немного опоздал.
Если ошибки округления не имеют значения, вы должны использовать std:: lrint.
Более быстрые преобразования с плавающей точкой в целочисленные рекомендуют использовать его для преобразования с плавающей точкой в int.