获取sqrt(n)整数部分的最快方法?

时间:2011-02-08 06:42:04

标签: c++ c algorithm math performance

我们知道如果n不是一个完美的正方形,那么sqrt(n)就不是一个整数。由于我只需要整数部分,我觉得调用sqrt(n)不会那么快,因为计算小数部分也需要时间。

所以我的问题是,

我们是否只能获得 sqrt(n)的整数部分而不计算sqrt(n)的实际值?该算法应该比sqrt(n)(在<math.h><cmath>中定义)更快?

如果可能,您也可以在asm块中编写代码。

13 个答案:

答案 0 :(得分:21)

我会尝试Fast Inverse Square Root技巧。

这是一种非常好的近似1/sqrt(n)而没有任何分支的方法,基于一些比特错误,因此不可移植(特别是在32位和64位平台之间)。

一旦得到它,你只需要反转结果,并取整数部分。

当然,可能会有更快的技巧,因为这个技巧有点过时了。

编辑:让我们这样做!

首先是一个小帮手:

// benchmark.h
#include <sys/time.h>

template <typename Func>
double benchmark(Func f, size_t iterations)
{
  f();

  timeval a, b;
  gettimeofday(&a, 0);
  for (; iterations --> 0;)
  {
    f();
  }
  gettimeofday(&b, 0);
  return (b.tv_sec * (unsigned int)1e6 + b.tv_usec) -
         (a.tv_sec * (unsigned int)1e6 + a.tv_usec);
}

然后是主体:

#include <iostream>

#include <cmath>

#include "benchmark.h"

class Sqrt
{
public:
  Sqrt(int n): _number(n) {}

  int operator()() const
  {
    double d = _number;
    return static_cast<int>(std::sqrt(d) + 0.5);
  }

private:
  int _number;
};

// http://www.codecodex.com/wiki/Calculate_an_integer_square_root
class IntSqrt
{
public:
  IntSqrt(int n): _number(n) {}

  int operator()() const 
  {
    int remainder = _number;
    if (remainder < 0) { return 0; }

    int place = 1 <<(sizeof(int)*8 -2);

    while (place > remainder) { place /= 4; }

    int root = 0;
    while (place)
    {
      if (remainder >= root + place)
      {
        remainder -= root + place;
        root += place*2;
      }
      root /= 2;
      place /= 4;
    }
    return root;
  }

private:
  int _number;
};

// http://en.wikipedia.org/wiki/Fast_inverse_square_root
class FastSqrt
{
public:
  FastSqrt(int n): _number(n) {}

  int operator()() const
  {
    float number = _number;

    float x2 = number * 0.5F;
    float y = number;
    long i = *(long*)&y;
    //i = (long)0x5fe6ec85e7de30da - (i >> 1);
    i = 0x5f3759df - (i >> 1);
    y = *(float*)&i;

    y = y * (1.5F - (x2*y*y));
    y = y * (1.5F - (x2*y*y)); // let's be precise

    return static_cast<int>(1/y + 0.5f);
  }

private:
  int _number;
};


int main(int argc, char* argv[])
{
  if (argc != 3) {
    std::cerr << "Usage: %prog integer iterations\n";
    return 1;
  }

  int n = atoi(argv[1]);
  int it = atoi(argv[2]);

  assert(Sqrt(n)() == IntSqrt(n)() &&
          Sqrt(n)() == FastSqrt(n)() && "Different Roots!");
  std::cout << "sqrt(" << n << ") = " << Sqrt(n)() << "\n";

  double time = benchmark(Sqrt(n), it);
  double intTime = benchmark(IntSqrt(n), it);
  double fastTime = benchmark(FastSqrt(n), it);

  std::cout << "Number iterations: " << it << "\n"
               "Sqrt computation : " << time << "\n"
               "Int computation  : " << intTime << "\n"
               "Fast computation : " << fastTime << "\n";

  return 0;
}

结果:

sqrt(82) = 9
Number iterations: 4096
Sqrt computation : 56
Int computation  : 217
Fast computation : 119

// Note had to tweak the program here as Int here returns -1 :/
sqrt(2147483647) = 46341 // real answer sqrt(2 147 483 647) = 46 340.95
Number iterations: 4096
Sqrt computation : 57
Int computation  : 313
Fast computation : 119

如果符合预期,快速计算的性能远远优于 Int 计算。

哦,顺便说一句,sqrt更快:)

答案 1 :(得分:16)

编辑:这个答案是愚蠢的 - 使用(int) sqrt(i)

使用正确的设置(-march=native -m64 -O3)进行分析后,上面的内容更快批次


好吧,有点老问题,但还没有给出“最快”的答案。最快的(我认为)是二进制平方根算法,完全在this Embedded.com article中解释。

基本上归结为:

unsigned short isqrt(unsigned long a) {
    unsigned long rem = 0;
    int root = 0;
    int i;

    for (i = 0; i < 16; i++) {
        root <<= 1;
        rem <<= 2;
        rem += a >> 30;
        a <<= 2;

        if (root < rem) {
            root++;
            rem -= root;
            root++;
        }
    }

    return (unsigned short) (root >> 1);
}

在我的机器上(Q6600,Ubuntu 10.10)我通过取数字1-100000000的平方根进行分析。使用iqsrt(i)花了2750毫秒。使用(unsigned short) sqrt((float) i)需要3600毫秒。这是使用g++ -O3完成的。使用-ffast-math编译选项,时间分别为2100ms和3100ms。请注意,这甚至不使用单行汇编程序,因此它可能仍然会更快。

上述代码适用于C和C ++,并且对Java也进行了少量语法更改。

对于有限范围更有效的是二分搜索。在我的机器上,它将上面的版本从水中吹出4倍。可悲的是它的范围非常有限:

#include <stdint.h>

const uint16_t squares[] = {
    0, 1, 4, 9,
    16, 25, 36, 49,
    64, 81, 100, 121,
    144, 169, 196, 225,
    256, 289, 324, 361,
    400, 441, 484, 529,
    576, 625, 676, 729,
    784, 841, 900, 961,
    1024, 1089, 1156, 1225,
    1296, 1369, 1444, 1521,
    1600, 1681, 1764, 1849,
    1936, 2025, 2116, 2209,
    2304, 2401, 2500, 2601,
    2704, 2809, 2916, 3025,
    3136, 3249, 3364, 3481,
    3600, 3721, 3844, 3969,
    4096, 4225, 4356, 4489,
    4624, 4761, 4900, 5041,
    5184, 5329, 5476, 5625,
    5776, 5929, 6084, 6241,
    6400, 6561, 6724, 6889,
    7056, 7225, 7396, 7569,
    7744, 7921, 8100, 8281,
    8464, 8649, 8836, 9025,
    9216, 9409, 9604, 9801,
    10000, 10201, 10404, 10609,
    10816, 11025, 11236, 11449,
    11664, 11881, 12100, 12321,
    12544, 12769, 12996, 13225,
    13456, 13689, 13924, 14161,
    14400, 14641, 14884, 15129,
    15376, 15625, 15876, 16129,
    16384, 16641, 16900, 17161,
    17424, 17689, 17956, 18225,
    18496, 18769, 19044, 19321,
    19600, 19881, 20164, 20449,
    20736, 21025, 21316, 21609,
    21904, 22201, 22500, 22801,
    23104, 23409, 23716, 24025,
    24336, 24649, 24964, 25281,
    25600, 25921, 26244, 26569,
    26896, 27225, 27556, 27889,
    28224, 28561, 28900, 29241,
    29584, 29929, 30276, 30625,
    30976, 31329, 31684, 32041,
    32400, 32761, 33124, 33489,
    33856, 34225, 34596, 34969,
    35344, 35721, 36100, 36481,
    36864, 37249, 37636, 38025,
    38416, 38809, 39204, 39601,
    40000, 40401, 40804, 41209,
    41616, 42025, 42436, 42849,
    43264, 43681, 44100, 44521,
    44944, 45369, 45796, 46225,
    46656, 47089, 47524, 47961,
    48400, 48841, 49284, 49729,
    50176, 50625, 51076, 51529,
    51984, 52441, 52900, 53361,
    53824, 54289, 54756, 55225,
    55696, 56169, 56644, 57121,
    57600, 58081, 58564, 59049,
    59536, 60025, 60516, 61009,
    61504, 62001, 62500, 63001,
    63504, 64009, 64516, 65025
};

inline int isqrt(uint16_t x) {
    const uint16_t *p = squares;

    if (p[128] <= x) p += 128;
    if (p[ 64] <= x) p +=  64;
    if (p[ 32] <= x) p +=  32;
    if (p[ 16] <= x) p +=  16;
    if (p[  8] <= x) p +=   8;
    if (p[  4] <= x) p +=   4;
    if (p[  2] <= x) p +=   2;
    if (p[  1] <= x) p +=   1;

    return p - squares;
}

可以在此处下载32位版本:https://gist.github.com/3481770

答案 2 :(得分:6)

虽然我怀疑你可以通过搜索“快速整数平方根”找到很多选项,但这里有一些可能很有效的新想法(每个都是独立的,或者你可以将它们结合起来):

  1. 在您要支持的域中创建所有完美正方形的static const数组,并对其执行快速无分支二进制搜索。数组中生成的索引是平方根。
  2. 将数字转换为浮点数并将其分解为尾数和指数。将指数减半并将尾数乘以某个神奇因子(找到它的工作)。这应该能够给你一个非常接近的近似值。如果它不准确,可以包括调整它的最后一步(或者将其用作上面二进制搜索的起点)。

答案 3 :(得分:6)

我认为Google search提供了很好的文章,例如Calculate an integer square root讨论了很多可能的快速计算方法,并且有很好的参考文章,我认为这里没有人可以比他们更好(如果有人可以首先制作关于它的论文),但是如果你阅读它们并且它们含糊不清,那么我们可以帮助你。

答案 4 :(得分:6)

如果你不介意近似,那么我拼凑在一起的整数sqrt函数怎么样。

int sqrti(int x)
{
    union { float f; int x; } v; 

    // convert to float
    v.f = (float)x;

    // fast aprox sqrt
    //  assumes float is in IEEE 754 single precision format 
    //  assumes int is 32 bits
    //  b = exponent bias
    //  m = number of mantissa bits
    v.x  -= 1 << 23; // subtract 2^m 
    v.x >>= 1;       // divide by 2
    v.x  += 1 << 29; // add ((b + 1) / 2) * 2^m

    // convert to int
    return (int)v.f;
}

它使用此Wikipedia文章中描述的算法。 在我的机器上,它几乎是sqrt的两倍:)

答案 5 :(得分:4)

要做整数sqrt,你可以使用牛顿方法的这种专业化:

Def isqrt(N):

    a = 1
    b = N

    while |a-b| > 1
        b = N / a
        a = (a + b) / 2

    return a

基本上对于任何x,sqrt位于范围(x ... N / x)中,因此我们只是在每个循环中将该间隔平分为新猜测。有点像二进制搜索但它收敛必须更快。

这收敛于O(loglog(N)),这非常快。它根本不使用浮点数,它也适用于任意精度整数。

答案 6 :(得分:3)

为什么没有人建议最快的方法?

如果:

  1. 数量范围有限
  2. 内存消耗并不重要
  3. 应用程序启动时间并不重要
  4. 然后使用int[MAX_X]创建sqrt(x)填充(启动时)(您不需要使用函数sqrt())。

    所有这些条件都很适合我的计划。 特别是,int[10000000]数组将消耗40MB

    您对此有何看法?

答案 7 :(得分:3)

这太短了,以至于99%的内联:

static inline int sqrtn(int num) {
    int i;
    __asm__ (
        "pxor %%xmm0, %%xmm0\n\t"   // clean xmm0 for cvtsi2ss
        "cvtsi2ss %1, %%xmm0\n\t"   // convert num to float, put it to xmm0
        "sqrtss %%xmm0, %%xmm0\n\t" // square root xmm0
        "cvttss2si %%xmm0, %0"      // float to int
        :"=r"(i):"r"(num):"%xmm0"); // i: result, num: input, xmm0: scratch register
    return i;
}

为什么干净的xmm0cvtsi2ss

的文档
  

目标操作数是XMM寄存器。结果存储在目标操作数的低位双字中,而高位三个双字保持不变。

GCC内部版本(仅在GCC上运行):

#include <xmmintrin.h>
int sqrtn2(int num) {
    register __v4sf xmm0 = {0, 0, 0, 0};
    xmm0 = __builtin_ia32_cvtsi2ss(xmm0, num);
    xmm0 = __builtin_ia32_sqrtss(xmm0);
    return __builtin_ia32_cvttss2si(xmm0);
}

英特尔固有版本(已在GCC,Clang和ICC上测试):

#include <xmmintrin.h>
int sqrtn2(int num) {
    register __m128 xmm0 = _mm_setzero_ps();
    xmm0 = _mm_cvt_si2ss(xmm0, num);
    xmm0 = _mm_sqrt_ss(xmm0);
    return _mm_cvtt_ss2si(xmm0);
}

^^^^它们都需要SSE 1(甚至不需要SSE 2)。

答案 8 :(得分:2)

在许多情况下,甚至不需要精确的整数sqrt值,足以具有良好的近似值。 (例如,它通常发生在DSP优化中,当32位信号应压缩为16位或16位到8位时,不会在零点附近失去太多精度)。

我发现了这个有用的等式:

k = ceil(MSB(n)/2); - MSB(n) is the most significant bit of "n"


sqrt(n) ~= 2^(k-2)+(2^(k-1))*n/(2^(2*k))); - all multiplications and divisions here are very DSP-friendly, as they are only 2^k.

此公式生成平滑曲线(n,sqrt(n)),其值与实际sqrt(n)差别不大,因此在近似精度足够时非常有用。

答案 9 :(得分:1)

如果你需要计算平方根的性能,我想你会计算很多。 那为什么不缓解答案呢?在你的情况下我不知道N的范围,也不知道你将计算同一整数的平方根的很多倍,但如果是,那么你可以在每次调用方法时缓存结果(在数组中将是最有效的,如果不是太大)。

答案 10 :(得分:1)

在我的计算机上使用gcc,使用-ffast-math,将32位整数转换为float并使用sqrtf每10 ^ 9个操作需要1.2 s(没有-ffast-math需要3.54 s)。

以下算法每10 ^ 9使用0.87秒而牺牲一些准确性:尽管RMS误差仅为0.79,但误差可能高达-7或+1:

uint16_t SQRTTAB[65536];

inline uint16_t approxsqrt(uint32_t x) { 
  const uint32_t m1 = 0xff000000;
  const uint32_t m2 = 0x00ff0000;
  if (x&m1) {
    return SQRTTAB[x>>16];
  } else if (x&m2) {
    return SQRTTAB[x>>8]>>4;
  } else {
    return SQRTTAB[x]>>8;
  }
}

该表使用:

构建
void maketable() {
  for (int x=0; x<65536; x++) {
    double v = x/65535.0;
    v = sqrt(v);
    int y = int(v*65535.0+0.999);
    SQRTTAB[x] = y;
  }
}

我发现使用进一步的if语句来改进二分法确实提高了准确性,但它也减慢了sqrtf更快的速度,至少使用-ffast-math。

答案 11 :(得分:1)

以下解决方案将计算整数部分,准确地表示floor(sqrt(x)),没有舍入错误。

其他方法存在的问题

  • 使用floatdouble既便携又不够精确
  • @orlp的isqrt给出了类似isqrt(100) = 15的疯狂结果
  • 基于巨大的查找表的方法超过32位是不实际的
  • 使用快速反方sqrt 是非常不精确的,最好使用sqrtf
  • 牛顿的方法需要昂贵的整数除法和良好的初始猜测

我的方法

我的地雷基于bit-guessing approach proposed on Wikipedia。不幸的是,维基百科上提供的伪代码存在一些错误,因此我不得不进行一些调整:

// C++20 also provides std::bit_width in its <bit> header
unsigned char bit_width(unsigned long long x) {
    return x == 0 ? 1 : 64 - __builtin_clzll(x);
}

template <typename Int, std::enable_if_t<std::is_unsigned<Int, int = 0>>
Int sqrt(const Int n) {
    unsigned char shift = bit_width(n);
    shift += shift & 1; // round up to next multiple of 2

    Int result = 0;

    do {
        shift -= 2;
        result <<= 1; // make space for the next guessed bit
        result |= 1;  // guess that the next bit is 1
        result ^= result * result > (n >> shift); // revert if guess too high
    } while (shift != 0);

    return result;
}
可以在恒定时间内对

bit_width进行求值,循环最多重复ceil(bit_width / 2)次。因此,即使对于64位整数,这也将是基本算术和按位运算的最坏32次迭代。

compile output仅约20条指令。

性能

我通过统一生成输入来针对基于float的方法进行基准测试。请注意,在现实世界中,大多数输入将比std::numeric_limits<...>::max()更接近于零。

  • 对于uint32_t,这比使用25x的效果要差std::sqrt(float)
  • 对于uint64_t,这比使用30x的效果要差std::sqrt(double)

准确性

与使用浮点数学的方法不同,此方法始终非常准确。

  • 使用sqrtf可以在[2 28 ,2 32 )范围内提供错误的舍入。例如,sqrtf(0xffffffff) = 65536的平方根实际上是65535.99999
  • 在[2 60 ,2 64 )范围内,双精度无法始终如一地工作。例如,sqrt(0x3fff...) = 2147483648的平方根实际上是2147483647.999999

唯一涵盖所有64位整数的是x86扩展精度long double,仅仅是因为它可以容纳整个64位整数。

结论

正如我所说,这是正确处理所有输入,避免整数除法且不需要查找表的唯一解决方案。 总而言之,如果您需要一种独立于精度且不需要巨大查找表的方法,那么这是您唯一的选择。 在性能并不重要的constexpr环境中,获得100%准确的结果可能更为重要。

使用牛顿法的替代方法

从一个好的猜测开始,牛顿的方法可能很快。对于我们的猜测,我们将向下舍入到2的下一个幂并在恒定时间内计算平方根。对于任何2 x ,我们都可以使用2 x / 2 求平方根。

template <typename Int, std::enable_if_t<std::is_unsigned_v<Int>, int> = 0>
Int sqrt_guess(const Int n)
{
    Int log2floor = bit_width(n) - 1;
    // sqrt(x) is equivalent to pow(2, x / 2 = x >> 1)
    // pow(2, x) is equivalent to 1 << x
    return 1 << (log2floor >> 1);
}

请注意,这不完全是2 x / 2 ,因为我们在右移期间损失了一些精度。而是2 floor(x / 2)。 还要注意,sqrt_guess(0) = 1实际上是避免在第一次迭代中被零除的必要条件:

template <typename Int, std::enable_if_t<std::is_unsigned_v<Int>, int> = 0>
Int sqrt_newton(const Int n)
{
    Int a = sqrt_guess(n);
    Int b = n;
    
    // compute unsigned difference
    while (std::max(a, b) - std::min(a, b) > 1) {
        b = n / a;
        a = (a + b) / 2;
    }

    // a is now either floor(sqrt(n)) or ceil(sqrt(n))
    // we decrement in the latter case
    // this is overflow-safe as long as we start with a lower bound guess
    return a - (a * a > n);
}

这种替代方法的执行效果与第一个提议大致相同,但是通常快几个百分点。但是,它严重依赖高效的硬件划分,结果可能会有很大差异。

使用sqrt_guess有很大的不同。这比使用1作为初始猜测要快大约五倍。

答案 12 :(得分:1)

或者只是做一个二分查找,不能写一个更简单的版本imo:

uint16_t sqrti(uint32_t num)
{
    uint16_t ret = 0;
    for(int32_t i = 15; i >= 0; i--)
    {
        uint16_t temp = ret | (1 << i);
        if(temp * temp <= num)
        {
            ret = temp;
        }
    }
    return ret;
}