【问题标题】:efficient bitwise sum calculation高效的按位和计算
【发布时间】:2021-11-27 17:27:22
【问题描述】:

有没有一种有效的方法来计算uint8_t 缓冲区的按位和(假设缓冲区的数量uint8)?基本上我想知道每个缓冲区的第 i 个位置设置了多少位。

例如:对于 2 个缓冲区

uint8 buf1[k] -> 0011 0001 ...
uint8 buf2[k] -> 0101 1000 ...
uint8 sum[k*8]-> 0 1 1 2 1 0 0 1... 

是否有针对此类需求的 BLAS 或 boost 例程?

这是一个高度矢量化的 IMO 操作。

更新: 以下是需求的简单实现

for (auto&& buf: buffers){
  for (int i = 0; i < buf_len; i++){
    for (int b = 0; b < 8; ++b) {
      sum[i*8 + b] += (buf[i] >> b) & 1;
    }
  }
}

【问题讨论】:

标签: c++ c algorithm blas bitset


【解决方案1】:

OP 朴素代码的替代方案:

一次执行 8 次加法。使用查找表将 8 位扩展为 8 个字节,每个位对应一个字节 - 请参阅ones[]

void sumit(uint8_t number_of_buf, uint8_t k, const uint8_t buf[number_of_buf][k]) {
  static const uint64_t ones[256] = { 0, 0x1, 0x100, 0x101, 0x10000, 0x10001, 
      /* 249 more pre-computed constants */ 0x0101010101010101};

  uint64_t sum[k];
  memset(sum, 0, sizeof sum):

  for (size_t buf_index = 0; buf_index < number_of_buf;  buf_index++) {
    for (size_t int i = 0; i < k; i++) {
      sum[i] += ones(buf[buf_index][i]);
    }
  }

  for (size_t int i = 0; i < k; i++) {
    for (size_t bit = 0; bit < 8;  bit++) {
      printf("%llu ", 0xFF & (sum[i] >> (8*bit)));
    }
  }
}

另见@Eric Postpischil

【讨论】:

  • @chux-ReinstateMonica 只是一个后续,这个 LUT 将是 2kB。您认为这会很重要并影响缓存性能吗?
  • @n1r44 也许吧。有许多潜在的优化。最好的通常涉及针对测试工具进行测试以实际评估代码。由于 OP 没有提供这一点,我们还有许多悬而未决的问题。 IMO,通常最佳解决方案涉及查看更高级别的任务,而不是像这里讨论的那样交易linear improvements。关于缓存,每年1亿个处理器是简单的嵌入式处理器。在 OP 的情况下,我们不知道平台。
【解决方案2】:

作为对 chux 方法的修改,查找表可以替换为向量移位和掩码。这是一个使用 GCC's vector extensions 的示例。

#include <stdint.h>
#include <stddef.h>

typedef uint8_t vec8x8 __attribute__((vector_size(8)));

void sumit(uint8_t number_of_buf,
           uint8_t k,
           const uint8_t buf[number_of_buf][k],
           vec8x8 * restrict sums) {
    static const vec8x8 shift = {0,1,2,3,4,5,6,7};

    for (size_t i = 0; i < k; i++) {
        sums[i] = (vec8x8){0};
        for (size_t buf_index = 0; buf_index < number_of_buf;  buf_index++) {
            sums[i] += (buf[buf_index][i] >> shift) & 1;
        }
    }
}

Try it on godbolt.

我交换了 chux 的答案中的循环,因为一次累积一个缓冲区索引的总和似乎更自然(然后可以将总和缓存在整个内部循环的寄存器中)。缓存性能可能会有所折衷,因为我们现在必须以列优先顺序读取二维 buf 的元素。

以ARM64为例,GCC 11.1编译内循环如下。

// v1 = sums[i]
// v2 = {0,-1,-2,...,-7} (right shift is done as left shift with negative count)
// v3 = {1,1,1,1,1,1,1,1}
.L4:
        ld1r    {v0.8b}, [x1]         // replicate buf[buf_index][i] to all elements of v0
        add     x0, x0, 1             
        add     x1, x1, x20
        ushl    v0.8b, v0.8b, v2.8b   // shift
        and     v0.8b, v0.8b, v3.8b   // mask
        add     v1.8b, v1.8b, v0.8b   // accumulate
        cmp     x0, x19
        bne     .L4

我认为一次执行两个字节会更有效(因此将 i 上的循环展开 2 倍)并使用 128 位向量操作。我把它留作练习:)

我不清楚这最终会比查找表快还是慢。您可能必须在感兴趣的目标机器上对两者进行概要分析。

【讨论】:

  • 我认为一次遍历缓冲区会更有效率。
  • @n1r44:你当然可以尝试两种方式来看看。您的方式的权衡是我们必须在内部循环的每次迭代中执行额外的 sums[i] 加载和存储,因此您必须担心 sums 在缓存中保持热状态。哪一个获胜可能取决于k 的大小。
猜你喜欢
  • 2014-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-25
  • 2021-10-28
  • 2021-09-23
  • 1970-01-01
相关资源
最近更新 更多