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


这是关于ANSI-C(C90)。这就是我所知道的:

  • 我可以直接告诉编译器我想要一个特定变量的位数。
  • 如果我想要1位,其值可以为零或一。
  • 或者值为0,1,2,3的2位,依此类推......;

我熟悉语法。

我有关于位域的问题:

  • 我想定义一个SET结构。
  • 它可以有最多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.

我知道这不高效;也许它甚至对我想要的东西都不好。这就是为什么我在寻求帮助。


11717
2018-06-28 06:40


起源

这可能会为每个算法位使用32位存储空间,这与您追求的空间效率相反。至少,每个算法位将使用8位存储(除非你有一个奇怪的机器,在这种情况下它可能使用而不是更少)。您需要考虑位掩码和适当大小的某些子类或其他子类的无符号整数数组(unsigned long long 要么 uint64_t 或类似的)。位字段偶尔会有其用途。这绝对不是其中之一。 - Jonathan Leffler
如果您的关注只是内存效率,我认为您最好编写自己的函数来获取/设置数组的1位元素,例如在一个char数组元素中一次打包8个元素。可能使用64位ibt会更好地缓存 - Pynchia
@kasperd是的,你明白了我的意思。编译器为八个数组元素分配了多少字节?如果不止一个,我的建议就成立了 - Pynchia
@kasperd因为1.位不能用C寻址,最小的可寻址存储单元是一个字节,而2.数组条目需要单独对齐。 - The Paramagnetic Croissant
@kasperd:不,位字段不会奇怪地使位在硬件级别可寻址。位字段通过使编译器生成逐位屏蔽操作来模拟位寻址访问来解决位不可寻址的问题。无法实现位字段数组。尝试使用位字段来节省空间是没有意义的。没有任何。它实际上花费了内存空间用于存储,并且在代码访问位的内存中花费更多空间。不是一个很好的权衡。 - Jonathan Leffler


答案:


正如在广泛的评论中所指出的那样,使用位字段不是可行的方法。对于包含值1..1024的集合,您只能使用128字节的存储空间。您需要将值N映射到位N-1(因此您可以使用位0..1023)。您还需要确定所需的操作。此代码支持'create','destroy','insert','delete'和'in_set'。它不支持对集合中元素的迭代;如果你想要它可以添加。

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 宏是基于 INT64_C 中定义的宏(和其他相关的宏) <stdint.h> 在C99及更高版本中,这也不是C90的一部分。


6
2018-06-28 08:38



这可以创建一个函数来找到这个方法的两个集合的交集吗? - 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
显然,set union和set difference与set intersection非常相似。 - Jonathan Leffler


根据我之前的评论,这里有一个例子,说明如何将8个1位元素打包成一个char物理元素。 我只实现了获取1位元素值的函数,我将函数设置为你(这很容易)。

注意:您可以轻松更改数组元素的类型(unsigned char)并尝试可以容纳更多位的类型(例如unsigned int),并测试它们在速度方面的表现是否更好。 您还可以修改代码以使其处理大于一位的元素。

#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

2
2018-06-28 07:56





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/  详细


1
2018-06-28 07:30



我写作的时候有+4条评论 - EpiGen
在我写的内容中写下什么错误。这就是使“downvote”非常有用的原因 - EpiGen
这不是我的投票,尽管我受到了极大的诱惑。你仍在使用一个位字段,虽然它可以保存0到1023之间的值(所以一个问题是你声称它将保持1024,但它不会)。麻烦的是,创建一组最多1024个这样的值将需要2048字节的内存。与128字节相比,这是浪费,这是可以使用的最小值。 - Jonathan Leffler
它根本没有任何意义。管理具有1024个元素的集合的邮政编码。 - Karoly Horvath
@Jonathan Leffler真的吗? “正如Jonathan Leffler评论的那样使用数组......”但不是SET-s数组。所以第一个代码没有连接到第二个。 - EpiGen


要存储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的值数组的最节省空间的方法。


1
2018-06-28 08:31



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


如果要存储“布尔数组”或设置标志,它可能很有用。例如,您可以一次初始化或比较多达64个值。

这些宏适用于unsigned char,short,int,long long ...但是如果你只选择一个类型会显着简化(所以你可以使用更安全的静态内联函数)

#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

如果您只访问一种类型的数组,最好将宏转换为适当的函数,如:

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

你也可以通过'|'标记一起比较多个标志并使用'&'ed mask;但是,当你超过本机类型时,它会变得更复杂一些

对于您的特定实例,您可以通过以下方式初始化为全零:

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");

1
2018-06-28 09:04



你不能使用这些宏 unsigned long long;该 1 是一个 int 如果,有时会有点转移到遗忘 sizeof(unsigned long long) > sizeof(int)。 - Jonathan Leffler
如果是独立的字节顺序,我会选择你的答案 - EpiGen
@EpiGen:为什么字节序在一组中很重要? - Jonathan Leffler
@Jonathan Leffler只要你使用bithifting操作错误的字节顺序就会导致数据丢失,错误。所以无论你使用什么,它总是很重要。 - EpiGen
@JonathanLeffler我删除了一个 (typeof(x)) 由于缺乏可移植的方式而在1上强制转换......我不确定编译器处理a的程度如何 1ULL 在那里,所以现在,我将离开很长一段时间。 - technosaurus


1)这个问题的正确解决方案是使用比特阵列

这个问题提供了解决方案 具有Struct的位字段。有两种典型的方法可以为位相关问题节省内存空间,另一种方法是使用 位数组。对于问题中的这种特定情况,更好的方法是使用Bit Array(演示如下)。

  • 如果在这种情况下就像纯粹独立的位标志一样,那就去吧 为了 位数组 
  • 如果存在一组相关位,例如IP地址或控制字定义,那么最好将它们与结构组合,即使用 带有Sturct的位字段

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)此外,这个问题值得讨论的一点是,

如何选择合适的解决方案 “比特阵列” 和 “带字段的位字段”

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


1
2018-06-28 08:16



不要硬编码 32... - Karoly Horvath
你有一个256个元素的数组(假设 sizeof(int) == 4);您初始化该数组的前1024个元素。你在那里遇到问题。此外,您已分配足够的空间来存储多达2048位。 - Jonathan Leffler
应该可以说是8 CHAR_BIT 从 <limits.h>。 OTOH,没有那么多系统所在的价值 CHAR_BIT 不是8(但有一些,特别是在DSP - 数字信号处理器 - 世界)。 - Jonathan Leffler
感谢您的提醒。我认为这里的关键点是如何在“位数组”和“带结构的位字段”之间选择合适的解决方案?我的示例代码只是提供一个参考来解释“位数组”本身。我为它的缺陷道歉。 - Eric Tsui
不要道歉;我们都犯错误,有时也会公开。修复它。请! - Jonathan Leffler