如何有效地计算小于或等于给定数字的2的最高功率?

时间:2017-03-04 11:15:52

标签: c++ optimization

到目前为止我想出了三个解决方案:

效率极低的标准库powlog2函数:

int_fast16_t powlog(uint_fast16_t n)
{
  return static_cast<uint_fast16_t>(pow(2, floor(log2(n))));
}

计算后续2次幂的效率要高得多,直到达到比我必须达到的数量更多的数字:

uint_fast16_t multiply(uint_fast16_t n)
{
  uint_fast16_t maxpow = 1;
  while(2*maxpow <= n)
    maxpow *= 2;
  return maxpow;
}

迄今为止最有效的bin搜索预先计算的权力表2:

uint_fast16_t binsearch(uint_fast16_t n)
{
  static array<uint_fast16_t, 20> pows {1,2,4,8,16,32,64,128,256,512,
    1024,2048,4096,8192,16384,32768,65536,131072,262144,524288};

  return *(upper_bound(pows.begin(), pows.end(), n)-1);
}

这可以进一步优化吗?可以在这里使用的任何技巧吗?

我使用的完整基准:

#include <iostream>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <array>
#include <algorithm>
using namespace std;
using namespace chrono;

uint_fast16_t powlog(uint_fast16_t n)
{
  return static_cast<uint_fast16_t>(pow(2, floor(log2(n))));
}

uint_fast16_t multiply(uint_fast16_t n)
{
  uint_fast16_t maxpow = 1;
  while(2*maxpow <= n)
    maxpow *= 2;
  return maxpow;
}

uint_fast16_t binsearch(uint_fast16_t n)
{
  static array<uint_fast16_t, 20> pows {1,2,4,8,16,32,64,128,256,512,
    1024,2048,4096,8192,16384,32768,65536,131072,262144,524288};

  return *(upper_bound(pows.begin(), pows.end(), n)-1);
}

high_resolution_clock::duration test(uint_fast16_t(powfunct)(uint_fast16_t))
{
  auto tbegin = high_resolution_clock::now();
  volatile uint_fast16_t sink;
  for(uint_fast8_t i = 0; i < UINT8_MAX; ++i)
    for(uint_fast16_t n = 1; n <= 999999; ++n)
      sink = powfunct(n);
  auto tend = high_resolution_clock::now();
  return tend - tbegin;
}

int main()
{
  cout << "Pow and log took " << duration_cast<milliseconds>(test(powlog)).count() << " milliseconds." << endl;
  cout << "Multiplying by 2 took " << duration_cast<milliseconds>(test(multiply)).count() << " milliseconds." << endl;
  cout << "Binsearching precomputed table of powers took " << duration_cast<milliseconds>(test(binsearch)).count() << " milliseconds." << endl;
}

使用-O2编译,这在我的笔记本电脑上显示了以下结果:

Pow and log took 19294 milliseconds.
Multiplying by 2 took 2756 milliseconds.
Binsearching precomputed table of powers took 2278 milliseconds.

6 个答案:

答案 0 :(得分:17)

评论中已经建议使用带有内在函数的版本,所以这里的版本不依赖于它们:

uint32_t highestPowerOfTwoIn(uint32_t x)
{
  x |= x >> 1;
  x |= x >> 2;
  x |= x >> 4;
  x |= x >> 8;
  x |= x >> 16;
  return x ^ (x >> 1);
}

这首先将最高设置位“涂抹”到右边,然后x ^ (x >> 1)只保留与它们直接左边的位不同的位(msb被认为是0到左边的位) it),这只是最高的设定位,因为涂抹的数字是0 n 1 m 的形式(用字符串表示法,而不是数字取幂)。 / p>

由于没有人真正发布它,你可以用内在函数来写(GCC,Clang)

uint32_t highestPowerOfTwoIn(uint32_t x)
{
  return 0x80000000 >> __builtin_clz(x);
}

或(MSVC,可能,未经测试)

uint32_t highestPowerOfTwoIn(uint32_t x)
{
  unsigned long index;
  // ignoring return value, assume x != 0
  _BitScanReverse(&index, x);
  return 1u << index;
}

当目标硬件直接支持时,应该更好。

Results on colirulatency results on coliru(也与基线相比,它应该大致指示开销)。在延迟结果中,highestPowerOfTwoIn的第一个版本看起来不再那么好了(仍然可以,但它是一长串依赖指令,所以它扩大与内在函数版本的差距并不是一个大惊喜) 。其中哪一项最相关的比较取决于您的实际使用情况。

如果你有一些具有快速位反转操作的奇数硬件(但可能是慢速移位或慢速clz),那么我们称之为_rbit,然后就可以了

uint32_t highestPowerOfTwoIn(uint32_t x)
{
  x = _rbit(x);
  return _rbit(x & -x);
}

这当然基于旧的x & -x,它隔离了最低的设置位,由位反转包围,它隔离了最高的设置位。

答案 1 :(得分:3)

查找表看起来像这里的最佳选择。因此,回答

  

这可以进一步优化吗?可以在这里使用的任何技巧吗?

是的,我们可以!让我们beat the standard library binary search

template <class T>
inline size_t
choose(T const& a, T const& b, size_t const& src1, size_t const& src2)
{
    return b >= a ? src2 : src1;
}
template <class Container>
inline typename Container::const_iterator
fast_upper_bound(Container const& cont, typename Container::value_type const& value)
{
    auto size = cont.size();
    size_t low = 0;

    while (size > 0) {
        size_t half = size / 2;
        size_t other_half = size - half;
        size_t probe = low + half;
        size_t other_low = low + other_half;
        auto v = cont[probe];
        size = half;
        low = choose(v, value, low, other_low);
    }

    return begin(cont)+low;
}

使用upper_bound的这种实现方式为我带来了实质性的改进:

g++ -std=c++14 -O2 -Wall -Wno-unused-but-set-variable -Werror main.cpp && ./a.out
Pow and log took 2536 milliseconds.
Multiplying by 2 took 320 milliseconds.
Binsearching precomputed table of powers took 349 milliseconds.
Binsearching (opti) precomputed table of powers took 167 milliseconds.

live on coliru) 请注意,我已经改进了基准以使用随机值;通过这样做,我删除了branch prediction bias

现在,如果您确实需要更加努力,您可以使用x86_64 asm为clang优化choose函数:

template <class T> inline size_t choose(T const& a, T const& b, size_t const& src1, size_t const& src2)
{
#if defined(__clang__) && defined(__x86_64)
    size_t res = src1;
    asm("cmpq %1, %2; cmovaeq %4, %0"
        :
    "=q" (res)
        :
        "q" (a),
        "q" (b),
        "q" (src1),
        "q" (src2),
        "0" (res)
        :
        "cc");
    return res;
#else
    return b >= a ? src2 : src1;
#endif
}

输出:

clang++ -std=c++14 -O2 -Wall -Wno-unused-variable -Wno-missing-braces -Werror main.cpp && ./a.out
Pow and log took 1408 milliseconds.
Multiplying by 2 took 351 milliseconds.
Binsearching precomputed table of powers took 359 milliseconds.
Binsearching (opti) precomputed table of powers took 153 milliseconds.

Live on coliru

答案 2 :(得分:1)

爬得更快但速度相同。

        uint multiply_quick(uint n)
        {
            if (n < 2u) return 1u;
            uint maxpow = 1u;

            if (n > 256u)
            {
                maxpow = 256u * 128u;

                // fast fixing the overshoot
                while (maxpow > n)
                    maxpow = maxpow >> 2;
                // fixing the undershoot
                while (2u * maxpow <= n)
                    maxpow *= 2u;
            }
            else
            {

                // quicker scan
                while (maxpow < n && maxpow != 256u)
                    maxpow *= maxpow;

                // fast fixing the overshoot
                while (maxpow > n)
                    maxpow = maxpow >> 2;

                // fixing the undershoot
                while (2u * maxpow <= n)
                    maxpow *= 2u;
            }
            return maxpow;
        }

这可能更适合使用65k常量字面值而不是256字节的32位变量。

答案 3 :(得分:0)

只是设置为0所有位,但第一个位。这应该是非常快速和有效的

答案 4 :(得分:0)

正如已经提到的@Jack,您可以简单地将除第一个之外的所有位设置为0。 在这里解决方案:

#include <iostream>

uint16_t bit_solution(uint16_t num)
{
    if ( num == 0 )
        return 0;

    uint16_t ret = 1;
    while (num >>= 1)
        ret <<= 1;

    return ret;
}

int main()
{
    std::cout << bit_solution(1024) << std::endl; //1024
    std::cout << bit_solution(1025) << std::endl; //1024
    std::cout << bit_solution(1023) << std::endl; //512
    std::cout << bit_solution(1) << std::endl; //1
    std::cout << bit_solution(0) << std::endl; //0
}

答案 5 :(得分:0)

好吧,它仍然是一个循环(并且它的循环计数取决于设置位的数量,因为它们被逐个重置),因此最坏的情况可能比使用块位操作的方法更糟。

但它很可爱。

uint_fast16_t bitunsetter(uint_fast16_t n)
{
  while (uint_fast16_t k = n & (n-1))
    n = k;
  return n;
}