【问题标题】:How to efficiently count the highest power of 2 that is less than or equal to a given number? [duplicate]如何有效地计算小于或等于给定数字的 2 的最高幂? [复制]
【发布时间】:2021-02-24 17:56:55
【问题描述】:

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

效率极低的标准库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;
}

迄今为止最有效的对预先计算的 2 幂表进行 bin 搜索:

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.

【问题讨论】:

  • 您需要什么类型的二的幂?像2**3 这样的整数或像2**(1/2)2**0.1 这样的任何(非)有理数?生成 2 的 整体 次方的最有效方法是位移,因此,您实际上不需要 pow(2, integer),您可以改为使用 2 &lt;&lt; integer
  • 我认为您的基准由于分支预测而被打破。您应该为 n 使用随机值。
  • 您的意思是问“如何有效地找到 ...”(而不是“计数”)?
  • 你知道表的最后一个元素超出了 16 位整数的范围吧?

标签: c++ optimization


【解决方案1】:

在 cmets 中已经建议了具有内在函数的版本,所以这里有一个不依赖它们的版本:

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 &gt;&gt; 1) 仅保留与它们直接左侧的位不同的位(msb 被认为左侧有一个 0) ,这只是最高设置位,因为由于涂抹,数字的形式为 0n1m (以字符串表示法,而不是数字幂)。


由于没有人实际发布它,因此您可以编写内部函数(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 &amp; -x 隔离最低设置位,由位反转包围它隔离最高设置位。

【讨论】:

  • 您可以将第一个解决方案的最后一条语句简化为:return x ^ (x &gt;&gt; 1);
  • 没错,让我们这样做
  • 一看到这个问题,在我阅读你的答案之前,我的脑海里立刻就想到了“2的幂?只要检查一下!”。绝对是最有效的方法。
【解决方案2】:

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

这可以进一步优化吗?有什么可以在这里使用的技巧吗?

是的,我们可以!让我们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 for 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)

【讨论】:

  • 如果你还是要使用内联汇编,为什么不使用 BSR?
  • @harold 我什至没有想到它>_
  • 好吧,我不知道如何用 GCC 的语法编写它,但是您可以扫描输入中的最高设置位,然后将 1 左移该数量,对吧?
  • 我通过手动编写二叉搜索树从fast_upper_bound(随机输入)获得了约 33% 的速度提升(这很乏味,但速度是这里最重要的事情)
  • @Sopel 我虽然打算这样做,但在开始之前就气馁了。
【解决方案3】:

嗯,它仍然是一个循环(它的循环计数取决于设置的位数,因为它们被一一重置),所以它的最坏情况可能比使用块位操作的方法更糟糕。

但它很可爱。

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

【讨论】:

    【解决方案4】:

    爬得更快,但以相同的速度回落。

            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 位变量。

    【讨论】:

      【解决方案5】:

      正如@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
      }
      

      【讨论】:

      • 我有benchmarked it,它没有帮助。
      • 将在一秒钟内进行基准测试。我在想-O2 会使这个解决方案与我计算随后的幂几乎相同,我可能在这里错了。
      • @YSC 很有趣。我猜 multiply_only_once 和我的解决方案是相同的解决方案,除了 0 检查。还要检查this solution
      • 不错!你应该edit你的答案或发布另一个答案。 (糟糕,哈罗德已经这么做了)。
      • 很有趣。您链接到的网站声称:“使用公式和使用查找表的 log base 2 方法通过 2 次操作会更快”好吧,您的基准测试似乎提出了一些不同的建议。
      【解决方案6】:

      只需将除第一个以外的所有位设置为 0。这应该非常快速和高效

      【讨论】:

      • 在 C++ 中执行此操作的速度不一定像在某些汇编语言中一样快,但 C++ 编译器可能提供了有用的内部函数。例如,GCC 提供int __builtin_clz (unsigned int x) 来计算前导 0,然后可以使用它来右移 1^31。只要确实有 CPU 支持,这可能只需要几个时钟周期。
      • 你会怎么做? JustRufus 对这个想法的实现并不比 gcc 的 -O2 好。
      • 这与告诉 OP“只需返回正确的结果”一样有用。
      猜你喜欢
      • 2010-09-26
      • 2021-11-04
      • 2013-03-09
      • 2018-05-28
      • 2014-07-20
      • 2013-06-07
      • 2013-04-06
      • 1970-01-01
      相关资源
      最近更新 更多