位压缩结构

Bit compressed structure

本文关键字:结构 压缩      更新时间:2023-10-16

我目前正在进行一个项目,在这个项目中,我需要在向量中存储大量(~数十亿单位)的结构。我还需要以线性方式迭代该向量,所以我必须遍历的数据越少越好。

因此,我自然而然地开始优化单个结构的大小。例如,如果我有多个bool值,我可以将true/false值存储在一个位中,并将所有bool值压缩为一个char/16位,无论大小是否足够。对于某些条目,我只需要20位无符号整数。因此,我可以再次压缩这些值。

然后我得到了这样的东西(注意这只是一个简化的例子):

class Foo {
private:
    uint32_t m_time;
    uint32_t m_comb;
public:
    Foo(uint32_t t, uint32_t big, uint16_t small, bool is_blue, bool is_nice)
        : m_time(t), m_comb((big << 12) | (small << 2) | (is_blue << 1) | is_nice)
    { }
    uint32_t get_time()  const { return m_time; }
    uint32_t get_big()   const { return m_comb >> 12; }
    uint16_t get_small() const { return m_comb & 0b11111111100; }
    uint16_t is_blue()   const { return m_comb & 0b10; }
    uint16_t is_nice()   const { return m_comb & 0b1; }
};

问题是,这是否可以通过模板实现自动化?我的想法是,我将插入条目的顺序、所需的比特大小,然后我将能够调用get<i>(),它将返回结构的第I个条目。我的动机是放弃手动编写代码,因为在我看来,挠痒痒的地方很容易出错。我试着自己实现这一点,但毫无希望地失败了。

使用位字段可以非常容易地实现这一点。

class Foo {
private:
    uint32_t m_time;
    uint32_t m_big : 20;
    uint32_t m_small : 10;
    uint32_t m_isblue : 1;
    uint32_t m_isnice : 1;
public:
    Foo(uint32_t t, uint32_t big, uint16_t small, bool is_blue, bool is_nice)
        : m_time(t), m_big(big), m_small(small), m_isblue(is_blue), m_isnice(is_nice)
    { }
    uint32_t get_time()  const { return m_time; }
    uint32_t get_big()   const { return m_big; }
    uint16_t get_small() const { return m_small; }
    uint16_t is_blue()   const { return m_isblue; }
    uint16_t is_nice()   const { return m_isnice; }
};

在线演示显示尺寸。

编辑:其他信息

总之,编译器将位字段打包在一起的方式取决于实现:

9.6/1(…)类对象中位字段的分配由实现定义。位字段的对齐实现定义。位字段被打包到一些可寻址的分配单位。[注意:有些位字段跨越分配单元机器,而不是其他机器。位字段从右到左分配一些机器,从左到右在其他机器上--尾注]

所以你不能保证,但编译器通常会尽最大努力将它们组合在一起。根据经验,只要你的比特字段是连续的,并且它们的总比特数小于你使用的基本类型,那么打包就很有可能是最佳的。如果需要,你可以微调基本类型,如这个在线演示所示。

由于位字段是implementation-defined,我决定编写位读/写函数。当您想在磁盘上存储数据(缓存或其他东西)时,它会派上用场。

这是源代码模拟链接。

#include <iostream>
bool GetBit(uint32_t num, uint8_t pos)
{
    //check pos < sizeof(num)*8
    return (num >> pos) & 0x1;
}
uint8_t GetByte(uint32_t num, uint8_t pos)
{
    //check pos < sizeof(num)*8
    return (num >> pos) & 0xFF;    
}
uint16_t GetShort(uint32_t num, uint8_t pos)
{
    //check pos < sizeof(num)*8
    return (num >> pos) & 0xFFFF;    
}
uint32_t SetBit(bool val, uint32_t &dest, uint8_t pos)
{
    dest ^= (-val ^ dest) & (1 << pos);
    return dest;
}
uint32_t SetByte(uint8_t val, uint32_t &dest, uint8_t pos)
{
    dest &= ~(0xFF<<pos); //clean
    dest |= val<<pos; //set
    return dest;
}
uint32_t SetShort(uint16_t val, uint32_t &dest, uint8_t pos)
{
    dest &= ~(0xFFFF<<pos); //clean
    dest |= val<<pos; //set
    return dest;
}
void PrintBin(uint32_t s, const char* pszComment)
{
    std::cout << pszComment << ": " << std::endl;
    for (size_t n = 0; n < sizeof(s) * 8; n++)
        std::cout << GetBit(s, n) << ' ';
    std::cout << std::endl;
}
int main()
{
    uint32_t s = 4294967295; //all bits
    PrintBin(s, "Start");
    SetBit(false, s, 2); 
    PrintBin(s, "Set bit 2 to FALSE");
    SetByte(0, s, 22);
    PrintBin(s, "Set byte 22 to val 0");
    SetByte(30, s, 22);
    PrintBin(s, "Set byte 22 to val 30");
    SetShort(0, s, 4);
    PrintBin(s, "Set short 4 to val 0");
    SetShort(65000, s, 4);
    PrintBin(s, "Set short 4 to val 65000");
    std::cout << "byte at 22 = " << (int)GetByte(s, 22) << std::endl;
    std::cout << "short at 4 = " << (int)GetShort(s, 4) << std::endl;    
}

输出:

Start: 
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
Set bit 2 to FALSE: 
1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
Set byte 22 to val 0: 
1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 
Set byte 22 to val 30: 
1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 1 
Set short 4 to val 0: 
1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 1 0 0 0 1 1 
Set short 4 to val 65000: 
1 1 0 1 0 0 0 1 0 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 1 
byte at 22 = 30
short at 4 = 65000

您可以将这些函数转换为#DEFINE,并在调试模式下添加范围检查。