通过快速浮点倒数

时间:2016-04-26 01:28:20

标签: c floating-point bit-manipulation division

我目前正在研究如何使用各种现代处理器的快速单精度浮点倒数功能来计算基于定点Newton-Raphson迭代的64位无符号整数除法的起始近似。它需要尽可能准确地计算2 64 /除数,其中初始近似必须小于或等于数学结果,基于以下定点迭代的要求。这意味着这种计算需要低估。我目前有以下代码,基于广泛的测试,效果很好:

#include <stdint.h> // import uint64_t
#include <math.h> // import nextafterf()

uint64_t divisor, recip;
float r, s, t;

t = uint64_to_float_ru (divisor); // ensure t >= divisor
r = 1.0f / t;
s = 0x1.0p64f * nextafterf (r, 0.0f);
recip = (uint64_t)s; // underestimate of 2**64 / divisor 

虽然此代码功能正常,但在大多数平台上并不是非常快。一个显而易见的改进,需要一些特定于机器的代码,是用使用硬件提供的快速浮点倒数的代码替换除r = 1.0f / t。这可以通过迭代来增强,以产生在数学结果的1 ulp内的结果,因此在现有代码的上下文中产生低估。 x86_64的示例实现是:

#include <xmmintrin.h>
/* Compute 1.0f/a almost correctly rounded. Halley iteration with cubic convergence */
inline float fast_recip_f32 (float a)
{
    __m128 t;
    float e, r;
    t = _mm_set_ss (a);
    t = _mm_rcp_ss (t);
    _mm_store_ss (&r, t);
    e = fmaf (r, -a, 1.0f);
    e = fmaf (e, e, e);
    r = fmaf (e, r, r);
    return r;
}

nextafterf()的实现通常不会对性能进行优化。在有能力快速将IEEE 754 binary32解释为int32并且反之亦然的平台上,通过内在函数float_as_int()int_as_float(),我们可以结合使用{ {1}}并缩放如下:

nextafterf()

假设这些方法在特定平台上是可行的,这使我们将s = int_as_float (float_as_int (r) + 0x1fffffff); float之间的转换视为主要障碍。大多数平台都没有提供使用静态舍入模式执行从uint64_tuint64_t的转换的指令(此处:朝向正无限=向上),有些平台不提供任何指示在float和浮点类型之间进行转换,使其成为性能瓶颈。

uint64_t

t = uint64_to_float_ru (divisor); r = fast_recip_f32 (t); s = int_as_float (float_as_int (r) + 0x1fffffff); recip = (uint64_t)s; /* underestimate of 2**64 / divisor */ 的便携但缓慢的实现使用FPU舍入模式的动态更改:

uint64_to_float_ru

我已经研究了各种分裂和比特纠缠的方法来处理转换(例如,在整数方面进行舍入,然后使用正常转换到#include <fenv.h> #pragma STDC FENV_ACCESS ON float uint64_to_float_ru (uint64_t a) { float res; int curr_mode = fegetround (); fesetround (FE_UPWARD); res = (float)a; fesetround (curr_mode); return res; } ,它使用IEEE 754舍入模式舍入到-nearest-even-even),但是这会产生的开销使得这种计算通过快速浮点数倒数从性能角度来看没有吸引力。就目前而言,看起来我最好通过使用带插值的经典LUT或定点多项式近似来生成起始近似,然后使用32位定点Newton-Raphson步骤来跟随它们。 / p>

有没有办法提高我当前方法的效率?涉及特定平台内在函数的便携式和半便携式方式会引起人们的兴趣(特别是x86和ARM作为当前占主导地位的CPU架构) )。使用英特尔编译器在非常高的优化(float)下编译x86_64时,初始近似的计算需要比迭代更多的指令,这需要大约20条指令。以下是完整的分部代码供参考,显示了上下文中的近似值。

/O3 /QxCORE-AVX2 /Qprec-div-

uint64_t udiv64 (uint64_t dividend, uint64_t divisor) { uint64_t temp, quot, rem, recip, neg_divisor = 0ULL - divisor; float r, s, t; /* compute initial approximation for reciprocal; must be underestimate! */ t = uint64_to_float_ru (divisor); r = 1.0f / t; s = 0x1.0p64f * nextafterf (r, 0.0f); recip = (uint64_t)s; /* underestimate of 2**64 / divisor */ /* perform Halley iteration with cubic convergence to refine reciprocal */ temp = neg_divisor * recip; temp = umul64hi (temp, temp) + temp; recip = umul64hi (recip, temp) + recip; /* compute preliminary quotient and remainder */ quot = umul64hi (dividend, recip); rem = dividend - divisor * quot; /* adjust quotient if too small; quotient off by 2 at most */ if (rem >= divisor) quot += ((rem - divisor) >= divisor) ? 2 : 1; /* handle division by zero */ if (divisor == 0ULL) quot = ~0ULL; return quot; } 通常会映射到特定于平台的内部函数或一些内联汇编代码。在x86_64上,我目前使用此实现:

umul64hi()

1 个答案:

答案 0 :(得分:2)

此解决方案结合了两个想法:

  • 只要数字在特定范围内,您只需将位重新解释为浮点并减去常量即可转换为浮点数。所以添加一个常量,重新解释,然后减去该常量。这将得到截断的结果(因此总是小于或等于所需的值)。
  • 您可以通过否定指数和尾数来近似倒数。这可以通过将位解释为int来实现。

选项1仅适用于某个范围,因此我们检查范围并调整使用的常数。这工作在64位,因为所需的浮点只有23位的精度。

此代码中的结果将是double,但转换为float是微不足道的,可以在位上或直接完成,具体取决于硬件。

在此之后你想要做Newton-Raphson迭代。

这些代码大部分只是转换为幻数。

double                                                       
u64tod_inv( uint64_t u64 ) {                                 
  __asm__( "#annot0" );                                      
  union {                                                    
    double f;                                                
    struct {                                                 
      unsigned long m:52; // careful here with endianess     
      unsigned long x:11;                                    
      unsigned long s:1;                                     
    } u64;                                                   
    uint64_t u64i;                                           
  } z,                                                       
        magic0 = { .u64 = { 0, (1<<10)-1 + 52, 0 } },        
        magic1 = { .u64 = { 0, (1<<10)-1 + (52+12), 0 } },   
        magic2 = { .u64 = { 0, 2046, 0 } };                  

  __asm__( "#annot1" );                                      
  if( u64 < (1UL << 52UL ) ) {                               
    z.u64i = u64 + magic0.u64i;                              
    z.f   -= magic0.f;                                       
  } else {                                                   
    z.u64i = ( u64 >> 12 ) + magic1.u64i;                    
    z.f   -= magic1.f;                                       
  }                                                          
  __asm__( "#annot2" );                                      

  z.u64i = magic2.u64i - z.u64i;                             

  return z.f;                                                
}                                                            

在英特尔核心7上进行编译会提供许多指令(和分支),但当然,根本没有乘法或除法。如果int和double之间的强制转换速度很快,那么这应该很快就会运行。

我怀疑float(只有23位精度)需要超过1或2次Newton-Raphson迭代才能获得你想要的精度,但我没有做过数学计算......