Выбор хороших первых оценок для подразделения Гольдшмидта - PullRequest
17 голосов
/ 18 апреля 2010

Я рассчитываю обратные значения с фиксированной точкой в ​​Q22.10 с делением Голдшмидта для использования в моем программном растеризаторе на 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 <stdio.h>
#include <stdint.h>

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) - D;
  /* N = F for the first iteration, because the numerator is simply 1.
     So don't waste a 64-bit UMULL on a multiply with 1 */
  N = F;
  D = ((unsigned long long)D*F)>>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<<BASE);
  result = divfp(D, &iter); 

  fa = (double)result / (double)(1UL << BASE);
  printf("Value: %8.8lf 1/value: %8.8lf FP value: 0x%.8X\n", fv, fa, result);
  printf("iteration: %d\n",iter);

  return 0;
}

Ответы [ 3 ]

11 голосов
/ 23 апреля 2010

Я не удержался, потратив час на твою проблему ...

Этот алгоритм описан в разделе 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 или отрицательным, результат не может быть представлен (входное значение слишком мало).

EDIT. В продолжение моего комментария, вот вторая версия с неявным 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
}
1 голос
/ 22 апреля 2010

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

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

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

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

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

0 голосов
/ 22 апреля 2010

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

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

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

  //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 не по битам, а по какому-то другому значению, которое мне лень сейчас выяснять:).

...