Выбор хороших первых оценок для подразделения Goldschmidt

Я вычисляю fixedpoint обратные величины в Q22.10 с подразделением Goldschmidt для использования в моем растрирующем процессоре программного обеспечения на ARM.

Это сделано, просто установив числитель на 1, т.е. числитель становится скаляром на первом повторении. Честно говоря, я отчасти следую алгоритму Википедии вслепую здесь. В статье говорится что, если знаменатель масштабируется в полуоткрытом диапазоне (0.5, 1.0], хорошая первая оценка может быть основана на одном только знаменателе: Позвольте F быть предполагаемым скаляром и D быть знаменателем, затем F = 2 - D.

Но при выполнении этого, я теряю большую точность. Скажите, хочу ли я найти обратную величину 512.00002f. Для уменьшения масштаба числа я теряю 10 битов точности в дробной части, которая переключается на верхний регистр. Так, мои вопросы:

  • Существует ли способ выбрать лучшую оценку, которая не требует нормализации? Почему? Почему нет? Математическое доказательство того, почему это или не возможно, было бы большим.
  • Кроме того, действительно ли возможно предварительно вычислить первые оценки, таким образом, ряд сходится быстрее? Прямо сейчас это сходится после 4-го повторения в среднем. На ARM это - приблизительно ~50 циклов худший случай, и это не принимает во внимание эмуляцию clz/bsr, ни поиски памяти. Если бы это возможно, я хотел бы знать, если выполнение так увеличивает ошибку, и сколько.

Вот мой тестовый сценарий.Примечание: Реализация программного обеспечения clz на строке 13 из моего сообщения здесь. Можно заменить его внутренним, если Вы хотите. clz должен возвратить количество начальных нулей, и 32 для значения 0.

#include 
#include 

const unsigned int BASE = 22ULL;

static unsigned int divfp(unsigned int val, int* iter)
{
  /* Numerator, denominator, estimate scalar and previous denominator */
  unsigned long long N,D,F, DPREV;
  int bitpos;

  *iter = 1;
  D = val;
  /* Get the shift amount + is right-shift, - is left-shift. */
  bitpos = 31 - clz(val) - BASE;
  /* Normalize into the half-range (0.5, 1.0] */
  if(0 < bitpos)
    D >>= bitpos;
  else
    D <<= (-bitpos);

  /* (FNi / FDi) == (FN(i+1) / FD(i+1)) */
  /* F = 2 - D */
  F = (2ULL<>BASE;

  while(1){
    DPREV = D;
    F = (2<<(BASE)) - D;
    D = ((unsigned long long)D*F)>>BASE;
    /* Bail when we get the same value for two denominators in a row.
      This means that the error is too small to make any further progress. */
    if(D == DPREV)
      break;
    N = ((unsigned long long)N*F)>>BASE;
    *iter = *iter + 1;
  }
  if(0 < bitpos)
    N >>= bitpos;
  else
    N <<= (-bitpos);
  return N;
}


int main(int argc, char* argv[])
{
  double fv, fa;
  int iter;
  unsigned int D, result;

  sscanf(argv[1], "%lf", &fv);

  D = fv*(double)(1<

17
задан Community 23 May 2017 в 11:54
поделиться

3 ответа

Я не мог не потратить час на вашу проблему ...

Этот алгоритм описан в разделе 5.5.2 книги «Arithmetique des ordinateurs» Жана-Мишеля Мюллера ( На французском). На самом деле это частный случай итераций Ньютона с 1 в качестве отправной точки. В книге дается простая формулировка алгоритма вычисления N / D с нормализацией D в диапазоне [1 / 2,1 [:

e = 1 - D
Q = N
repeat K times:
  Q = Q * (1+e)
  e = e*e

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

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

После того, как ваше входное значение нормализовано, вам не нужно беспокоиться о значении BASE, пока вы не получите обратное.У вас просто есть 32-битное число X, нормализованное в диапазоне от 0x80000000 до 0xFFFFFFFF, и вычисляется приближение Y = 2 ^ 64 / X (Y не более 2 ^ 33).

Этот упрощенный алгоритм может быть реализован для вашего представления Q22.10 следующим образом:

// Fixed point inversion
// EB Apr 2010

#include <math.h>
#include <stdio.h>

// Number X is represented by integer I: X = I/2^BASE.
// We have (32-BASE) bits in integral part, and BASE bits in fractional part
#define BASE 22
typedef unsigned int uint32;
typedef unsigned long long int uint64;

// Convert FP to/from double (debug)
double toDouble(uint32 fp) { return fp/(double)(1<<BASE); }
uint32 toFP(double x) { return (int)floor(0.5+x*(1<<BASE)); }

// Return inverse of FP
uint32 inverse(uint32 fp)
{
  if (fp == 0) return (uint32)-1; // invalid

  // Shift FP to have the most significant bit set
  int shl = 0; // normalization shift
  uint32 nfp = fp; // normalized FP
  while ( (nfp & 0x80000000) == 0 ) { nfp <<= 1; shl++; } // use "clz" instead

  uint64 q = 0x100000000ULL; // 2^32
  uint64 e = 0x100000000ULL - (uint64)nfp; // 2^32-NFP
  int i;
  for (i=0;i<4;i++) // iterate
    {
      // Both multiplications are actually
      // 32x32 bits truncated to the 32 high bits
      q += (q*e)>>(uint64)32;
      e = (e*e)>>(uint64)32;
      printf("Q=0x%llx E=0x%llx\n",q,e);
    }
  // Here, (Q/2^32) is the inverse of (NFP/2^32).
  // We have 2^31<=NFP<2^32 and 2^32<Q<=2^33
  return (uint32)(q>>(64-2*BASE-shl));
}

int main()
{
  double x = 1.234567;
  uint32 xx = toFP(x);
  uint32 yy = inverse(xx);
  double y = toDouble(yy);

  printf("X=%f Y=%f X*Y=%f\n",x,y,x*y);
  printf("XX=0x%08x YY=0x%08x XX*YY=0x%016llx\n",xx,yy,(uint64)xx*(uint64)yy);
}

Как отмечено в коде, операции умножения не являются полными 32x32-> 64 бит. E будет становиться все меньше и меньше и изначально умещается на 32 бита. Q всегда будет на 34 бита. Мы берем только старшие 32 бита продуктов.

Вывод 64-2 * BASE-shl оставлен в качестве упражнения для читателя :-). Если он становится 0 или отрицательным, результат не представляется возможным (введенное значение слишком мало).

РЕДАКТИРОВАТЬ. В продолжение моего комментария вот вторая версия с неявным 32-м битом на Q. И E, и Q теперь хранятся в 32 битах:

uint32 inverse2(uint32 fp)
{
  if (fp == 0) return (uint32)-1; // invalid

  // Shift FP to have the most significant bit set
  int shl = 0; // normalization shift for FP
  uint32 nfp = fp; // normalized FP
  while ( (nfp & 0x80000000) == 0 ) { nfp <<= 1; shl++; } // use "clz" instead
  int shr = 64-2*BASE-shl; // normalization shift for Q
  if (shr <= 0) return (uint32)-1; // overflow

  uint64 e = 1 + (0xFFFFFFFF ^ nfp); // 2^32-NFP, max value is 2^31
  uint64 q = e; // 2^32 implicit bit, and implicit first iteration
  int i;
  for (i=0;i<3;i++) // iterate
    {
      e = (e*e)>>(uint64)32;
      q += e + ((q*e)>>(uint64)32);
    }
  return (uint32)(q>>shr) + (1<<(32-shr)); // insert implicit bit
}
11
ответ дан 30 November 2019 в 14:28
поделиться

Пара идей для вас, но ни одна из них не решает вашу проблему напрямую как указано.

  1. Почему этот алгоритм деления? Большинство делений, которые я видел в ARM, используют некоторые варианты
    
      adcs hi, den, hi, lsl # 1 
    subcc hi, hi, den 
    adcs lo, lo , lo 
     

повторяется n бит раз с двоичным поиском вне clz, чтобы определить, с чего начать. Это чертовски быстро.

  1. Если точность является большой проблемой, вы не ограничены 32/64 битами для представления с фиксированной точкой. Это будет немного медленнее, но вы можете добавить / adc или sub / sbc для перемещения значений по регистрам. Муль / мл также предназначены для такого рода работ.

Опять же, не прямые ответы для вас, но, возможно, несколько идей для дальнейшего развития. Просмотр реального кода ARM, вероятно, тоже мне немного поможет.

1
ответ дан 30 November 2019 в 14:28
поделиться

Мэдс, вы совсем не теряете точности. Когда вы делите 512.00002f на 2 ^ 10, вы просто уменьшаете показатель степени вашего числа с плавающей запятой на 10. Мантисса остается прежней. Конечно, если показатель степени не достигает минимального значения, но этого не должно происходить, поскольку вы масштабируете до (0,5, 1].

РЕДАКТИРОВАТЬ: Хорошо, поэтому вы используете фиксированную десятичную точку. В этом случае вы должны разрешить другое представление знаменателя в вашем алгоритме. Значение D находится из (0,5, 1] ​​не только в начале, но и на протяжении всего вычисления (легко доказать, что x * (2-x) <1 для x <1) . Таким образом, вы должны представить знаменатель с десятичной точкой в ​​базе = 32. Таким образом, у вас будет 32 бита точности все время.

РЕДАКТИРОВАТЬ: Чтобы реализовать это, вам нужно будет изменить следующие строки вашего кода:

  //bitpos = 31 - clz(val) - BASE;
  bitpos = 31 - clz(val) - 31;
...
  //F = (2ULL<<BASE) - D;
  //N = F;
  //D = ((unsigned long long)D*F)>>BASE;
  F = -D;
  N = F >> (31 - BASE);
  D = ((unsigned long long)D*F)>>31;
...
    //F = (2<<(BASE)) - D;
    //D = ((unsigned long long)D*F)>>BASE;
    F = -D;
    D = ((unsigned long long)D*F)>>31;
...
    //N = ((unsigned long long)N*F)>>BASE;
    N = ((unsigned long long)N*F)>>31;

Также в конце вам придется сдвигать N не на битовые позиции, а на какое-то другое значение, которое мне сейчас лень вычислять :).

0
ответ дан 30 November 2019 в 14:28
поделиться
Другие вопросы по тегам:

Похожие вопросы: