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

时间:2015-07-30 18:44:08

标签: c struct bit-fields

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

  1. 用于紧密循环;

  2. 他们的价值被读取十亿次/秒,这是 程序的瓶颈;

  3. 整个计划包含大量数十亿Foo s;

  4. 在这种情况下,我发现自己难以决定如何有效地表达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;
    };
    

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

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

15 个答案:

答案 0 :(得分:34)

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

struct Foo {
    unsigned char a:2;
    unsigned char b:2;
    unsigned char c:2;
    unsigned char d:2;
}

这个大小为1个字节,可以简单地访问字段,即foo.afoo.b等。

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

编辑:

总结评论:

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

答案 1 :(得分:20)

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

答案 2 :(得分:11)

没有确定的答案,而且您还没有足够的信息来允许"对"选择。有权衡。

您声明您的主要目标是时间效率"是不够的,因为你还没有指定I / O时间(例如从文件中读取数据)是否比计算效率更重要(例如,在用户点击&#34之后,某些计算集需要多长时间;转到"按钮)。

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

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

答案 3 :(得分: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
}

这与在大多数情况下使用位域大不相同,但可以大大减少条件逻辑。

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)

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

附录: 进一步考虑,你可以使用union作为你的类型,并使用char和@ dbush的位域进行联合(这些标志操作仍然可以在unsigned char上使用)或者为每个a,b,c使用char类型,d并将它们与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;
  };
};

甚至进一步扩展

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

此方法确实引入了一些字节序差异,如果您使用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;
  };
};

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

答案 4 :(得分:9)

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

在x86 上,将带有符号或零扩展名的8位值加载到32或64位寄存器(movzxmovsx)的速度与普通{{1}一样快一个字节或32位双字。存储32位寄存器的低字节也没有开销。 (参见Agner Fog的instruction tables and C / asm optimization guides here)。

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

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

鉴于此,我建议打包以进行存储,对临时使用int 。 (或int,但这会在x86-64上稍微增加代码大小,因为需要long前缀来指定64位操作数大小。)例如。

REX

位域

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

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

x86 takes more instructions会使BMI1 / BMI2 (Bit-Manipulation) instruction-set extensions(不破坏周围的比特)更有效率。 BMI1:Haswell,Piledriver。 BMI2:Haswell,挖掘机(未发布)。请注意,与SSE / AVX一样,这意味着您需要功能的BMI版本,以及不支持这些指令的CPU的后备非BMI版本。 AFAIK,编译器没有选项来查看这些指令的模式并自动使用它们。它们只能通过内在函数(或asm)使用。

copying data from a register into some destination bits,根据您使用字段的方式,打包到位域可能是一个不错的选择。您的第四个选项(将四个单独的foo值打包到一个结构中)可能是一个错误,除非您可以使用四个连续abcd值(矢量样式)执行某些有用的操作。

代码一般来说,尝试两种方式

对于您的代码广泛使用的数据结构,设置是有意义的,这样您就可以从一个实现转换到另一个实现,并进行基准测试。 Dbush's answer是一个很好的方法。但是,仅使用abcd临时值并使用字段作为结构的单独成员应该可以正常工作。对于打包的位域,由编译器生成代码来测试字节的正确位。

如果有保证,准备SIMD

如果您有任何代码只检查每个结构的一个或几个字段,尤其是循环遍历顺序结构值,然后Nir Friedman's answer, with getters/setters将是有用的。 x86向量指令具有单个字节作为最小粒度,因此使用单独字节中的每个值的数组结构将允许您使用int快速扫描a == something中的第一个元素。 / p>

答案 5 :(得分:7)

首先,精确定义“最有效”的含义。最佳内存利用率?最佳表现?

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

选择一个更符合“最有效”原始定义的那个。

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

答案 6 :(得分: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; }
}

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

答案 7 :(得分:4)

使用int s

对其进行编码

将字段视为int s。

在你的所有代码中都有

blah.x,除了声明将是你将要做的所有事情。整体推广将照顾大多数情况。

当你完成所有工作后,有3个等效的包含文件:一个使用int的包含文件,一个使用char,一个使用位域。

然后简介。在这个阶段不要担心它,因为它过早优化,除了你选择的包含文件之外什么都不会改变。

答案 8 :(得分:4)

大量数组和内存不足错误

  
      
  1. 整个计划由数十亿个Foos组成;
  2.   

首先,对于#2,您可能会发现自己或您的用户(如果其他人运行该软件)经常无法成功分配此数组(如果它跨越千兆字节)。这里常见的错误是认为内存不足错误意味着&#34;没有更多可用内存&#34; ,而它们通常意味着操作系统找不到连续集< / em>未使用的页面与请求的内存大小匹配。由于这个原因,人们经常在他们请求分配一个千兆字节块时感到困惑,即使他们有30千兆字节的物理内存空闲,例如一旦开始分配内存的大小超过(例如)可用内存量的1%,通常需要考虑避免使用一个巨大的阵列代表整个内存。

因此,您需要做的第一件事就是重新考虑数据结构。通过分配较小的块(较小的阵列聚合在一起),您通常可以显着降低遇到问题的几率,而不是分配数十亿个元素的单个数组。例如,如果您的访问模式本质上是顺序的,则可以使用展开的列表(链接在一起的数组)。如果需要随机访问,您可能会使用类似指向数组的指针数组,每个数组跨越4千字节。这需要更多的工作来索引元素,但是由于这种数十亿元素的规模,它通常是必需的。

访问模式

问题中未指明的一个问题是内存访问模式。这部分对于指导您的决策至关重要。

例如,数据结构是否仅按顺序遍历,还是需要随机访问?所有这些字段:abcd是否一直需要,或者可以一次访问一个或两个或三个?

让我们尝试涵盖所有可能性。在我们谈论的规模上,这个:

struct Foo {
    int a1;
    int b1;
    int c1;
    int d1
};

......不太可能有所帮助。在这种输入规模,并在紧密循环中访问,您的时间通常将由内存层次结构的高层(分页和CPU缓存)控制。重点关注层次结构的最低级别(寄存器和相关指令)不再那么重要。换句话说,要处理数十亿个元素,您应该担心的最后一件事是将此内存从L1缓存行移动到寄存器的成本以及按位指令的成本,例如: (不是说它根本不是一个问题,只是说它的优先级低得多)。

在足够小的范围内,整个热数据都适合CPU缓存并需要随机访问,这种简单的表示可以显示性能改进,因为层次结构的最低级别的改进(寄存器)和指令),但它需要一个比我们所说的更小规模的输入。

所以即使这可能是一个相当大的改进:

struct Foo {
    char a1;
    char b1;
    char c1;
    char d1;
};

......还有更多:

// Each field packs 4 values with 2-bits each.
struct Foo {
    char a4; 
    char b4;
    char c4;
    char d4;
};

* 请注意,您可以使用位域进行上述操作,但根据所使用的编译器,位域往往会有与它们相关的警告。由于通常描述的可移植性问题,我经常小心避免它们,尽管在您的情况下这可能是不必要的。然而,当我们冒险进入下面的SoA和热/冷场分裂区域时,我们将达到无论如何都不能使用位域的程度。

此代码还将重点放在水平逻辑上,这可以开始更容易探索一些进一步的优化路径(例如:转换代码以使用SIMD),因为它已经是微型SoA形式。 / p>

数据&#34;消费&#34;

特别是在这种规模上,当你的内存访问本质上是顺序的时候更是如此,它有助于从数据和消费的角度来考虑&#34; (机器可以多快加载数据,执行必要的算术运算并存储结果)。我觉得有用的一个简单的心理形象就是将计算机想象成一个“大嘴巴”。如果我们一次性提供足够大量的数据,而不是少量的茶匙,并且将更多相关数据紧密地装入连续的勺子中,它会更快。

Hungry Computer

热/冷场分裂

到目前为止,上述代码假设所有这些字段都同样热(频繁访问),并一起访问。您可能有一些冷场或字段只能成对访问关键代码路径。我们假设您很少访问cd,或者您的代码有一个访问ab的关键循环,另一个访问c 1}}和d。在这种情况下,将它拆分为两个结构会很有帮助:

struct Foo1 {
    char a4; 
    char b4;
};
struct Foo2 {
    char c4;
    char d4;
};

如果我们再次喂养&#34;计算机数据,我们的代码目前仅对ab字段感兴趣,如果我们有连续的话,我们可以将更多内容打包到ab字段中阻止仅包含 ab字段,而不是cd字段。在这种情况下,cd字段将是计算机此刻可以消化的数据,但它会混合到a和{之间的内存区域中{1}}字段。如果我们希望计算机尽可能快地使用数据,那么我们现在应该仅向其提供感兴趣的相关数据,因此在这些情况下分割结构是值得的。

用于顺序访问的SIMD SoA

转向矢量化,并假设顺序访问,计算机消耗数据的最快速率通常使用SIMD并行。在这种情况下,我们可能会得到这样的表示:

b

...小心注意对齐和填充(对于AVX,大小/对齐应该是16或32字节的倍数,或者对于未来的AVX-512,应该是64的倍数),以便在XMM / YMM寄存器中使用更快的对齐移动(并且可能在将来使用AVX指令。)

用于随机/多场访问的AoSoA

不幸的是,如果经常一起访问struct Foo1 { char* a4n; char* b4n; }; a,上述表示可能会开始失去很多潜在的好处,尤其是随机访问模式。在这种情况下,更优化的表示可以看起来像这样:

b

...我们现在正在聚合这个结构。这使得struct Foo1 { char a4x32[32]; char b4x32[32]; }; a字段不再分散,允许32个ba字段组合成一个64字节的缓存行并快速访问。我们现在还可以将128或256个ba元素放入XMM / YMM寄存器中。

仿形

通常情况下,我会尽量避免在性能问题上提出一般性的智慧建议,但我注意到这个问题似乎避免了那些掌握了剖析器的人通常会提到的细节。所以我很抱歉,如果这有点像光顾,或者如果一个分析器已经被积极使用,但我认为这个问题需要保证。

作为一则轶事,我经常做得更好(我不应该!)优化由比我更了解计算机体系结构知识的人编写的生产代码(我曾经使用过很多那些来自打卡时代并且能够一目了然地了解汇编代码的人,并且经常会被调用以优化他们的代码(这感觉非常奇怪)。这有一个简单的原因:我被欺骗了#34;并使用了一个分析器(VTune)。我的同龄人经常没有(他们对它过敏,并认为他们理解热点和剖析器一样,并认为剖析是浪费时间)。

当然,最理想的是找到一个既拥有计算机架构专业知识又掌握了分析器的人,但缺少一个或另一个,分析器可以给予更大的优势。优化仍然会奖励一种生产率思维模式,这种思维模式取决于最有效的优先级,最有效的优先级是优化真正最重要的部分。分析器为我们提供了详细的故障,详细说明了花费了多少时间和地点,以及有用的指标,如缓存未命中和分支误预测,即使是最先进的人类通常也无法预测任何接近于分析器可以显示的精确度。此外,通过追逐热点并研究它们存在的原因,分析通常是发现计算机体系结构如何以更快的速度运行的关键。对我来说,分析是更好地理解计算机体系结构实际工作方式的最终切入点,而不是我想象它的工作方式。只有到那时,在这方面经验丰富的人b的着作才开始变得越来越有意义。

界面设计

这里可能会开始明显的一点是,有许多优化可能性。这类问题的答案将是关于策略而不是绝对方法。在您尝试某些事情之后,事后才能发现很多事情,并且在您需要时仍然会针对越来越多的最佳解决方案进行迭代。

复杂代码库中的困难之一是在接口中留下足够的喘息空间来进行实验并尝试不同的优化技术,迭代并迭代以获得更快的解决方案。如果界面留有空间来寻求这些优化,那么我们可以整天优化,并且如果我们正确地测量事情,即使有试错思路,也可以获得一些了不起的结果。

为了在实现中留出足够的喘息空间来进行实验和探索更快的技术,通常需要接口设计接受批量中的数据。如果接口涉及间接函数调用(例如:通过dylib或函数指针),其中内联不再是有效的可能性,则尤其如此。在这种情况下,在没有级联接口中断的情况下留出优化空间通常意味着设计远离接收简单标量参数的思维模式,有利于将指针传递给整个数据块(如果存在各种交错可能性,可能需要大步)。因此,虽然这是一个非常广泛的领域,但优化的许多优先事项将归结为留下足够的喘息空间来优化实现,而不会在整个代码库中进行级联更改,并且有一个分析器可以指导您正确的方式。

TL; DR

无论如何,其中一些策略应该有助于指导您正确的方法。这里没有绝对的东西,只有指南和尝试的东西,总是最好用手中的分析器。然而,当处理这种巨大规模的数据时,它总是值得记住饥饿怪物的形象,以及如何最有效地为它们提供这些适当大小和包装好的一勺相关数据。

答案 9 :(得分:3)

让我们说,你的内存总线有点老了,可以提供10 GB / s。现在采用2.5 GHz的CPU,你会发现每个周期至少需要处理4个字节才能使内存总线饱和。因此,当您使用

的定义时
struct Foo {
    char a;
    char b;
    char c;
    char d;
}

并在每次传递数据时使用所有四个变量,您的代码将受CPU限制。你不能通过更密集的包装获得任何速度。

现在,当每次传递仅对四个值之一执行一个简单的操作时,这是不同的。在这种情况下,你最好使用数组结构:

struct Foo {
    size_t count;
    char* a;    //a[count]
    char* b;    //b[count]
    char* c;    //c[count]
    char* d;    //d[count]
}

答案 10 :(得分:3)

您已经说明了常见且含糊不清的C / C ++标记。

假设使用C ++,请将数据设为私有并添加getter / setter。 不,这不会导致性能损失 - 只要打开优化器即可。

然后,您可以更改实现以使用替代方案而无需更改调用代码 - 因此可以根据基准测试的结果更轻松地实现实现。

对于记录,我希望{@ 1}}根据@dbush的位字段最有可能是给定描述的最快。

注意所有这些都是将数据保存在缓存中 - 您可能还想查看调用算法的设计是否可以帮助解决这个问题。

答案 11 :(得分:3)

回到问题:

  

用于紧密循环;

     

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

     

整个计划由数十亿个Foos组成;

这是一个典型的例子,说明何时编写特定于平台的高性能代码需要花时间为每个实现平台进行设计,但其好处超过了成本。

由于它是整个程序的瓶颈,因此您不需要寻找通用解决方案,但要认识到这需要测试多种方法并针对实际数据进行定时,因为最佳解决方案将是< em>特定于平台的

OP也应该考虑使用OpenCLOpenMP作为潜在的解决方案,以便最大限度地利用可用资源。运行时硬件。这有点依赖于您对数据的需求,但它可能是此类问题最重要的方面 - 如何利用可用的并行性。

但这个问题没有一个正确的答案,IMO。

答案 12 :(得分:2)

最有效的性能/执行是使用处理器的字大小。不要让处理器执行额外的包装或拆包工作。

某些处理器有一个以上的有效尺寸。许多ARM处理器可以在8/32位模式下运行。这意味着处理器针对处理8位量或32位量进行了优化。对于这样的处理器,我建议使用8位数据类型。

您的算法与效率有很大关系。如果要移动数据或复制数据,可能需要考虑一次移动32位数据(4个8位数量)。这里的想法是减少处理器的提取次数。

为了提高性能,请编写代码以使用寄存器,例如使用更多本地变量。从存储器获取到寄存器比直接使用寄存器更昂贵。

最重要的是,查看您的编译器优化设置。设置编译以获得最高性能(速度)设置。接下来,生成函数的汇编语言列表。查看列表以查看编译器如何生成代码。调整代码以提高编译器的优化功能。

答案 13 :(得分:2)

如果您追求的是空间效率,那么您应该考虑完全避免使用struct。编译器将根据需要在结构表示中插入填充,以使其大小为其对齐要求的倍数,该对齐要求可能多达16个字节(但更可能是4或8个字节,并且最终可能只有1个字节)。

如果您仍然使用结构,那么使用哪个结构取决于您的实现。如果@ dbush的位域方法产生一个字节的结构,那么它很难被击败。如果您的实现将表示填充至少四个字节,无论如何,那么这可能是使用的那个:

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

或者我想我可能会使用这个变体:

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

由于我们假设您的结构占用了至少四个字节,因此将数据打包到较小的空间是没有意义的。事实上,这会适得其反,因为它还会使处理器完成额外的工作包装并解压缩其中的值。

为了处理大量数据,有效利用CPU缓存比避免几个整数操作提供了更大的胜利。如果你的数据使用模式至少有点系统化(例如,如果在访问你之前的struct数组的一个元素之后,你可能会访问下一个的那个元素)那么你可能会通过打包来提高空间效率和速度。数据尽可能紧密。根据您的C实现(或者如果您想避免实现依赖性),您可能需要以不同方式实现 - 例如,通过整数数组。对于四个字段的特定示例,每个字段需要两位,我会考虑代表每个&#34; struct&#34;改为uint8_t,每个总共1个字节。

也许是这样的:

#include <stdint.h>

#define NUMBER_OF_FOOS 1000000000
#define A 0
#define B 2
#define C 4
#define D 6

#define SET_FOO_FIELD(foos, index, field, value) \
    ((foos)[index] = (((foos)[index] & ~(3 << (field))) | (((value) & 3) << (field))))
#define GET_FOO_FIELD(foos, index, field) (((foos)[index] >> (field)) & 3)

typedef uint8_t foo;

foo all_the_foos[NUMBER_OF_FOOS];

字段名宏和访问宏提供了一种更易读且可调整的方式来访问单个字段,而不是直接操作数组(但要注意这些特定的宏不止一次地评估它们的一些参数) 。使用每一位,只需通过选择数据结构就可以实现良好的缓存使用率。

答案 14 :(得分:2)

我做了一段时间的视频解压缩。最快的事情是这样的:

short ABCD; //use a 16 bit data type for your example

并设置一些宏。也许:

#define GETA ((ABCD >> 12) & 0x000F)
#define GETB ((ABCD >> 8) & 0x000F)
#define GETC ((ABCD >> 4) & 0x000F)
#define GETD (ABCD  & 0x000F)  // no need to shift D

在实践中,您应该尝试移动32位长或64位长,因为这是大多数现代处理器上的原生MOVE大小。

使用结构将始终在编译的代码中创建额外指令的开销,这些指令从结构的基址到字段。如果你真的想收紧你的循环,那就远离它。

编辑: 上面的例子给出了4位值。如果你真的只需要0..3的值,那么你可以做同样的事情来取出你的2位数,所以,,, GETA可能看起来像这样:

GETA ((ABCD >> 14) & 0x0003)

如果你真的在移动数十亿的东西,我不怀疑它,只需填充一个32位变量并移动并屏蔽你的方式。

希望这会有所帮助。