17

我在 Q22.10 中使用Goldschmidt 除法计算定点倒数,以用于我在 ARM 上的软件光栅化器。

这是通过将分子设置为 1 来完成的,即分子在第一次迭代时变为标量。老实说,我在这里有点盲目地遵循维基百科算法。文章说,如果分母在半开范围(0.5, 1.0] 内缩放,一个好的初步估计可以单独基于分母:设 F 为估计的标量,D 为分母,则 F = 2 - D.

但是这样做时,我会失去很多精度。假设我想找到 512.00002f 的倒数。为了缩小数字,我在小数部分损失了 10 位精度,将其移出。所以,我的问题是:

  • 有没有办法选择一个不需要标准化的更好的估计?为什么?为什么不?为什么这可能或不可能的数学证明会很棒。
  • 此外,是否可以预先计算第一个估计值,以便序列更快地收敛?现在,它平均在第 4 次迭代后收敛。在 ARM 上,这大约是 50 个周期的最坏情况,并且没有考虑 clz/bsr 的仿真,也没有考虑内存查找。如果可能的话,我想知道这样做是否会增加错误,以及增加多少。

这是我的测试用例。注意:第13行的软件实现clz来自我的帖子here。如果需要,您可以将其替换为内在函数。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;
}
4

3 回答 3

12

我忍不住花了一个小时来解决你的问题......

该算法在 Jean-Michel Muller(法语)的“Arithmetique des ordinateurs”第 5.5.2 节中进行了描述。它实际上是牛顿迭代的一个特例,以 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 的值,直到你得到相反的值。您只需在 0x80000000 到 0xFFFFFFFF 范围内标准化一个 32 位数字 X,然后计算 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 或负数,则结果不可表示(输入值太小)。

编辑。作为我评论的后续,这里是第二个版本,在 Q 上有一个隐式的第 32 位。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
}
于 2010-04-23T15:38:24.560 回答
1

为您提供了一些想法,但没有一个可以直接解决您的问题。

  1. 为什么这个除法算法?我在 ARM 中看到的大多数划分都使用了一些不同的
    
          adcs hi, den, hi, lsl #1
          subcc hi, hi, den
          adcs lo, lo, lo
    

用 clz 的二进制搜索重复 n 位次以确定从哪里开始。这真是太快了。

  1. 如果精度是一个大问题,您的定点表示不限于 32/64 位。它会慢一点,但你可以做 add/adc 或 sub/sbc 跨寄存器移动值。mul/mla 也是为这类工作而设计的。

同样,不是直接为您提供答案,但可能有一些想法可以推动这一点。查看实际的 ARM 代码可能也会对我有所帮助。

于 2010-04-22T18:37:03.960 回答
0

Mads,你一点也不失精确度。当您将 512.00002f 除以 2^10 时,您只需将浮点数的指数减 10。尾数保持不变。当然,除非指数达到其最小值,但这不应该发生,因为您正在缩放到 (0.5, 1]。

编辑:好的,所以您使用的是固定小数点。在这种情况下,您应该允许在算法中使用不同的分母表示。D 的值不仅在开始时来自 (0.5, 1],而且在整个计算过程中都来自 (0.5, 1])(很容易证明 x * (2-x) < 1 for x < 1)。所以你应该用小数表示分母指向base = 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;

最后,您还必须通过 bitpos 而不是通过一些不同的值来移动 N,我现在懒得弄清楚:)。

于 2010-04-22T10:16:51.387 回答