【问题标题】:Which is the most efficient way to extract an arbitrary range of bits from a contiguous sequence of words?从连续的单词序列中提取任意位范围的最有效方法是什么?
【发布时间】:2015-02-21 10:50:20
【问题描述】:

假设我们有一个 std::vector 或任何其他序列容器(有时它是双端队列),它们存储 uint64_t 元素。

现在,让我们将此向量视为size() * 64 连续位的序列。我需要找到由给定[begin, end) 范围内的位组成的单词,假设end - begin <= 64 适合一个单词。

我现在的解决方案是找到两个单词的部分将形成结果,并分别屏蔽和组合它们。因为我需要它尽可能高效,所以我尝试在没有任何 if 分支的情况下对所有内容进行编码,以免导致分支错误预测,因此例如,当整个范围适合一个词或当它时,代码在这两种情况下都有效跨越两个词,没有采取不同的路径。为此,我需要编写 shiftlshiftr 函数,它们只会将单词移动指定的数量,例如 >><< 运算符,但优雅地处理 n 时的情况大于 64,否则将是未定义的行为。

另一点是,现在编码的 get() 函数也适用于数学意义上的空范围,例如不仅是如果开始 == 结束,而且如果开始 > 结束,这是调用此函数的主算法所要求的。同样,在这种情况下,我尝试在不简单地分支并返回零的情况下做到这一点。

但是,再看看汇编代码,这一切似乎太复杂了,无法执行如此看似简单的任务。这段代码运行在一个性能关键的算法中,运行速度有点太慢了。 valgrind 告诉我们这个函数被调用了 2.3 亿次,占总执行时间的 40%,所以我真的需要让它更快。

那么您能帮我找到一种更简单和/或更有效的方法来完成这项任务吗? 我不太关心可移植性太多。使用 x86 SIMD 内部函数 (SSE3/4/AVX ecc...) 或编译器内置函数的解决方案都可以,只要我可以使用 g++clang 编译它们。

我当前的代码如下:

using word_type = uint64_t;
const size_t W = 64;

// Shift right, but without being undefined behaviour if n >= 64
word_type shiftr(word_type val, size_t n)
{
    uint64_t good = n < W;

    return good * (val >> (n * good));
}

// Shift left, but without being undefined behaviour if n >= 64
word_type shiftl(word_type val, size_t n)
{
    uint64_t good = n < W;

    return good * (val << (n * good));
}

// Mask the word preserving only the lower n bits.
word_type lowbits(word_type val, size_t n)
{
    word_type mask = shiftr(word_type(-1), W - n);

    return val & mask;
}

// Struct for return values of locate()
struct range_location_t {
    size_t lindex; // The word where is located the 'begin' position
    size_t hindex; // The word where is located the 'end' position
    size_t lbegin; // The position of 'begin' into its word
    size_t llen;   // The length of the lower part of the word
    size_t hlen;   // The length of the higher part of the word
};

// Locate the one or two words that will make up the result
range_location_t locate(size_t begin, size_t end)
{
    size_t lindex = begin / W;
    size_t hindex = end / W;
    size_t lbegin = begin % W;
    size_t hend   = end % W;

    size_t len = (end - begin) * size_t(begin <= end);
    size_t hlen = hend * (hindex > lindex);
    size_t llen = len - hlen;

    return { lindex, hindex, lbegin, llen, hlen };
}

// Main function.
template<typename Container>
word_type get(Container const&container, size_t begin, size_t end)
{
    assert(begin < container.size() * W);
    assert(end <= container.size() * W);

    range_location_t loc = locate(begin, end);

    word_type low = lowbits(container[loc.lindex] >> loc.lbegin, loc.llen);

    word_type high = shiftl(lowbits(container[loc.hindex], loc.hlen), loc.llen);

    return high | low;
}

非常感谢。

【问题讨论】:

  • 使用std::bitset 作为中介怎么样?
  • 当您返回 'good*...' 时,您不能假设 'good' 为 0 或 1:这是不可移植的。改用三元运算符:'return good?...:0;'
  • @Christophe 在 C++ 中完全可移植。 &lt; 产生 bool,从它转换为任何其他整数只会产生 0 或 1。请参阅 C++11, 4.7/4。
  • 您是否真的分析了此代码的简单分支版本?
  • 另外,您是否运行了 cachegrind 来检查问题是否在此函数中没有很多缓存未命中(如果向量很大并且访问模式非常随机),而不是实际的 CPU 时间?因为那样的话,你需要优化这个函数的使用模式,而不是函数本身。

标签: c++ algorithm bit-manipulation simd intrinsics


【解决方案1】:

这将替换 get() 和 get() 使用的所有辅助函数。它包含一个条件分支并保存了大约 16 个算术运算,这意味着它通常应该运行得更快。经过一些优化后,它还生成了非常短的代码。最后解决了end==container.size()*W情况下导致container[container.size()]被访问的bug。

最棘手的部分是“hi-(hi>0)”,它从 hi 中减去 1,除非 hi 为 0。减去 1 不会改变任何内容,除非 hi 指向单词边界,即 hi%64 ==0。在这种情况下,我们需要来自上部容器条目的 0 位,因此仅使用下部容器条目就足够了。通过在计算 hi_off 之前减去 1,我们确保了条件“hi_off==lo_off”,我们遇到了更简单的情况。

在那个更简单的情况下,我们只需要一个容器入口并在两侧切掉一些位。 hi_val 就是那个入口,高位已经被切掉了,所以剩下要做的就是删除一些低位。

在不太简单的情况下,我们还必须读取较低的容器条目,去除其中未使用的字节,然后合并两个条目。

namespace {
  size_t   const upper_mask = ~(size_t)0u << 6u;
  unsigned const lower_mask = (unsigned)~upper_mask;
}

word_type get ( Container const &container, size_t lo, size_t hi )
{
  size_t lo_off = lo       >>6u;  assert ( lo_off < container.size() );
  size_t hi_off = hi-(hi>0)>>6u;  assert ( hi_off < container.size() );
  unsigned hi_shift = lower_mask&(unsigned)(upper_mask-hi);
  word_type hi_val = container[hi_off] << hi_shift >> hi_shift;
  unsigned lo_shift = lower_mask&(unsigned)lo;
  if ( hi_off == lo_off ) return hi_val >> lo_shift; // use hi_val as lower word
  return ( hi_val<<W-lo_shift | container[lo_off]>>lo_shift ) * (lo_off<hi_off);
}

【讨论】:

  • 非常有趣!不过我还是要好好理解一下……你为什么要转6?
  • >>6u 的作用与 /W 相同 - 它除以 64,计算包含所需位的单词的索引。通常编译器的优化器无论如何都应该看到,但以防万一它失败,我告诉它如何快速除以 64。顺便说一句,你会注意到我没有使用一个特殊的移位函数,它可以很好地移位 64 位或更多位。这是因为在我的实现中,所有移位操作的移位都小于 64 位。
  • 是的,这种转变很明显,抱歉......无论如何,我认为任何合理的编译器都不需要它。因此,您使用 lower_mask 来获得模 64 的数字,而不是使用 % 运算符,对吗?但是你为什么要upper_mask - hi
  • (upper_mask-hi)%64 与 (64-hi%64)%64 相同,只是更快。 hi_shift 是在高位字的左侧要切除的位数。通常与 64-hi%64 相同,但如果 hi 是 64 的倍数则不一样。这种情况下,64-hi%64 为 64,hi_off 与 lo_off 相同,因此 hi_val 将用作较低的word 并且我想在左侧不截断任何内容,因此 hi_shift 应该为 0。我通过首先从 64 的最高可能倍数中减去 hi 然后将 %64 应用于结果来实现这一点。这只是让 hi_shift 在所有情况下都具有我需要的价值的最快方法。
  • 我刚刚测试了速度。与原始版本相比,我的建议需要不到一半的时间,既使用 std::vector 也使用带有 -O2 的 g++ 编译,但我的建议很难阅读。关键是要理解这两种可能的情况,hi_val 与更简单情况下的低位单词相同的事实,以及如果 hi 是 64 的倍数而不为该情况添加另一个特殊检查的处理方式。不太简单的情况 hi_off!=lo_off 更简单。即使 hi 是 64 的倍数,简单的情况 hi_off==lo_off 也能正常工作,这不是很明显。
【解决方案2】:

正如聊天中所宣布的,我添加了一个精致的答案。它包含三个部分,每个部分后面都有对该部分的描述。

第 1 部分 get.h 是我的解决方案,但很笼统,有一处更正。

第二部分 got.h 是问题中发布的原始算法,也适用于任何无符号类型的任何 STL 容器。

第 3 部分 main.cpp 包含用于验证正确性和衡量性能的单元测试。

#include <cstddef>

using std::size_t;

template < typename C >
typename C::value_type get ( C const &container, size_t lo, size_t hi )
{

   typedef typename C::value_type item; // a container entry
   static unsigned const bits = (unsigned)sizeof(item)*8u; // bits in an item
   static size_t const mask = ~(size_t)0u/bits*bits; // huge multiple of bits

   // everthing above has been computed at compile time. Now do some work:

   size_t lo_adr = (lo       ) / bits; // the index in the container of ...
   size_t hi_adr = (hi-(hi>0)) / bits; // ... the lower or higher item needed

   // we read container[hi_adr] first and possibly delete the highest bits:

   unsigned hi_shift = (unsigned)(mask-hi)%bits;
   item hi_val = container[hi_adr] << hi_shift >> hi_shift;

   // if all bits are in the same item, we delete the lower bits and are done:

   unsigned lo_shift = (unsigned)lo%bits;
   if ( hi_adr <= lo_adr ) return (hi_val>>lo_shift) * (lo<hi);

   // else we have to read the lower item as well, and combine both

   return ( hi_val<<bits-lo_shift | container[lo_adr]>>lo_shift );

}

第一部分,上面的 get.h,是我最初的解决方案,但可以泛化为与任何无符号整数类型的 STL 容器一起使用。因此,您也可以使用和测试 32 位整数或 128 位整数。对于非常小的数字,我仍然使用 unsigned,但您也可以将它们替换为 size_t。该算法几乎没有变化,只是稍作修正——如果 lo 是容器中的总位数,我之前的 get() 将访问容器大小上方的项目。现在已修复。

#include <cstddef>

using std::size_t;

// Shift right, but without being undefined behaviour if n >= 64
template < typename val_type >
val_type shiftr(val_type val, size_t n)
{
   val_type good = n < sizeof(val_type)*8;
   return good * (val >> (n * good));
}

// Shift left, but without being undefined behaviour if n >= 64
template < typename val_type >
val_type shiftl(val_type val, size_t n)
{
   val_type good = n < sizeof(val_type)*8;
   return good * (val << (n * good));
}

// Mask the word preserving only the lower n bits.
template < typename val_type >
val_type lowbits(val_type val, size_t n)
{
    val_type mask = shiftr<val_type>((val_type)(-1), sizeof(val_type)*8 - n);
    return val & mask;
}

// Struct for return values of locate()
struct range_location_t {
   size_t lindex; // The word where is located the 'begin' position
   size_t hindex; // The word where is located the 'end' position
   size_t lbegin; // The position of 'begin' into its word
   size_t llen;   // The length of the lower part of the word
   size_t hlen;   // The length of the higher part of the word
};

// Locate the one or two words that will make up the result
template < typename val_type >
range_location_t locate(size_t begin, size_t end)
{
   size_t lindex = begin / (sizeof(val_type)*8);
   size_t hindex = end / (sizeof(val_type)*8);
   size_t lbegin = begin % (sizeof(val_type)*8);
   size_t hend   = end % (sizeof(val_type)*8);

   size_t len = (end - begin) * size_t(begin <= end);
   size_t hlen = hend * (hindex > lindex);
   size_t llen = len - hlen;

   range_location_t l = { lindex, hindex, lbegin, llen, hlen };
   return l;
}

// Main function.
template < typename C >
typename C::value_type got ( C const&container, size_t begin, size_t end )
{
   typedef typename C::value_type val_type;
   range_location_t loc = locate<val_type>(begin, end);
   val_type low = lowbits<val_type>(container[loc.lindex] >> loc.lbegin, loc.llen);
   val_type high = shiftl<val_type>(lowbits<val_type>(container[loc.hindex], loc.hlen), loc.llen);
   return high | low;
}

上面的第二部分 got.h 是问题中的原始算法,也被概括为接受任何无符号整数类型的任何 STL 容器。与 get.h 一样,该版本除了定义容器类型的单个模板参数外不使用任何定义,因此可以轻松测试其他项目大小或容器类型。

#include <vector>
#include <cstddef>
#include <stdint.h>
#include <stdio.h>
#include <sys/time.h>
#include <sys/resource.h>
#include "get.h"
#include "got.h"

template < typename Container > class Test {

   typedef typename Container::value_type val_type;
   typedef val_type (*fun_type) ( Container const &, size_t, size_t );
   typedef void (Test::*fun_test) ( unsigned, unsigned );
   static unsigned const total_bits = 256; // number of bits in the container
   static unsigned const entry_bits = (unsigned)sizeof(val_type)*8u;

   Container _container;
   fun_type _function;
   bool _failed;

   void get_value ( unsigned lo, unsigned hi ) {
      _function(_container,lo,hi); // we call this several times ...
      _function(_container,lo,hi); // ... because we measure ...
      _function(_container,lo,hi); // ... the performance ...
      _function(_container,lo,hi); // ... of _function, ....
      _function(_container,lo,hi); // ... not the performance ...
      _function(_container,lo,hi); // ... of get_value and ...
      _function(_container,lo,hi); // ... of the loop that ...
      _function(_container,lo,hi); // ... calls get_value.
   }

   void verify ( unsigned lo, unsigned hi ) {
      val_type value = _function(_container,lo,hi);
      if ( lo < hi ) {
         for ( unsigned i=lo; i<hi; i++ ) {
            val_type val = _container[i/entry_bits] >> i%entry_bits & 1u;
            if ( val != (value&1u) ) {
               printf("lo=%d hi=%d [%d] is'nt %d\n",lo,hi,i,(unsigned)val);
               _failed = true;
            }
            value >>= 1u;
         }
      }
      if ( value ) {
         printf("lo=%d hi=%d value contains high bits set to 1\n",lo,hi);
         _failed = true;
      }
   }

   void run ( fun_test fun ) {
      for ( unsigned lo=0; lo<total_bits; lo++ ) {
         unsigned h0 = 0;
         if ( lo > entry_bits ) h0 = lo - (entry_bits+1);
         unsigned h1 = lo+64;
         if ( h1 > total_bits ) h1 = total_bits;
         for ( unsigned hi=h0; hi<=h1; hi++ ) {
            (this->*fun)(lo,hi);
         }
      }
   }

   static uint64_t time_used ( ) {
      struct rusage ru;
      getrusage(RUSAGE_THREAD,&ru);
      struct timeval t = ru.ru_utime;
      return (uint64_t) t.tv_sec*1000 + t.tv_usec/1000;
   }

public:

   Test ( fun_type function ): _function(function), _failed() {
      val_type entry;
      unsigned index = 0; // position in the whole bit array
      unsigned value = 0; // last value assigned to a bit
      static char const entropy[] = "The quick brown Fox jumps over the lazy Dog";
      do {
         if ( ! (index%entry_bits) ) entry = 0;
         entry <<= 1;
         entry |= value ^= 1u & entropy[index/7%sizeof(entropy)] >> index%7;
         ++index;
         if ( ! (index%entry_bits) ) _container.push_back(entry);
      } while ( index < total_bits );
   }

   bool correctness() {
      _failed = false;
      run(&Test::verify);
      return !_failed;
   }

   void performance() {
      uint64_t t1 = time_used();
      for ( unsigned i=0; i<999; i++ ) run(&Test::get_value);
      uint64_t t2 = time_used();
      printf("used %d ms\n",(unsigned)(t2-t1));
   }

   void operator() ( char const * name ) {
      printf("testing %s\n",name);
      correctness();
      performance();
   }

};

int main()
{
   typedef typename std::vector<uint64_t> Container;
   Test<Container> test(get<Container>); test("get");
   Test<Container> tost(got<Container>); tost("got");
}

上面的第 3 部分 main.cpp 包含一类单元测试并将它们应用于 get.h 和 got.h,即应用于我的解决方案和问题的原始代码,稍作修改。单元测试验证正确性并测量速度。他们通过创建一个 256 位的容器来验证正确性,用一些数据填充它,读取所有可能的位部分(最多可容纳到容器条目中的位数以及许多病理情况),并验证每个结果的正确性。他们通过再次经常阅读相同的部分并报告线程在用户空间中使用的时间来衡量速度。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-06-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-20
    • 2021-10-05
    • 2013-01-08
    • 1970-01-01
    相关资源
    最近更新 更多