【问题标题】:How can I use Bit-Fields to save memory?如何使用位域来节省内存?
【发布时间】:2015-06-28 06:40:23
【问题描述】:

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

  • 我可以直接告诉编译器我想要一个特定变量多少位。
  • 如果我想要 1 位,它的值可以为零或一。
  • 或 2 位表示值 0、1、2、3 等...;

我对语法很熟悉。

我有关于位域的问题:

  • 我想定义一个 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.

我知道这没有效率;也许它甚至不适合我想要的。这就是我寻求帮助的原因。

【问题讨论】:

  • 每个算法位可能会使用 32 位存储空间,这与您所追求的空间效率相反。至少,每个算法位将使用 8 位存储(除非您有一个古怪的机器,在这种情况下它可能会使用而不是更少)。您需要考虑位掩码和适当大小的某些子物种或其他(unsigned long longuint64_t 等)的无符号整数数组。位域有时也有其用途。这绝对不是其中之一。
  • 如果您只关心内存效率,我认为您最好编写自己的函数来获取/设置数组的 1 位元素,例如一次打包 8 个元素在一个 char 数组元素中。可能使用 64 位 ibt 在缓存方面会更好
  • @kasperd 是的,你明白我的意思。编译器为八个数组元素分配了多少字节?如果不止一个,那么我的建议成立
  • @kasperd 因为 1. 位不能在 C 中寻址,最小的可寻址存储单元是一个字节,以及 2. 数组条目需要单独对齐。
  • @kasperd:不,位域不会神奇地使位在硬件级别上可寻址。位域通过让编译器生成按位掩码操作来模拟位寻址访问来解决位不可寻址的事实。没有办法实现位域数组。像这样尝试使用位域来节省空间是没有意义的。没有任何。它实际上会占用内存中的存储空间,而代码访问这些位会占用更多的内存空间。不是一个好的权衡。

标签: c bit-fields


【解决方案1】:

正如在广泛的 cmets 中所指出的,使用位字段不是要走的路。对于包含值 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 宏基于 C99 及更高版本的 &lt;stdint.h&gt; 中定义的 INT64_C 宏(和其他相关宏),它也不是 C90 的一部分。

【讨论】:

  • 是否可以用这种方法创建一个函数来查找两个集合的交集?
  • 是的 - 很容易。 void set_intersection(Set *set1, Set *set2, Set *result) { int i; for (i = 0; i &lt; ARRAY_SIZE; i++) result-&gt;set[i] = set1-&gt;set[i] &amp; set2-&gt;set[i]; }.
  • 显然,集合并集和集合差与集合交集非常相似。
【解决方案2】:

根据我之前的 cmets,这里有一个示例,说明如何将 8 个 1 位元素打包到一个 char 物理元素中。 我只实现了获取 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

【讨论】:

    【解决方案3】:
    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 所评论的那样,使用数组(无符号短[]) 并定义位掩码

    #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 cmets 而我正在写作
    • 请写出我写的错误。这就是让“downvote”非常有用的原因
    • 这不是我的反对票,尽管我非常想。您仍在使用一个位域,尽管它可以保存 0 到 1023 之间的值(所以一个问题是您声称它将保存 1024,但它不会)。问题是,要创建一组最多 1024 个这样的值将需要 2048 字节的内存。与可以使用的最小值 128 字节相比,这是一种浪费。
    • 这根本没有任何意义。管理包含 1024 个元素的集合的邮政编码。
    • @Jonathan Leffler 真的吗? “正如 Jonathan Leffler 所评论的那样,使用数组......”但不是 SET-s 数组。所以第一个代码没有连接到第二个。
    【解决方案4】:

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

    【讨论】:

    • 也许这些无用的 2 位对中的 5 个可以在 5 个元素之后填充一次。
    • 包含从 1 到 1024 的值的集合最多可以存储 1024 个不同的值。您尚未确定数组的尺寸以容纳那么多值。您还要求调用者为数字建立存储位置,并且不要确保集合中的值是唯一的,而根据定义,数学集合不包含任何重复项。
    • @JonathanLeffler:啊——我认为你是对的。我一直在做“值为 1 到 1024 的元素数组”,而不是数学意义上的集合(这只是要求 1024 位数组的一种非常复杂的方式)。
    【解决方案5】:

    如果您要存储“布尔数组”或设置标志,它会很有用。例如,您一次最多可以初始化或比较 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};

    //111100001111000011110000111100001111111111111111111111111111111

    如果您只访问一种类型的数组,最好将宏变成适当的函数,例如:

    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 掩码;但是,当您超出本机类型时,它确实会变得更加复杂

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

    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) &gt; sizeof(int) 有时会被移位到遗忘。
    • 如果它与字节序无关,我会选择你的答案
    • @EpiGen:为什么字节序在集合中很重要?
    • @Jonathan Leffler 只要您使用移位操作,错误的字节序会导致数据丢失、错误。因此,无论您使用什么,它总是很重要。
    • @JonathanLeffler 由于缺乏可移植方式,我在 1 上删除了 (typeof(x)) 演员表...我不确定编译器在那里处理 1ULL 的效果如何,所以现在,我会离开长长的。
    【解决方案6】:

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

    该问题通过带有结构的位字段提供了解决方案。对于位相关的问题,有两种典型的节省内存空间的方法,另一种是使用Bit Array。对于问题中的这种特定情况,更好的方法是使用Bit Array(演示如下)。

    • 如果是这里的纯独立位标志这样的情况,去 对于Bit Array
    • 如果有一组相关位,例如IP地址或控制字定义,那么最好将它们与一个结构体结合起来,即使用Bit Fields with 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) 此外,这个问题值得讨论的一点是,

    如何在“Bit Array”“Bit fields with struct”之间选择合适的解决方案

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

    【讨论】:

    • 你有一个包含 256 个元素的数组(假设 sizeof(int) == 4);您初始化该数组的前 1024 个元素。我觉得你那里有问题。此外,您已经分配了足够的空间来存储多达 2048 位。
    • 8 应该是 CHAR_BIT 来自 &lt;limits.h&gt;。 OTOH,CHAR_BIT 的值不是 8 的系统并不多(但有一些系统,尤其是在 DSP(数字信号处理器)世界中)。
    • 感谢您的提醒。我认为这里的关键是如何在“Bit Array”和“Bit fields with struct”之间选择合适的解决方案?我的示例代码只是为解释“位数组”本身提供参考。我为它的缺陷道歉。
    • 不要道歉;我们都会犯错误,有时会在公共场合犯错。改为修复它。请!
    • 更新答案。感谢所有帮助我完善的建议。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-04
    • 2011-04-02
    • 2016-07-19
    • 1970-01-01
    • 2010-10-19
    相关资源
    最近更新 更多