【问题标题】:Extract 14-bit values from an array of bytes in C从 C 中的字节数组中提取 14 位值
【发布时间】:2016-01-15 21:21:33
【问题描述】:

在 C 语言中的任意大小的字节数组中,我想存储 14 位数字 (0-16,383) 紧密排列。也就是说,在序列中:

0000000000000100000000000001

有两个数字我希望能够任意存储和检索成 16 位整数。 (在这种情况下,它们都是 1,但可以是给定范围内的任何值)如果我有函数 uint16_t 14bitarr_get(unsigned char* arr, unsigned int index)void 14bitarr_set(unsigned char* arr, unsigned int index, uint16_t value),我将如何实现这些函数?

这不是一个家庭作业项目,只是我自己的好奇心。我有一个特定的项目可以使用它,它是整个项目的关键/中心。

我不想要一个包含 14 位值的结构数组,因为这会为每个存储的结构产生浪费位。我希望能够将尽可能多的 14 位值紧密打包到一个字节数组中。 (例如:在我发表的评论中,将尽可能多的 14 位值放入 64 字节的块中是可取的,没有浪费位。这些 64 字节的工作方式对于特定用例来说是完全紧凑的,这样即使是一点点的浪费会带走存储另一个 14 位值的能力)

【问题讨论】:

  • 您所描述的技术称为“打包位”或“位打包”。如果您知道这一点,则查找有关如何操作的信息会容易得多。特别是catb.org/esr/structure-packing
  • @RobertHarvey - 链接到的文章似乎主要是关于结构成员的排序以避免填充。
  • 这是关于单词对齐的。你将需要知道这一点。还有很多其他文章我没有链接。
  • 我不认为两个 14 位数字适合 16 位整数。它应该是 16 位整数数组。
  • @MikeCAT:是的。再次阅读问题。

标签: c arrays bit-manipulation


【解决方案1】:

最简单的解决方案是使用八个位域的struct

typedef struct __attribute__((__packed__)) EightValues {
    uint16_t v0 : 14,
             v1 : 14,
             v2 : 14,
             v3 : 14,
             v4 : 14,
             v5 : 14,
             v6 : 14,
             v7 : 14;
} EightValues;

该结构的大小为14*8 = 112 位,即14 个字节(七个uint16_t)。现在,您只需要使用数组索引的最后三位来选择正确的位域:

uint16_t 14bitarr_get(unsigned char* arr, unsigned int index) {
    EightValues* accessPointer = (EightValues*)arr;
    accessPointer += index >> 3;    //select the right structure in the array
    switch(index & 7) {    //use the last three bits of the index to access the right bitfield
        case 0: return accessPointer->v0;
        case 1: return accessPointer->v1;
        case 2: return accessPointer->v2;
        case 3: return accessPointer->v3;
        case 4: return accessPointer->v4;
        case 5: return accessPointer->v5;
        case 6: return accessPointer->v6;
        case 7: return accessPointer->v7;
    }
}

你的编译器会帮你搞定。

【讨论】:

  • 不错的尝试,但默认情况下这通常不会起作用,因为整体结构通常会在单词边界处获得额外的填充(不保证,但非常常见)。最安全的方法是将元素的数量扩展到 16 个(即 14 个字),因为对齐通常不比硬件级别更严格,即使在 64 位平台上也是如此(处理 64 位值时除外)。
  • @DonalFellows 位域的基本数据类型为uint16_t,其中7个将被分配。因此,我假设整个结构将被填充并与uint16_t 边界对齐。但我同意我可能过于自信了,应该声明结构是打包的。我现在已经添加了。
【解决方案2】:

嗯,这是最好的一点摆弄。用一个字节数组做它比用更大的元素更复杂,因为一个 14 位的数量可以跨越 3 个字节,其中 uint16_t 或更大的任何东西都需要不超过两个。但我会相信你的话,这就是你想要的(不是双关语)。此代码实际上将使用设置为 8 或更大的常量(但不会超过 int 的大小;为此,需要额外的类型转换)。当然如果大于 16 则需要调整值类型。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define W 14

uint16_t arr_get(unsigned char* arr, size_t index) {
  size_t bit_index = W * index;
  size_t byte_index = bit_index / 8;
  unsigned bit_in_byte_index = bit_index % 8;
  uint16_t result = arr[byte_index] >> bit_in_byte_index;
  for (unsigned n_bits = 8 - bit_in_byte_index; n_bits < W; n_bits += 8)
    result |= arr[++byte_index] << n_bits;
  return result & ~(~0u << W);
}

void arr_set(unsigned char* arr, size_t index, uint16_t value) {
  size_t bit_index = W * index;
  size_t byte_index = bit_index / 8;
  unsigned bit_in_byte_index = bit_index % 8;
  arr[byte_index] &= ~(0xff << bit_in_byte_index);
  arr[byte_index++] |= value << bit_in_byte_index;
  unsigned n_bits = 8 - bit_in_byte_index;
  value >>= n_bits;
  while (n_bits < W - 8) {
    arr[byte_index++] = value;
    value >>= 8;
    n_bits += 8;
  }
  arr[byte_index] &= 0xff << (W - n_bits);
  arr[byte_index] |= value;
}

int main(void) {
  int mod = 1 << W;
  int n = 50000;
  unsigned x[n];
  unsigned char b[2 * n];
  for (int tries = 0; tries < 10000; tries++) {
    for (int i = 0; i < n; i++) {
      x[i] = rand() % mod;
      arr_set(b, i, x[i]);
    }
    for (int i = 0; i < n; i++)
      if (arr_get(b, i) != x[i])
        printf("Err @%d: %d should be %d\n", i, arr_get(b, i), x[i]);
  }
  return 0;
}

更快的版本 既然您在 cmets 中说过性能是一个问题:在原始版本中包含的小测试驱动程序上,开放编码循环使我的机器的速度提高了大约 10%。这包括随机数生成和测试,因此原语可能快 20%。我相信 16 位或 32 位数组元素会提供进一步的改进,因为字节访问很昂贵:

uint16_t arr_get(unsigned char* a, size_t i) {
  size_t ib = 14 * i;
  size_t iy = ib / 8;
  switch (ib % 8) {
  case 0:
    return (a[iy] | (a[iy+1] << 8)) & 0x3fff;
  case 2:
    return ((a[iy] >> 2) | (a[iy+1] << 6)) & 0x3fff;
  case 4:
    return ((a[iy] >> 4) | (a[iy+1] << 4) | (a[iy+2] << 12)) & 0x3fff;
  }
  return ((a[iy] >> 6) | (a[iy+1] << 2) | (a[iy+2] << 10)) & 0x3fff;
}

#define M(IB) (~0u << (IB))
#define SETLO(IY, IB, V) a[IY] = (a[IY] & M(IB)) | ((V) >> (14 - (IB)))
#define SETHI(IY, IB, V) a[IY] = (a[IY] & ~M(IB)) | ((V) << (IB))

void arr_set(unsigned char* a, size_t i, uint16_t val) {
  size_t ib = 14 * i;
  size_t iy = ib / 8;
  switch (ib % 8) {
  case 0:
    a[iy] = val;
    SETLO(iy+1, 6, val);
    return;
  case 2:
    SETHI(iy, 2, val);
    a[iy+1] = val >> 6;
    return;
  case 4:
    SETHI(iy, 4, val);
    a[iy+1] = val >> 4;
    SETLO(iy+2, 2, val);
    return;
  }
  SETHI(iy, 6, val);
  a[iy+1] = val >> 2;
  SETLO(iy+2, 4, val);
}

另一种变体 这在我的机器上要快得多,比上面的要快 20%:

uint16_t arr_get2(unsigned char* a, size_t i) {
  size_t ib = i * 14;
  size_t iy = ib / 8;
  unsigned buf = a[iy] | (a[iy+1] << 8) | (a[iy+2] << 16);
  return (buf >> (ib % 8)) & 0x3fff;
}

void arr_set2(unsigned char* a, size_t i, unsigned val) {
  size_t ib = i * 14;
  size_t iy = ib / 8;
  unsigned buf = a[iy] | (a[iy+1] << 8) | (a[iy+2] << 16);
  unsigned io = ib % 8;
  buf = (buf & ~(0x3fff << io)) | (val << io);
  a[iy] = buf;
  a[iy+1] = buf >> 8;
  a[iy+2] = buf >> 16;
}

请注意,为了安全起见,您应该在打包数组的末尾分配一个额外的字节。即使所需的 14 位在前 2 位中,它也始终读取和写入 3 个字节。

另一种变体 最后,它的运行速度比上面的慢一点(同样在我的机器上;YMMV),但您不需要额外的字节。每个操作使用一个比较:

uint16_t arr_get2(unsigned char* a, size_t i) {
  size_t ib = i * 14;
  size_t iy = ib / 8;
  unsigned io = ib % 8;
  unsigned buf = ib % 8 <= 2
      ? a[iy] | (a[iy+1] << 8)
      : a[iy] | (a[iy+1] << 8) | (a[iy+2] << 16);
  return (buf >> io) & 0x3fff;
}

void arr_set2(unsigned char* a, size_t i, unsigned val) {
  size_t ib = i * 14;
  size_t iy = ib / 8;
  unsigned io = ib % 8;
  if (io <= 2) {
    unsigned buf = a[iy] | (a[iy+1] << 8);
    buf = (buf & ~(0x3fff << io)) | (val << io);
    a[iy] = buf;
    a[iy+1] = buf >> 8;
  } else {
    unsigned buf = a[iy] | (a[iy+1] << 8) | (a[iy+2] << 16);
    buf = (buf & ~(0x3fff << io)) | (val << io);
    a[iy] = buf;
    a[iy+1] = buf >> 8;
    a[iy+2] = buf >> 16;
  }
}

【讨论】:

  • 我有兴趣查看在 uint16_t 数组上运行的版本。但事实上,这似乎是我的目的的最佳解决方案,因为它似乎是最快的解决方案。 (虽然我想知道在 uint16_t 数组上操作是否会更快)
  • @Freezerburn 你没有提到速度很重要。编码 14 位字节自定义解决方案可能有一些更快的方法(疯狂猜测 10% 到 50%)。在这里,我试图笼统。
  • 啊,对不起。如果有必要,您是否知道我可以用来构建更快的解决方案的任何资源? (实际上,在 -O3 下,如果我的时间正确,set 需要约 11 纳秒,而 get 是约 5 纳秒,考虑到微基准擅长撒谎。至少现在这对我的目的来说应该足够了)
  • 如前所述,具有固定指令序列的 switch / case 提高了性能。我的答案中的示例没有完全优化(使用后增量而不是索引 + 1),但给出了这个想法。数组数据一次可以读取或写入 32 位,但由于大部分时间不会对齐,我不确定这对性能有多大帮助。
  • @Freezerburn 我添加了另一个变体,它在我的机器上仍然快 20%。它根本没有分支。
【解决方案3】:

这是我的版本(已更新以修复错误):

#define PACKWID        14                    // number of bits in packed number
#define PACKMSK    ((1 << PACKWID) - 1)

#ifndef ARCHBYTEALIGN
#define ARCHBYTEALIGN    1                // align to 1=bytes, 2=words
#endif
#define ARCHBITALIGN    (ARCHBYTEALIGN * 8)

typedef unsigned char byte;
typedef unsigned short u16;
typedef unsigned int u32;
typedef long long s64;

typedef u16 pcknum_t;                    // container for packed number
typedef u32 acc_t;                        // working accumulator

#ifndef ARYOFF
#define ARYOFF long
#endif
#define PRT(_val)    ((unsigned long) _val)
typedef unsigned ARYOFF aryoff_t;            // bit offset

// packary -- access array of packed numbers
// RETURNS: old value
extern inline pcknum_t
packary(byte *ary,aryoff_t idx,int setflg,pcknum_t newval)
// ary -- byte array pointer
// idx -- index into array (packed number relative)
// setflg -- 1=set new value, 0=just get old value
// newval -- new value to set (if setflg set)
{
    aryoff_t absbitoff;
    aryoff_t bytoff;
    aryoff_t absbitlhs;
    acc_t acc;
    acc_t nval;
    int shf;
    acc_t curmsk;
    pcknum_t oldval;

    // get the absolute bit number for the given array index
    absbitoff = idx * PACKWID;

    // get the byte offset of the lowest byte containing the number
    bytoff = absbitoff / ARCHBITALIGN;

    // get absolute bit offset of first containing byte
    absbitlhs = bytoff * ARCHBITALIGN;

    // get amount we need to shift things by:
    // (1) our accumulator
    // (2) values to set/get
    shf = absbitoff - absbitlhs;

#ifdef MODSHOW
    do {
        static int modshow;

        if (modshow > 50)
            break;
        ++modshow;

        printf("packary: MODSHOW idx=%ld shf=%d bytoff=%ld absbitlhs=%ld absbitoff=%ld\n",
            PRT(idx),shf,PRT(bytoff),PRT(absbitlhs),PRT(absbitoff));
    } while (0);
#endif

    // adjust array pointer to the portion we want (guaranteed to span)
    ary += bytoff * ARCHBYTEALIGN;

    // fetch the number + some other bits
    acc = *(acc_t *) ary;

    // get the old value
    oldval = (acc >> shf) & PACKMSK;

    // set the new value
    if (setflg) {
        // get shifted mask for packed number
        curmsk = PACKMSK << shf;

        // remove the old value
        acc &= ~curmsk;

        // ensure caller doesn't pass us a bad value
        nval = newval;
#if 0
        nval &= PACKMSK;
#endif
        nval <<= shf;

        // add in the value
        acc |= nval;

        *(acc_t *) ary = acc;
    }

    return oldval;
}

pcknum_t
int_get(byte *ary,aryoff_t idx)
{

    return packary(ary,idx,0,0);
}

void
int_set(byte *ary,aryoff_t idx,pcknum_t newval)
{

    packary(ary,idx,1,newval);
}

以下是基准:

设置:354740751 7.095 -- 基因
设置:203407176 4.068 -- rcgldr
套装:298946533 5.979 -- 克雷格

获取:268574627 5.371 -- 基因
获取:166839767 3.337 -- rcgldr
得到:207764612 4.155——克雷格

【讨论】:

  • 这似乎是位打包的小端版本。 OP没有提到他是想要大端还是小端位包装。它还假设 32 位读/写不必对齐。
  • @rcgldr 是的。在 BE 架构上,在 int fetch 之后和 store 之前,只需在 acc 上添加一个 endian swap [为简洁起见省略]。但是,只有当拱门是 BE [CPU 也没有真空管:-)] 时,BE 才真正有意义(仍然没问题,因为阵列只能通过访问函数访问)。几乎所有 bigint 包都执行 LE。我从头开始编写自己的。我曾经讨厌 LE,直到我详细比较了它——它让一切变得更简单。而且,自 80 年代以来,大多数拱门上都不需要对齐 int 提取。甚至古老的 IBM/370 也通过 ICM 支持未对齐。
  • 我在考虑标准压缩格式,其中大部分是大端 (BE)。我确实记得备份 DAT 磁带驱动器使用了小端 (LE) 压缩格式,但我所知道的几乎所有其他东西都使用大端格式。至于对齐问题,68000 系列和更早的 ARM 系列需要对齐数据。对于其他读取此内容的人,BE 将顺序数据读入工作寄存器的低位并左移以获取代码,LE 将顺序数据读入工作寄存器的高位并右移。
  • @rcgldr 修复了错误并添加了字对齐。两个 LE:用于单元格(例如 int)的 arch LE 和用于 bigint 向量的 LE。拱决定细胞。但是,总是将 LE 用于 vec。当 mult n-digit num * m-digit num 时,你得到 (n+m) digit num。使用 vec LE,很容易通过 realloc 等扩展 vec 大小。
【解决方案4】:

更新 - 假设您想要大端位打包。这是用于固定大小代码字的代码。它基于我用于数据压缩算法的代码。开关盒和固定逻辑有助于提高性能。

typedef unsigned short uint16_t;

void bit14arr_set(unsigned char* arr, unsigned int index, uint16_t value)
{
unsigned int bitofs = (index*14)%8;
    arr += (index*14)/8;
    switch(bitofs){
        case  0:   /* bit offset == 0 */
            *arr++  = (unsigned char)(value >>  6);
            *arr   &= 0x03;
            *arr   |= (unsigned char)(value <<  2);
            break;
        case  2:   /* bit offset == 2 */
            *arr   &= 0xc0;
            *arr++ |= (unsigned char)(value >>  8);
            *arr    = (unsigned char)(value <<  0);
            break;
        case  4:   /* bit offset == 4 */
            *arr   &= 0xf0;
            *arr++ |= (unsigned char)(value >> 10);
            *arr++  = (unsigned char)(value >>  2);
            *arr   &= 0x3f;             
            *arr   |= (unsigned char)(value <<  6);
            break;
        case  6:   /* bit offset == 6 */
            *arr   &= 0xfc;
            *arr++ |= (unsigned char)(value >> 12);
            *arr++  = (unsigned char)(value >>  4);
            *arr   &= 0x0f;
            *arr   |= (unsigned char)(value <<  4);
            break;
    }
}

uint16_t bit14arr_get(unsigned char* arr, unsigned int index)
{
unsigned int bitofs = (index*14)%8;
unsigned short value;
    arr += (index*14)/8;
    switch(bitofs){
        case  0:   /* bit offset == 0 */
            value  = ((unsigned int)(*arr++)     ) <<  6;
            value |= ((unsigned int)(*arr  )     ) >>  2;
            break;
        case  2:   /* bit offset == 2 */
            value  = ((unsigned int)(*arr++)&0x3f) <<  8;
            value |= ((unsigned int)(*arr  )     ) >>  0;
            break;
        case  4:   /* bit offset == 4 */
            value  = ((unsigned int)(*arr++)&0x0f) << 10;
            value |= ((unsigned int)(*arr++)     ) <<  2;
            value |= ((unsigned int)(*arr  )     ) >>  6;
            break;
        case  6:   /* bit offset == 6 */
            value  = ((unsigned int)(*arr++)&0x03) << 12;
            value |= ((unsigned int)(*arr++)     ) <<  4;
            value |= ((unsigned int)(*arr  )     ) >>  4;
            break;
    }
    return value;
}

【讨论】:

  • 在我的回答中添加了 Gene、我的和你的基准。你的代码是最快的。
【解决方案5】:

存储问题的基础

您面临的最大问题是“我的存储基础是什么?”您所面临的最大问题是基本问题,您知道基础知识,您可以使用的是char,@ 987654322@,int,等等……最小的是8-bits。不管你如何分割你的存储方案,它最终都必须基于这种每字节 8 位布局的内存单元中的内存。

唯一最佳的、不浪费位的内存分配是在 14 位的最小公倍数中声明一个 char 数组。在这种情况下,它是完整的 112-bits7-shorts14-chars)。这可能是最好的选择。在这里,声明一个包含 7 个短字符或 14 个字符的数组,将允许精确存储 8 个14-bit 值。当然,如果您不需要其中的 8 个,那么它无论如何也没有多大用处,因为它会浪费比单个无符号值丢失的 4 位更多。

如果您想进一步探索,请告诉我。如果是,我很乐意帮助实施。

位域结构

关于位域打包位打包的cmets正是你需要做的。这可以涉及单独的结构或与联合的组合,或者根据需要直接手动右/左移动值。

适用于您的情况的简短示例(如果我理解正确,您希望内存中有 2 个 14 位区域)是:

#include <stdio.h>

typedef struct bitarr14 {
    unsigned n1 : 14,
             n2 : 14;
} bitarr14;

char *binstr (unsigned long n, size_t sz);

int main (void) {

    bitarr14 mybitfield;

    mybitfield.n1 = 1;
    mybitfield.n2 = 1;

    printf ("\n mybitfield in memory : %s\n\n", 
            binstr (*(unsigned *)&mybitfield, 28));

    return 0;
}

char *binstr (unsigned long n, size_t sz)
{
    static char s[64 + 1] = {0};
    char *p = s + 64;
    register size_t i = 0;

    for (i = 0; i < sz; i++) {
        p--;
        *p = (n >> i & 1) ? '1' : '0';
    }

    return p;
}

输出

$ ./bin/bitfield14

 mybitfield in memory : 0000000000000100000000000001

注意:为了在内存中打印值而取消引用mybitfield打破了严格的别名,它只是为了输出示例的目的而故意的。

以所提供的方式使用结构的优点和目的在于它允许直接访问结构的每个 14 位部分,而无需手动移位等。

【讨论】:

  • 我可能没有明确我的要求:设置/获取数组中任意 14 位的能力。不幸的是,这个答案不能满足这个需求,因为如果我要生成它们的数组,仍然会有浪费位(32-28=4)。如果我将尽可能多的这些数据塞入 64 字节,我不想浪费 64 位(这是 4 个可能的 14 位值)。是的,我确实想在我心目中的项目中尽可能多地把它们塞进 64 个字节。
  • 当有人提供帮助时,如果你想要更多,那么你不想做的第一件事就是咬正在喂你的手。你不清楚,让我们从那里开始,最简单的方法来完成你想要的,没有任何浪费,然后是 2 个简短的函数来直接设置和检索位。如果你能学会多一点外交,我会举个例子。
  • 如果我听起来不文明,我深表歉意。我试图澄清原始问题(我已将其编辑到问题中),同时详细说明您的原始答案为何不适合该问题。不幸的是,文字是一种糟糕的传达语气的媒介:(我非常感谢您的帮助,真诚地。
  • 没关系,我很确定我明白你的意思,可能措辞更好一些。您提到了short,但您似乎真的想避免在每个短片中浪费2 位,这会使事情变得更加复杂。给我一点,我会修改答案。
猜你喜欢
  • 2017-03-02
  • 1970-01-01
  • 2012-05-20
  • 2015-04-08
  • 1970-01-01
  • 2020-01-03
  • 2021-10-31
  • 1970-01-01
  • 2015-04-14
相关资源
最近更新 更多