如何使用位域来节省内存?

11

这是关于ANSI-C(C90)的内容。以下是我所了解到的:

  • 我可以直接告诉编译器我想要特定变量的位数。
  • 如果我想要一个只有0或1值的1位;
  • 或者2位,它们的值为0、1、2、3等等……

我熟悉语法。

我遇到了有关位域的问题:

  • 我想定义一个SET结构。
  • 它最多可以有1024个元素(可以少于1024个,但最多只能有1024个元素)。
  • 集合的范围是从1到1024。因此,元素的任何值都可以是1-1024。

我试图创建一个用于SET的结构,并且必须尽可能地高效利用内存部分。

我尝试过:

typedef struct set
{
    unsigned int var: 1;
} SET;
//now define an array of SETS
SET array_of_sets[MAX_SIZE]  //didn't define MAX_SIZE, but no more than 1024 elements in each set.

我知道这不太高效;甚至可能不适合我想要的。这就是为什么我正在寻求帮助。


7
每个算法位可能会使用32位存储,这与您追求的空间效率背道而驰。最少每个算法位将使用8位存储(除非您有一个奇怪的机器,在这种情况下它很可能会使用更多而不是更少)。您需要考虑位掩码和适当大小的无符号整数数组,例如unsigned long longuint64_t之类的类型。位字段偶尔有其用处,但绝对不是这种情况。 - Jonathan Leffler
2
如果您关心的仅是内存效率,我认为最好编写自己的函数来获取/设置数组中的1位元素,例如将8个元素一次打包到一个char数组元素中。在缓存方面,可能使用64位比特会更好。 - Pynchia
3
@kasperd,是的,你理解我的意思了。编译器会为八个数组元素分配多少字节?如果分配的字节数大于1,则我的建议仍然成立。 - Pynchia
2
@kasperd 因为1.在C语言中无法寻址位,最小可寻址存储单元是字节,2.数组条目需要单独对齐。 - The Paramagnetic Croissant
3
不,位域并不能在硬件级别神奇地使位可寻址。位域通过让编译器生成按位掩码操作来模拟位寻址访问,以解决位不可寻址的问题。无法实现位域数组。尝试像这样使用位域来节省空间是没有意义的。实际上,它会占用更多的存储器空间,并且代码访问位时还需要更多的存储器空间,这是一个很糟糕的权衡。 - Jonathan Leffler
显示剩余13条评论
6个回答

6

如大量注释所述,使用位域不是正确的方式。您只需要使用128字节的存储空间来存储包含值1..1024的集合。您需要将值N映射到位N-1(这样您就可以使用位0..1023)。您还需要决定集合所需的操作。这段代码支持“创建”、“销毁”、“插入”、“删除”和“在集合中”。它不支持遍历集合中的元素;如果需要,可以添加。

sets.h

#ifndef SETS_H_INCLUDED
#define SETS_H_INCLUDED

typedef struct Set Set;
enum { MAX_ELEMENTS = 1024 };

extern Set *create(void);
extern void destroy(Set *set);
extern void insert(Set *set, int value);
extern void delete(Set *set, int value);
extern int in_set(Set *set, int value);

#endif /* SETS_H_INCLUDED */

sets.c

#include "sets.h"
#include <assert.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>

typedef unsigned long Bits;
#define BITS_C(n)  ((Bits)(n))
enum { ARRAY_SIZE = MAX_ELEMENTS / (sizeof(Bits) * CHAR_BIT) };

struct Set
{
     Bits set[ARRAY_SIZE];
};

Set *create(void)
{
    Set *set = malloc(sizeof(*set));
    if (set != 0)
        memset(set, 0, sizeof(*set));
    return set;
}

void destroy(Set *set)
{
    free(set);
}

void insert(Set *set, int value)
{
    assert(value >= 1 && value <= MAX_ELEMENTS);
    value--;  /* 0..1023 */
    int index = value / (sizeof(Bits) * CHAR_BIT);
    int bitnum = value % (sizeof(Bits) * CHAR_BIT);
    Bits mask = BITS_C(1) << bitnum;
    /* printf("I: %d (%d:%d:0x%.2lX)\n", value+1, index, bitnum, mask); */
    set->set[index] |= mask;
}

void delete(Set *set, int value)
{
    assert(value >= 1 && value <= MAX_ELEMENTS);
    value--;  /* 0..1023 */
    int index = value / (sizeof(Bits) * CHAR_BIT);
    int bitnum = value % (sizeof(Bits) * CHAR_BIT);
    Bits mask = BITS_C(1) << bitnum;
    /* printf("D: %d (%d:%d:0x%.2lX)\n", value+1, index, bitnum, mask); */
    set->set[index] &= ~mask;
}

/* C90 does not support <stdbool.h> */
int in_set(Set *set, int value)
{
    assert(value >= 1 && value <= MAX_ELEMENTS);
    value--;  /* 0..1023 */
    int index = value / (sizeof(Bits) * CHAR_BIT);
    int bitnum = value % (sizeof(Bits) * CHAR_BIT);
    Bits mask = BITS_C(1) << bitnum;
    /* printf("T: %d (%d:%d:0x%.2lX) = %d\n", value+1, index, bitnum, mask,
              (set->set[index] & mask) != 0); */
    return (set->set[index] & mask) != 0;
}

#include <stdio.h>

enum { NUMBERS_PER_LINE = 15 };

int main(void)
{
    Set *set = create();
    if (set != 0)
    {
        int i;
        int n = 0;
        for (i = 1; i <= MAX_ELEMENTS; i += 4)
             insert(set, i);
        for (i = 3; i <= MAX_ELEMENTS; i += 6)
             delete(set, i);

        for (i = 1; i <= MAX_ELEMENTS; i++)
        {
             if (in_set(set, i))
             {
                 printf(" %4d", i);
                 if (++n % NUMBERS_PER_LINE == 0)
                 {
                     putchar('\n');
                     n = 0;
                 }
             }
        }
        if (n % NUMBERS_PER_LINE != 0)
            putchar('\n');
        destroy(set);
    }
    return 0;
}

需要给函数加上系统前缀,例如set_。宏BITS_C是基于<stdint.h>中C99及以后版本中定义的INT64_C宏(和其他相关宏)而来,这些宏在C90中也不包括。


能否使用这种方法创建一个函数来找到两个集合的交集? - idan di
可以 - 轻松地。void set_intersection(Set *set1, Set *set2, Set *result) { int i; for (i = 0; i < ARRAY_SIZE; i++) result->set[i] = set1->set[i] & set2->set[i]; } - Jonathan Leffler
显然,集合的并集和差集与集合的交集非常相似。 - Jonathan Leffler

2
根据我之前的评论,这里是一个示例,展示如何将8个1比特元素打包到一个字符物理元素中。我只实现了获取1比特元素值的函数,设置函数留给你(这很容易做到)。
注意:你可以轻松更改数组元素类型(无符号字符)并尝试使用可容纳更多比特的类型(例如无符号整数),以测试它们在速度方面表现是否更好。你还可以修改代码使其处理大于1比特的元素。
#include <stdio.h>
#include <limits.h>

unsigned int get_el(unsigned char* array, unsigned int index)
{
    unsigned int bits_per_arr_el = sizeof(unsigned char)*CHAR_BIT;
    unsigned int arr_index = index / bits_per_arr_el;
    unsigned int bit_offset = index % bits_per_arr_el;
    unsigned int bitmask = 1 << bit_offset;
    unsigned int retval;

    // printf("index=%u\n", index);
    // printf("bits_per_arr_el=%u\n", bits_per_arr_el);
    // printf("arr_index=%u\n", arr_index);
    // printf("bit_offset=%u\n", bit_offset);

    retval = array[arr_index] & bitmask ? 1 : 0; // can be simpler if only True/False is needed
    return(retval);
}

#define MAX_SIZE 10
unsigned char bitarray[MAX_SIZE];

int main()
{
    bitarray[1] = 3; // 00000011
    printf("array[7]=%u, array[8]=%u, array[9]=%u, array[10]=%u\n",
            get_el(bitarray, 7),
            get_el(bitarray, 8),
            get_el(bitarray, 9),
            get_el(bitarray,10));

    return 0;
}

输出

array[7]=0, array[8]=1, array[9]=1, array[10]=0

1
typedef struct set
{
    unsigned short var:10; // uint var:1 will be padded to 32 bits
} SET;                     // ushort var:10 (which is max<=1024) padded to 16 bits

如@Jonathan Leffler所评论,使用数组(unsigned short[])并定义位掩码。
#define bitZer 0x00  //(unsigned)(0 == 0)? true:true;
#define bitOne 0x10  // so from (both inclusive)0-1023 = 1024
...                  // added for clarification  
#define bitTen 0x0A

查看每个元素的位。详见http://www.catb.org/esr/structure-packing/


我写作时有+4条评论。 - EpiGen
请指出我写的有什么问题。这就是“踩”按钮非常有用的原因。 - EpiGen
@Jonathan Leffler 真的吗?“正如Jonathan Leffler所评论的,使用数组...”但不是SET-s的数组。因此,第一段代码与第二段代码没有关联。 - EpiGen
@EpiGen 这有赞有损。有时你会遇到那些有诚信的人,他们会用评论来支持一个负评,以帮助每个人学习。其他时候,你会遇到一群“刚入门”的孩子们,他们喜欢通过给一个答案打负评来感觉重要或优越。他们很少关心 SO 实际上提供的学习,所以如果是这种情况,你不应该指望得到任何有意义的帮助... - David C. Rankin
@Karoly Horvath 阅读原帖。问题是在将类型定义为[指定位宽]的数组声明时尽可能节省内存,但不要在该结构中保留1-1024个单独元素。 - EpiGen
显示剩余7条评论

1

1) 这道题的正确解决方案是使用位数组。

该题提供了使用结构体中的位域作为解决方案。在处理位相关问题时,有两种典型的节省内存空间的方法,另一种是使用位数组。对于这个具体的问题,更好的方法是使用位数组(如下所示的演示)。

  • 如果仅涉及独立的位标志,则选择位数组
  • 如果有一组相关位,例如IP地址或控制字定义,则最好将它们与结构体结合起来,即使用结构体中的位域

2) 以下是演示位数组的示例代码。

#include<limits.h>
#define BITS_OF_INT (sizeof(int)*CHAR_BIT)  
void SetBit(int A[], int k)
     {
       //Set the bit at the k-th position
       A[k/BITS_OF_INT] |= 1 <<(k%BITS_OF_INT);
     } 
void ClearBit(int A[], int k)
     {
       //RESET the bit at the k-th position
       A[k/BITS_OF_INT] &= ~(1 <<(k%BITS_OF_INT)) ;
     }  
int TestBit(int A[], int k)
     {
       // Return TRUE if bit set    
       return ((A[k/BITS_OF_INT] & (1 <<(k%BITS_OF_INT)))!= 0) ;
     }

#define MAX_SIZE 1024
int main()
{
    int A[MAX_SIZE/BITS_OF_INT];
    int i;
    int pos = 100; // position

    for (i = 0; i < MAX_SIZE/BITS_OF_INT; i++)
        A[i] = 0; 

    SetBit(A, pos);
    if (TestBit(A, pos)){//do something}
    ClearBit(A, pos); 
}

3) 此外,这个问题值得讨论的一个重点是:

如何在 "Bit Array""Bit fields with struct" 之间选择适当的解决方案?

以下是有关此主题的一些参考资料。


你有一个包含256个元素的数组(假设 sizeof(int) == 4);你初始化了该数组的前1024个元素。我认为你在这里有一个问题。此外,你已经分配了足够的空间来存储多达2048位。 - Jonathan Leffler
8 应该被认为是来自于 <limits.h>CHAR_BIT。另一方面,没有太多的系统其 CHAR_BIT 的值不是 8(但在 DSP - 数字信号处理器 - 领域中有一些)。 - Jonathan Leffler
谢谢提醒。我认为这里的关键点是如何在“位数组”和“带有结构体的位域”之间选择适当的解决方案?我的示例代码只是为了提供一个参考来解释“位数组”本身。对于它的缺陷,我深表歉意。 - Eric Tsui
我已经修复了代码,它在数组上越界。 - Jonathan Leffler
@JonathanLeffler 感谢您的帮助。根据您的经验,您能否给出一些更多关于何时使用 Bit Array 和何时使用 Bit fields struct 的建议? - Eric Tsui
显示剩余4条评论

1

要存储从0到1023(或从1到1024,本质上是相同的,只涉及加/减1)的值,您需要至少10位。

这意味着对于32位(无符号)整数,您可以将3个值打包到30位中,这给出了2个无用填充位。

示例:

%define ELEMENTS 100

uint32_t myArray[ (ELEMENTS + 2) / 3 ];

void setValue(int n, int value) {
    uint32_t temp;
    uint32_t mask = (1 << 10) - 1;

    if(n >= ELEMENTS) return;
    value--;                        // Convert "1 to 1024" into "0 to 1023"
    temp = myArray[n / 3];
    mask = mask << (n % 3)*10;
    temp = (temp & ~mask) | (value << (n % 3)*10);
    myArray[n / 3] = temp; 
}

int getValue(int n) {
    uint32_t temp;
    uint32_t mask = (1 << 10) - 1;

    if(n >= ELEMENTS) return 0;
    temp = myArray[n / 3];
    temp >>= (n % 3)*10;
    return (temp & ~mask) + 1;
}

你可以使用位域来实现这个功能,但是获取/设置单个值的代码将会使用分支语句(例如 switch( n%3 )),在实践中会更慢。
去掉这2位填充会增加一些复杂性和开销。例如:
%define ELEMENTS 100

uint32_t myArray[ (ELEMENTS*10 + 31) / 32 ];

int getValue(int n) {
    uint64_t temp;
    uint64_t mask = (1 << 10) - 1;

    if(n >= ELEMENTS) return 0;

    temp = myArray[n*10/32 + 1];
    temp = (temp << 32) | myArray[n*10/32];

    temp >>= (n*10 % 32);

    return (temp & ~mask) + 1;
}

这不能用位域来实现。这是存储值范围从1到1024的数组最节省空间的方式。

也许在5个元素之后,这些无用的2位二进制对中的五个可以被填充。 - huseyin tugrul buyukisik
一个包含从1到1024的值的集合最多可以存储1024个不同的值。你没有定义数组来容纳那么多的值。你还要求调用者为数字建立存储位置,并且不确保集合中的值是唯一的,而数学集合根据定义不包含任何重复项。 - Jonathan Leffler
@JonathanLeffler:啊——我想你是对的。我一直在做“元素数组,其值为1到1024”,而不是数学意义上的集合(这只是一种可怕的复杂方式,要求一个1024位的数组)。 - Brendan

1
如果您正在存储“布尔数组”或设置标志,这可能很有用。例如,您可以一次初始化或比较最多64个值。
这些宏适用于无符号字符、短整型、整型、长长整型等类型,但如果您只选择一种类型(因此您可以使用更安全的静态内联函数),则会显着简化。
#define getbit(x,n) x[n/(sizeof(*x)*8)]  &  (typeof(*x))1 << (n&((sizeof(*x)*8)-1)) 
#define setbit(x,n) x[n/(sizeof(*x)*8)] |=  (typeof(*x))1 << (n&((sizeof(*x)*8)-1)) 
#define flpbit(x,n) x[n/(sizeof(*x)*8)] ^=  (typeof(*x))1 << (n&((sizeof(*x)*8)-1)) 
#define clrbit(x,n) x[n/(sizeof(*x)*8)] &= ~( (typeof(*x))1 << (n&((sizeof(*x)*8)-1)) ) 

要初始化一个大的布尔数组,你只需要这样做:char cbits[]={0,0xF,0,0xFF};

或者对于全零:char cbits[4]={0};

另外还有一个int的例子:int ibits[]={0xF0F0F0F0,~0};

//1111000011110000111100001111000011111111111111111111111111111111

如果你只会访问1种类型的数组,那么最好将宏定义变成适当的函数,例如:

static inline unsigned char getbit(unsigned char *x, unsigned n){ 
  return x[n>>3]  &  1 << (n&7); 
}
//etc... similar for other types and functions from macros above

你也可以通过使用“|”将多个标志组合在一起并使用“&”掩码进行比较;但是,当超出本机类型时,它会变得更加复杂。
对于你的特定情况,你可以通过以下方式初始化为所有零:
unsigned char flags[128]={0};

将所有数字变为1,方法如下:
uint64_t flags[128] = {~0,~0,~0,~0,~0,~0,~0,~0,~0,~0,~0,~0,~0,~0,~0,~0};

你甚至可以使用枚举来为你的标志命名。
enum{
  WHITE, //0
  RED, //1
  BLUE, //2
  GREEN, //3
  ...
  BLACK //1023
}

if (getbit(flags,WHITE) && getbit(flags,RED) && getbit(flags,BLUE))
  printf("red, white and blue\n");

你不能在 unsigned long long 中使用那些宏;1 是一个 int,如果 sizeof(unsigned long long) > sizeof(int),有时会被位移至遗忘。 - Jonathan Leffler
如果您的答案与字节序无关,我会选择它。 - EpiGen
只要你在使用位移运算时出现错误的字节序,都会导致数据丢失和错误。因此,无论在什么情况下使用位移运算都非常重要。@Jonathan Leffler - EpiGen
@EpiGen:我不同意那个评估。我认为你是错的。 - Jonathan Leffler
@EpiGen 我替换了大部分的移位操作(大多数编译器会将它们优化回来),以便无论字节顺序如何,唯一的位移应该是一致的。 - technosaurus
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接