在结构中表示小值的最有效方法是什么?

Mai*_*tor 38 c struct bit-fields

我经常发现自己必须代表一个由非常小的值组成的结构.例如,Foo有4个值a, b, c, d,范围从0 to 3.通常我不在乎,但有时,这些结构是

  1. 用于紧密循环;

  2. 他们的价值读数十亿次/秒,这是该计划瓶颈;

  3. 整个计划由数十亿美元组成Foo;

在这种情况下,我发现自己无法决定如何Foo有效地表达.我基本上有4种选择:

struct Foo {
    int a;
    int b;
    int c;
    int d;
};

struct Foo {
    char a;
    char b;
    char c;
    char d;
};

struct Foo {
    char abcd;
};

struct FourFoos {
    int abcd_abcd_abcd_abcd;
};
Run Code Online (Sandbox Code Playgroud)

它们分别使用128,32,8,8位Foo,从稀疏到密集.第一个例子可能是最语言的例子,但使用它实际上会增加16倍于程序的大小,这听起来不太合适.而且,大部分内存都会被零填充而根本不被使用,这让我想知道这不是浪费.另一方面,密集地打包它们会带来额外的开销来阅读它们.

在结构中表示小值的计算"最快"方法是什么?

dbu*_*ush 34

对于不会产生大量读取开销的密集打包,我建议使用带位域的结构.在您的示例中,您有四个值,范围从0到3,您可以按如下方式定义结构:

struct Foo {
    unsigned char a:2;
    unsigned char b:2;
    unsigned char c:2;
    unsigned char d:2;
}
Run Code Online (Sandbox Code Playgroud)

这具有1个字节的大小,并且字段可以简单地访问,即foo.a,foo.b

通过使您的结构更密集,这应该有助于缓存效率.

编辑:

总结评论:

使用位域仍然有点小问题,但它是由编译器完成的,并且很可能比你手工编写的更有效(更不用说它使你的源代码更简洁,更不容易引入错误).并且考虑到你将要处理的大量结构,使用诸如此类的压缩结构所获得的缓存未命中的减少可能会弥补结构所施加的位操作的开销.

  • 使用"数十亿个Foos"可能会在缓存效率方面表现出明显的性能,缓存未命中比对齐位数的几个方面要昂贵得多 (9认同)
  • 在此基础上构建:单个高速缓存未命中通常可能花费大约200个周期.即使L1/L2未命中L3缓存也需要花费大约20个周期.如果您在1%的时间内缓存丢失,那么大部分时间都会花费缓存丢失.另一方面,进行比特篡改的额外周期可能具有低于表观成本,如果提取多个独立变量,则处理器可能能够在每个周期中退出多个指令. (4认同)
  • 根据编译器的不同,位域通常会引入"读取开销" - 唯一的区别是编译器生成位代码而不是程序员手动执行,并且编译器能够针对特定目标机器进行更好的调整. (2认同)

sta*_*ark 20

仅在考虑空间时才打包它们 - 例如,1,000,000个结构的数组.否则,进行移位和屏蔽所需的代码大于数据空间的节省.因此,您在I-cache上比D-cache更容易出现缓存未命中.

  • 这是我发现的常识,通常是不正确的.http://stackoverflow.com/questions/16738579/is-using-a-vector-of-boolean-values-slower-than-a-dynamic-bitset.如果你用eratosthenes的筛子看基准,你会看到vector <bool>在任何不完全适合缓存的大小中轻松地传递vector <char>.引用沃尔特·布莱特(Walter Bright)的话说:测量可以帮助你了解那些无法衡量的专家. (5认同)
  • 大多数处理器都有一个"加载字节"指令,与"load int"同时执行,所以第二种形式(`char`s,no bit-packing)应该总是比第一种形式有所改进(`int`s ,没有位包装). (3认同)

Pet*_*ter 11

没有明确的答案,而且您没有提供足够的信息来做出"正确"的选择.有权衡.

您的"主要目标是时间效率"的陈述是不够的,因为您尚未指定I/O时间(例如,从文件读取数据)是否比计算效率更重要(例如,某些计算集需要多长时间)用户点击"开始"按钮后.

因此,将数据写为单个字符(以减少读取或写入的时间),但将其解压缩为四个数组int(因此后续计算更快)可能是合适的.

此外,无法保证a int是32位(在您的语句中假设第一个打包使用128位).一个int可以是16位.


tec*_*rus 9

Foo有4个值,a,b,c,d,范围从0到3.通常我不关心,但有时,这些结构是......

还有另一种选择:因为值0 ... 3可能表示某种状态,你可以考虑使用"flags"

enum{
  A_1 = 1<<0,
  A_2 = 1<<1,
  A_3 = A_1|A_2,
  B_1 = 1<<2,
  B_2 = 1<<3,
  B_3 = B_1|B_2, 
  C_1 = 1<<4,
  C_2 = 1<<5,
  C_3 = C_1|C_2,
  D_1 = 1<<6,
  D_2 = 1<<7,
  D_3 = D_1|D_2,
  //you could continue to  ... D7_3 for 32/64 bits if it makes sense
}
Run Code Online (Sandbox Code Playgroud)

这与在大多数情况下使用位域没有太大区别,但可以大大减少条件逻辑.

if ( a < 2 && b < 2 && c < 2 && d < 2) // .... (4 comparisons)
//vs.
if ( abcd & (A_2|B_2|C_2|D_2) !=0 ) //(bitop with constant and a 0-compare)
Run Code Online (Sandbox Code Playgroud)

根据您将对数据执行的操作类型,使用4或8组abcd并根据需要使用0填充结束可能是有意义的.这可以允许最多32个比较用bitop和0-compare代替.例如,如果你想在64位变量中的所有8组4上设置"1位",uint64_t abcd8 = 0x5555555555555555ULL;那么你可以设置所有2位,你abcd8 |= 0xAAAAAAAAAAAAAAAAULL;现在可以将所有值设置为3


附录:进一步考虑,你可以使用union作为你的类型,并使用char和@ dbush的位域进行联合(这些标志操作仍然可以在unsigned char上使用)或者为每个a,b,c,d使用char类型并使用unsigned int将它们联合起来.这将允许紧凑的表示和有效的操作,具体取决于您使用的联合成员.

union Foo {
  char abcd; //Note: you can use flags and bitops on this too
  struct {
    unsigned char a:2;
    unsigned char b:2;
    unsigned char c:2;
    unsigned char d:2;
  };
};
Run Code Online (Sandbox Code Playgroud)

甚至进一步扩展

union Foo {
  uint64_t abcd8;  //Note: you can use flags and bitops on these too
  uint32_t abcd4[2];
  uint16_t abcd2[4];
  uint8_t  abcd[8];
  struct {
    unsigned char a:2;
    unsigned char b:2;
    unsigned char c:2;
    unsigned char d:2;
  } _[8];
};
union Foo myfoo = {0xFFFFFFFFFFFFFFFFULL};
//assert(myfoo._[0].a == 3 && myfoo.abcd[0] == 0xFF);
Run Code Online (Sandbox Code Playgroud)

此方法确实引入了一些字节序差异,如果您使用union来覆盖其他方法的任何其他组合,这也会成为一个问题.

union Foo {
  uint32_t abcd;
  uint32_t dcba; //only here for endian purposes
  struct { //anonymous struct
    char a;
    char b;
    char c;
    char d;
  };
};
Run Code Online (Sandbox Code Playgroud)

您可以使用不同的联合类型和算法进行实验和测量,以查看哪些部分值得保留,然后丢弃那些无用的联合.您可能会发现同时对多个char/short/int类型进行操作会自动优化为AVX/simd指令的某些组合,而使用位域除非您手动展开它们...在测试和测量它们之前无法知道.


Pet*_*des 9

在缓存中安装数据集至关重要.较小的总是更好,因为超线程竞争性地共享硬件线程(在Intel CPU上)之间的每核心缓存.对此答案的评论包括一些缓存未命中成本的数字.

在x86上,将带有符号或零扩展的8位值加载到32或64位寄存器(movzxmovsx)中的字面速度与mov字节或32位双字的速度一样快.存储32位寄存器的低字节也没有开销.(请参阅Agner Fog的说明表和C/asm优化指南).

仍然x86特定:[u]int8_t临时也很好,但避免[u]int16_t临时.([u]int16_t在内存中加载/存储是很好的,但是在寄存器中使用16位值会对英特尔CPU上的操作数大小前缀解码造成严重影响.)如果要将它们用作数组索引,32位临时值会更快.(使用8位寄存器不会将高24/56位归零,因此需要额外的指令来进行归零或符号扩展,使用8位寄存器作为数组索引,或者使用更宽类型的表达式(如将其添加到一个int.)

我不确定ARM或其他架构可以做什么,从单字节加载或单字节存储有效的零/符号扩展.

鉴于此,我的建议包括存储,int临时使用.(或者long,这会在x86-64上略微增加代码大小,因为需要REX前缀来指定64位操作数大小.)例如,

int a_i = foo[i].a;
int b_i = foo[i].b;
...;
foo[i].a = a_i + b_i;
Run Code Online (Sandbox Code Playgroud)

位域

打包到位域将有更多的开销,但仍然值得.在字节或32/64位内存块中测试编译时常量位(或多位)的速度很快.如果你真的需要将一些位域解压缩为ints并将它们传递给非内联函数调用或其他东西,那么将需要一些额外的指令来移位和屏蔽.如果这样可以减少缓存未命中数,那么这是值得的.

测试,设置(到1)或清除(到0)一位或一组位可以用OR或有效地完成AND,但是将未知的布尔值分配给位域需要更多的指令来将新位与其他字段的位合并.如果您经常将变量分配给位域,这可能会使代码显着膨胀.因此int foo:6,在结构中使用类似的东西,因为你知道foo不需要前两位,所以不太可能有用.如果你没有保存很多位而不是将每个东西放在它自己的byte/short/int中,那么缓存未命中的减少将不会超过额外的指令(这可能会导致I-cache/uop-cache未命中,以及直接的额外延迟和指令的工作.)

x86 BMI1/BMI2(位操作)指令集扩展将使寄存器中的数据复制到某些目标位(而不会破坏周围的位)更有效.BMI1:Haswell,Piledriver.BMI2:Haswell,挖掘机(未发布).请注意,与SSE/AVX一样,这意味着您需要功能的BMI版本,以及不支持这些指令的CPU的后备非BMI版本.AFAIK,编译器没有选项来查看这些指令的模式并自动使用它们.它们只能通过内在函数(或asm)使用.

Dbush的回答,打包到位域可能是一个不错的选择,这取决于你如何使用你的领域.您的第四个选项(将四个单独的abcd值打包到一个结构中)可能是一个错误,除非您可以使用四个连续abcd值(矢量样式)执行一些有用的操作.

一般来说,尝试两种方式

对于您的代码广泛使用的数据结构,设置是有意义的,这样您就可以从一个实现转换到另一个实现,并进行基准测试. 尼尔弗里德曼的回答,有吸气剂/安装者是一个很好的方法.但是,仅使用int临时工作并使用字段作为结构的单独成员应该可以正常工作.对于打包的位域,由编译器生成代码来测试字节的正确位.

如果有必要,准备SIMD

如果你有任何代码只检查每个结构的一个或几个字段,尤其是 循环遍历顺序结构值,然后由cmaster给出的数组结构答案将是有用的.x86向量指令具有单个字节作为最小粒度,因此具有单独字节中的每个值的数组结构将允许您快速扫描第一个元素where a == something,using PCMPEQB / PTEST.


And*_*nle 7

首先,通过"最有效"来精确定义您的意思.最佳内存利用率?最棒的表演?

然后以两种方式实现您的算法,并在实际的硬件上实际对其进行概要分析,以便在您交付时打算在其下运行它的实际条件下运行它.

选择一个更符合您"最有效"原始定义的那个.

其他任何东西都只是猜测.无论你选择什么都可以正常工作,但如果没有在你使用软件的确切条件下实际测量差异,你永远不会知道哪种实现会"更有效".


Nir*_*man 5

我认为唯一真正的答案可能是通常编写代码,然后使用所有这些编写完整的程序.我认为这不会花费那么多时间,尽管看起来有点尴尬.基本上,我会做这样的事情:

template <bool is_packed> class Foo;
using interface_int = char;

template <>
class Foo<true> {
    char m_a, m_b, m_c, m_d;
 public: 
    void setA(interface_int a) { m_a = a; }
    interface_int getA() { return m_a; }
    ...
}

template <>
class Foo<false> {
  char m_data;
 public:
    void setA(interface_int a) { // bit magic changes m_data; }
    interface_int getA() { // bit magic gets a from m_data; }
}
Run Code Online (Sandbox Code Playgroud)

如果您只是编写这样的代码而不是暴露原始数据,那么切换实现和配置文件将很容易.函数调用将内联,不会影响性能.请注意,我刚写了setA和getA而不是返回引用的函数,这实现起来比较复杂.