Эффективное сравнение с плавающей запятой (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, не имеет значения, сравниваю ли я целые числа, описанным способом или плавающими.


person Alex    schedule 30.04.2012    source источник
comment
Людям на codereview.stackexchange.com, возможно, придется что-то сказать и по этому поводу.   -  person PlasmaHH    schedule 30.04.2012
comment
Ваш код несовместим с вашей постановкой проблемы - данные являются плавающими, но вы показываете порог как int - вы также приводите каждое значение данных с плавающей запятой к int - почему? Если у вас данные с плавающей запятой, то порог должен быть с плавающей запятой, и вы должны выполнить сравнение с плавающей запятой (т.е. без преобразований int-float). Кроме того, что вы планируете делать со значениями, превышающими (или меньшими) пороговыми значениями (это определит, подходит ли NEON или нет)?   -  person Paul R    schedule 30.04.2012
comment
Многие люди отказываются от NEON из-за того, что он медленнее, чем ARM, не зная, чего следует избегать и как правильно программировать SIMD. В зависимости от того, что именно вы хотите, либо начать с SIMD нереально, либо вы не знаете, как справиться с if-else с NEON.   -  person Jake 'Alquimista' LEE    schedule 02.05.2012
comment
if-else на NEON: 1. Создайте значение маски с помощью инструкции VCnn. 2. вычислить результаты для обоих случаев (if, else) в двух разных регистрах. 3. объедините оба результата с инструкцией VBnn, используя значение маски от 1.   -  person Jake 'Alquimista' LEE    schedule 02.05.2012
comment
Поскольку вам всегда нужно рассчитывать оба случая на NEON, независимо от выполняемого условия, это может быть медленнее, чем на ARM, если вычисления очень сложные. Если есть несколько связанных if-elses, забудьте о NEON.   -  person Jake 'Alquimista' LEE    schedule 02.05.2012
comment
@ Jake'Alquimista'LEE, к сожалению, я не могу забыть о NEON, так как мне приходится использовать его для всех вычислений с плавающей запятой.   -  person Alex    schedule 02.05.2012
comment
Передачи регистров NEON- ›ARM всегда страдают от 11 до 14 циклов. Этого следует избегать любой ценой внутри петель. И зачем это приведение типов, когда можно очень быстро сравнивать числа с плавающей запятой, даже без ошибок округления? Я покажу вам, как это сделать, в отдельном ответе.   -  person Jake 'Alquimista' LEE    schedule 02.05.2012


Ответы (4)


Если числа с плавающей запятой являются 32-битными IEEE-754, а целые числа тоже 32-битными, и если нет значений + infinity, -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 ++ в приведенном выше коде, вы можете переписать код на ассемблере.

person Alexey Frunze    schedule 30.04.2012
comment
Вроде отличная идея. У меня все еще проблема с vmov.32, но в основном это хорошая идея. Спасибо. - person Alex; 30.04.2012
comment
@vasile: О какой ошибке ты говоришь? Что сложного? Если да, то как сделать это проще? - person Alexey Frunze; 30.04.2012
comment
Я имел в виду ответ @ Paul-R. - person Sam; 02.05.2012

Если ваши данные являются плавающими, вам следует провести сравнение с плавающими, например

float arr[10000];
float threshold;
....

float a = arr[20]; // e.g.
if (threshold > a) {....}

в противном случае у вас будут дорогостоящие преобразования типа float-int.

person Paul R    schedule 30.04.2012
comment
Если я сравниваю 2 числа с плавающей запятой, это вызывает дорогостоящую передачу регистров флагов. Вот почему я попытался сравнить 2 целых числа. - person Alex; 30.04.2012
comment
Какие операции вы впоследствии выполняете, когда пороговая проверка верна / ложна? - person Paul R; 30.04.2012
comment
vcmpe.f32 s17, s16 vmrs APSR_nzcv, fpscr Если я понял ваш вопрос. - person Alex; 30.04.2012
comment
Алекс: я имел в виду: что происходит в { ... } после вашего порогового теста? Это определит, уместно ли использовать NEON для теста. - person Paul R; 30.04.2012
comment
После теста много кода, поэтому я пометил его как .... - person Alex; 30.04.2012
comment
Что ж, если кода много, тогда стоимость теста должна быть незначительной (если только подавляющее большинство точек данных не нуждается в обработке?). - person Paul R; 30.04.2012

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

Он загружает значение с помощью NEON, чтобы преобразовать его в int, затем выполняет передачу NEON-> ARM, которая вызывает очистку конвейера, что приводит к потере 11 ~ 14 циклов.

Лучшим решением было бы написать функцию полностью в ручной сборке.

Однако есть простой трюк, который позволяет быстро сравнивать числа с плавающей запятой без преобразования типов И усечения:

Положительный порог (точно так же, как сравнение int):

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

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

person Jake 'Alquimista' LEE    schedule 02.05.2012

Если ошибки округления не имеют значения, вам следует использовать std :: lrint.

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

person BЈовић    schedule 30.04.2012