【问题标题】:bit slicing: finding minimum value位切片:找到最小值
【发布时间】:2020-05-28 12:13:42
【问题描述】:

短版

我需要找到编码为位切片的 64 个uint8_t 变量的最小值。

即变量的每一位都被编码成八个独立的uint64_t

//Normal layout:
uint8_t values[64]; // This is what you normally use. 
                    // Finding minimum would be a simple 
                    // matter of a for loop

/***********************/

// BITSLICE layout:
uint64_t slices[8]; // This is what I have, due to performance 
                    // reasons in other parts of the code (not shown here)

slice[0]; //LSB: Least signignificant bit (for all 64 values)
slice[7]; //MSB: Most significant bit (for all 64 values)

现在,我如何找出这些的最小值?(我不关心它的位置,只关心它的值)

更多上下文:

实际上,出于性能原因,我在一个已经使用位切片的算法中有一个更长的数组(比 64 个)。

所以我所拥有的实际上更像(上面的问题被简化了):

uint64_t slices[8][100];

所以我真正需要的是所有 100*64 值中的最小值。但我认为这可以通过应用上述简化问题的答案在常规 for 循环中完成。

编辑:显然我的问题并没有我想的那么清楚,所以它已经更新了

【问题讨论】:

  • 我觉得这个问题在初读时无法理解,也不是很清楚。你能添加更多关于预期结果的细节吗?
  • 是指最小的 8 位值,还是最小的 64 位值放在一起?您还提到了 64 个uint8_t 位切片变量,但仅显示 8 个数组元素。
  • 也许你需要一个循环?
  • 我已经更新了这个问题。我希望现在更清楚了。

标签: c bit-manipulation 64-bit bitwise-operators


【解决方案1】:

我至少能想到两种方法。最简单的方法是暴力破解:通过适当的按位算术一次一个地重构 64 个整数中的每一个,并跟踪最小结果。大致如下:

uint8_t min = 0xff;

// iterate over the collection of values
for (uint64_t which = 1; which; which <<= 1) {
    // reconstitute one value in 'test'
    uint8_t test = 0;

    for (int bit = 0; bit < 8; bit++) {
        // verify this decoding -- your bit order may be different:
        test += (!!(slices[bit] & which)) << bit;
    }

    // track the minimum
    if (test < min) {
        min = test;
    }
}

另一方面,通过slices 扫描一次并直接累积最小值也应该可以更快地完成。我没有时间对此进行测试,但它应该传达了大致的想法:

uint8_t min = 0xff;
uint64_t mask = ~(uint64_t)0;  // a mask of candidate positions; all bits initially set

for (int i = 7; i >= 0; i--) {  // assumes slice 7 is most significant
    // which of the remaining candidates have this bit set:
    uint64_t bits_set = slice[i] & mask;

    // If at least one of the remaining candidates does not have this bit set
    if (bits_set != mask) {
        min ^= (1 << i);   // turn off this bit in the result
        mask ^= bits_set;  // remove the candidates that do have this bit set
    }
}

后者类似于基数排序。

【讨论】:

  • 我喜欢第二种方法。假设您检查 b7 中的每个 64 位“行”。如果它们都是0 或所有1,则移至下一行。否则,消除所有带有1 位的列,直到只剩下一列,或者完成 b0,此时必须有重复的最小值。
【解决方案2】:

这里有一些简单而高效的函数,用于计算编码为 8 个uint64_t 包的 64 字节值集合的最小值和最大值,每个包存储 64 个值中的每个值的 1 位:

#include <stdint.h>

uint8_t maxslice(const uint64_t s[8]) {
    uint8_t max = 0, bit = 0x80;
    uint64_t mask = ~0ULL;
    for (int i = 8; i-- > 0; bit >>= 1) {
        uint64_t x = s[i] & mask;
        if (x) {
            max |= bit;
            mask &= x;
        }
    }
    return max;
}

uint8_t minslice(const uint64_t s[8]) {
    uint8_t min = 0, bit = 0x80;
    uint64_t mask = ~0ULL;
    for (int i = 8; i-- > 0; bit >>= 1) {
        uint64_t x = ~s[i] & mask;
        if (x) {
            min |= bit;
            mask &= x;
        }
    }
    return ~min;
}

正如可以在Godbolt's Compiler Explorer 上验证的那样,clang 为这两个函数生成无分支代码。

为了计算以这种方式组织的一组较大值中的最小值的扩展目标uint64_t slices[8][100],您可以简单地在数组上迭代此代码并逐步计算最小值。如果已经找到了0 的绝对最小值,那么在此循环的每个步骤中进行测试可能是值得的。棘手的部分是数组的组织方式:

uint64_t slices[8][100] 定义了一个由 100 个 uint64_t 组成的 8 个数组的数组。换句话说,内存中的布局是 6400 个低阶位,然后是 6400 个 2 阶位,...,最后是 6400 个权重 128 位。

uint8_t minarray(const uint64_t s[8][100]) {
    uint8_t all_max = 0;
    for (int j = 0; j < 100; j++) {
        uint8_t max = 0, bit = 0x80;
        uint64_t mask = ~0ULL;
        for (int i = 8; i-- > 0; bit >>= 1) {
            uint64_t x = ~s[i][j] & mask;
            if (x) {
                max |= bit;
                mask &= x;
            }
        }
        if (all_max < max) {
            all_max = max;
            if (all_max == 255)
                break;
        }
    }
    return ~all_max;
}

为了向量化这段代码,我们可以转置循环:用xmask作为100个数组计算uint64_t会产生相同的结果,但会让编译器向量化一些内部循环:

uint8_t minarray1(const uint64_t s[8][100]) {
    uint8_t max = 0, bit = 0x80;
    uint64_t mask[100] = {
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
        ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL, ~0ULL,
    };
    for (int i = 8; i-- > 0; bit >>= 1) {
        uint64_t x[100];
        uint64_t xall = 0;
        for (int j = 0; j < 100; j++) {
            x[j] = ~s[i][j] & mask[j];
            xall |= x[j];
        }
        if (xall) {
            max |= bit;
            for (int j = 0; j < 100; j++) {
                mask[j] &= x[j];
            }
        }
    }
    return ~max;
}

再次 clang 生成 unrolled vectorized code。基准测试将判断这种方法是否比前一种方法提供更好的性能。

【讨论】:

  • 我将已接受的答案移至此答案,因为它是关于附加上下文的最完整答案。也因为你也展示了 max 函数,即使它没有被要求。 +1
【解决方案3】:

使用联合:

#include <stdio.h>
#include <inttypes.h>

int main()
  {
  union
    {
    uint64_t slices[8];
    uint8_t  bits[64];
    } a_union;

  int     i;
  uint8_t min;

  for(i = 0 ; i < sizeof(a_union.slices)/sizeof(a_union.slices[0]) ; ++i)
    {
    a_union.slices[i] = (i+1) * 0x1122334455667788;
    printf("a_union.slices[%d] = 0x%"PRIX64"\n", i, a_union.slices[i]);
    }

  for(i = 0, min = 255 ; i < sizeof(a_union.bits) ; ++i)
    if(a_union.bits[i] < min)
      min = a_union.bits[i];

  printf("min = %u (0x%X)\n", min, min);
  }

onlinegdb test here

编辑

更好 - 使用 Duff 的设备。

#include <stdio.h>
#include <inttypes.h>
#include <limits.h>
#include <stdlib.h>

uint8_t min_in_mem_block(uint8_t *p, size_t len)
  {
  /* Find the minimum byte value in the block of memory of length len pointed to by p */

  size_t  n   = (len + 7) / 8;
  uint8_t min = UINT8_MAX;

  switch (len % 8) 
    {
    case 0: do { min = *p < min ? *p : min; p++;
    case 7:      min = *p < min ? *p : min; p++;
    case 6:      min = *p < min ? *p : min; p++;
    case 5:      min = *p < min ? *p : min; p++;
    case 4:      min = *p < min ? *p : min; p++;
    case 3:      min = *p < min ? *p : min; p++;
    case 2:      min = *p < min ? *p : min; p++;
    case 1:      min = *p < min ? *p : min; p++;
               } while (--n > 0);
    }

  return min;
  }

int main()
  {
  uint64_t block[8];

  for(size_t i = 0 ; i < sizeof(block)/sizeof(block[0]) ; ++i)
    {
    block[i] = ((i+1) * 0x1122334455667788u) | 0x0101010101010101;
    printf("block[%zu] = 0x%"PRIX64"\n", i, block[i]);
    }

  uint8_t min = min_in_mem_block((uint8_t *)block, sizeof(block));

  printf("min = %" PRIX8 "\n", min);
  }

onlinegdb test here

【讨论】:

  • 使用联合不是一个好主意,因为这会使代码不必要地依赖字节序。最好在 uint64_t 上使用位移位和掩码,这样您就可以获得 100% 可移植的代码。
  • 另外(7+1) * 0x1122334455667788 会给你一个有符号long long 类型的整数溢出。在十六进制常量上使用u 后缀。
  • @Lundin:字节序对于在 64 字节内存块中查找最小字节值有何影响?
  • Endianess 很重要,因为您不知道哪个字节包含最小值,即使您会打印“byte 0”等。使用 union 进行逐字节类型双关语通常不是一个好习惯。
  • @Lundin:关于知道哪个字节包含最小值的要求中没有任何内容 - 只是为了找到它。
猜你喜欢
  • 1970-01-01
  • 2012-08-27
  • 2020-10-17
  • 1970-01-01
  • 1970-01-01
  • 2014-04-03
  • 2017-09-06
  • 1970-01-01
  • 2012-06-21
相关资源
最近更新 更多