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

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

  1. 用于紧密的环路中;

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

  3. 整个程序由大量的数十亿美元组成;

在这种情况下,我发现自己很难决定如何有效地expressionFoo 。 我基本上有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; }; 

它们分别使用128,32,8,8比特,每个Foo从稀疏到密集。 第一个例子可能是最具语言性的,但是使用它的程度基本上会增加16倍,这听起来不太对。 而且,大部分的记忆都是零填充,根本不用,这让我怀疑这是不是浪费。 另一方面,将它们密集地包装起来会带来额外的阅读费用。

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

对于不会导致大量读取的密集打包,我会推荐一个带有位域的结构。 在你的例子中,你有四个值从0到3,你可以定义结构如下:

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

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

通过使您的结构更密集,这应该有助于caching效率。

编辑:

总结评论:

编译器完成的操作仍然有点儿麻烦,而且最有可能比手动编写更有效率(更不用说使源代码更简洁,不太容易引入错误)。 考虑到你要处理大量的结构,通过使用像这样的打包结构获得的caching未命中的减less将可能弥补结构所施加的位操作的开销。

只有空间是一个考虑因素时才包装它们 – 例如,一百万个结构的数组。 否则,需要进行移位和屏蔽的代码大于数据空间的节省。 因此,你更可能有一个高速caching未命中的Icaching比Dcaching。

没有明确的答案,而且你没有提供足够的信息来做出“正确的”select。 有折衷。

您的“主要目标是时间效率”的陈述是不够的,因为您没有指定I / O时间(例如从文件中读取数据)是否比计算效率更关心问题(例如,某些计算集合需要多长时间在用户点击“开始”button之后)。

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

另外,不能保证int是32位(你在声明中假定第一个包装使用128位)。 一个int可以是16位。

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

还有另一种select:因为值0 … 3可能表示某种状态,所以可以考虑使用“标志”

 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 } 

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

 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个比较被replace为bitop和0比较。 例如,如果你想在64位variables的所有8组中设置“1位”,你可以这样做: uint64_t abcd8 = 0x5555555555555555ULL; 然后设置所有你可以做的2位abcd8 |= 0xAAAAAAAAAAAAAAAAULL; 现在所有的价值3


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

这种方法确实会引入一些sorting差异,如果使用联合来覆盖其他方法的任何其他组合,这也会是一个问题。

 union Foo { uint32_t abcd; uint32_t dcba; //only here for endian purposes struct { //anonymous struct char a; char b; char c; char d; }; }; 

你可以用不同的联合types和algorithm进行实验和测量,看看联盟的哪些部分值得保留,然后丢弃那些没用的联盟。 你可能会发现同时使用几个char / short / inttypes的函数会自动优化AVX / simd指令的某种组合,而使用位域则不会除非你手动展开它们…除非你testing和测量它们,否则无法知道。

将你的数据集合在caching中是至关重要的。 越小越好,因为超线程在硬件线程(Intel CPU)之间竞争地共享每个内核的caching。 这个答案的评论包括caching未命中成本的一些数字。

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

仍然x86特定: [u]int8_t临时也是好的,但避免[u]int16_t临时。 (在内存中加载/存储来自/到[u]int16_t是可以的,但是在寄存器中使用16位的值会在英特尔CPU上缓慢地执行操作数大小的前缀解码。)如果要使用32位临时对象,速度会更快作为数组索引。 (使用8位寄存器不会使高24位/ 56位为零,所以需要一个额外的指令来进行零或符号扩展,将8位寄存器用作数组索引,或者使用更宽types的expression式(如将其添加到一个int 。)

我不确定ARM或其他体系结构可以做什么,只要从单字节加载或单字节存储中进行有效的零/符号扩展即可。

鉴于此,我的build议是包装存储,临时使用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; 

位域

打包到位域会有更多的开销,但仍然值得。 在一个字节或32/64位内存块中testing编译时间恒定位位置(或多位)是快速的。 如果实际上需要将一些位域解压缩为int并将它们传递给非内联函数调用或其他内容,则需要一些额外的指令来移位和屏蔽。 如果这可以使caching未命中略有减less,这可能是值得的。

testing,设置(到1)或清除(到0)一个比特或一组比特可以用ORAND来有效完成,但是将一个未知的布尔值分配给一个比特字段需要更多的指令来合并新比特和其他比特领域。 如果您经常将variables分配给位域,这可能会使代码显着膨胀。 所以在你的结构中使用int foo:6和类似的东西,因为你知道foo不需要最高两位,不太可能有帮助。 如果你把每一个东西都放在自己的byte / short / int中,而不是保存很多位,那么caching未命中的减less将不会超过额外的指令(这会增加I-cache / uop-cache未命中,以及指令的直接额外延迟和工作。)

x86 BMI1 / BMI2(位操作)指令集扩展将使得将寄存器中的数据复制到某些目标位 (不会破坏周围位),效率更高。 BMI1:Haswell,打桩机。 BMI2:Haswell,挖掘机(未发行)。 请注意,就像SSE / AVX一样,这将意味着您需要使用BMI版本的函数,并且为不支持这些指令的CPU回退非BMI版本。 AFAIK,编译器没有选项来查看这些指令的模式并自动使用它们。 它们只能通过内部函数(或asm)使用。

Dbush的答案 ,打包到位域可能是一个不错的select,取决于你如何使用你的领域。 你的第四个选项(将四个单独的abcd值打包到一个结构中)可能是一个错误, 除非你能用四个连续的abcd值(向量样式)做一些有用的事情。

一般的代码,尝试两种方式

对于您的代码广泛使用的数据结构,设置事物是有意义的,因此您可以从一个实现跳转到另一个实现,并进行基准testing。 尼尔·弗里德曼(Nir Friedman)的回答是,吸气剂/吸附剂是一个很好的select。 但是,只是使用int临时工和作为结构的单独成员的字段应该工作正常。 编译器需要生成代码来testing一个字节的正确位,以便打包位域。

准备SIMD,如果担保

如果你有任何代码只检查每个结构的一个或几个字段, 循环顺序结构值,那么由cmaster给出的struct-of-array答案将是有用的。 x86向量指令有一个单字节作为最小的粒度,所以一个单独的字节中的每个值的数组struct-of-arrays将让你快速扫描第一个元素,在哪里a == something ,使用PCMPEQB / PTEST

首先,准确地定义“最高效”的含义。 最佳内存利用率? 最棒的表演?

然后用两种方法来实现你的algorithm,然后在你打算运行它的实际条件下,在你要运行的实际硬件上实际分析它。

select一个更符合“最高效”的原始定义。

其他任何事情只是一个猜测。 无论你select什么样的方式,都可能工作得很好,但是如果没有在你使用软件的确切条件下真正测量差异,你永远不会知道哪个实现更“高效”。

我认为唯一真正的答案是一般编写你的代码,然后用所有的程序分析完整的程序。 我不认为这会花费很多时间,虽然看起来可能会更尴尬。 基本上,我会做这样的事情:

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

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

int s编码

将字段视为int

blah.x在你的所有代码,除了declarion将是所有你会做的。 整体推广将照顾大多数情况下。

当你完成了,有3个等价物包含文件:一个使用int的包含文件,一个使用char和一个使用位域。

然后configuration文件。 在这个阶段不要担心,因为它过早的优化,除了你select的包含文件将会改变。

大规模arrays和内存不足错误

  1. 整个计划包括大量的数十亿Foos;

首先,对于#2,你可能发现自己或你的用户(如果其他人运行该软件的话)通常无法成功地分配这个数组,如果它跨越千兆字节。 这里常见的错误是认为内存不足意味着“没有更多的内存可用” ,而这往往意味着操作系统找不到与所请求的内存大小相匹配的连续的未使用页面。 正因为这个原因,当人们要求分配一个千兆字节块时,即使它们有30千兆字节的物理内存空闲,也经常会感到困惑。例如,一旦开始分配内存的大小超过了比如1典型的可用内存量的百分比,通常是考虑避免一个巨大的arrays来代表整个事情的时候了。

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

访问模式

在这个问题中未指定的事情之一是内存访问模式。 这部分对于指导您的决定至关重要。

例如,数据结构是单独遍历的,还是需要随机访问? 所有这些领域: abcd ,一直需要在一起,还是一次可以访问一两个或三个?

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

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

…不太可能有帮助。 在这种input范围内,并且在紧密循环中访问时,您的时间通常将由内存层次结构(分页和CPUcaching)的上层控制。 关注于层级的最低级别(寄存器和相关指令)不再是至关重要的。 换句话说,在数十亿个元素的处理中,最后一件你应该担心的事情是将这个内存从L1caching行移动到寄存器的成本以及按位指令的成本,例如(不是说这不是一个问题所有,只是说这是一个非常低的优先级)。

在整个热数据放入CPU高速caching并需要随机访问的足够小规模的情况下,由于层次结构(寄存器和指令)的最低级别的改进,这种简单的表示可以显示性能改进, ,但是这需要比我们所谈论的要小得多的input。

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

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

* 请注意,您可以使用上述的位字段,但位域往往有警告与他们有关,取决于正在使用的编译器。 由于可移植性问题,我经常小心避免这些问题,尽pipe这可能对您不必要。 然而,当我们冒险进入SoA以及下面的热场/冷场分裂领域时,我们将达到无法使用位场的点。

这段代码还把重点放在了水平逻辑上,它可以开始更容易地探索一些进一步的优化path(例如:转换代码以使用SIMD),因为它已经是一个微型的SoAforms。

数据“消费”

特别是在这种规模的情况下,甚至当你的存储器访问本质上是连续的时候,它有助于从数据“消耗”(机器能够多快载入数据,进行必要的算术运算并存储结果) 。 我觉得有用的一个简单的心理图像是把电脑想象成一个“大嘴巴”。 如果我们一次性提供足够多的一勺数据,而不是less量的茶匙,并且将更多的相关数据紧紧包装在一个连续的一勺中,那么速度会更快。

饿计算机

热/冷场分裂

上面的代码到目前为止都假设所有这些字段是同样热(频繁访问),并一起访问。 您可能有一些只能在关键代码path中成对访问的冷区或区域。 假设你很less访问cd ,或者你的代码有一个访问ab关键循环,另一个访问cd 。 在这种情况下,把它分成两个结构是有帮助的:

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

再次,如果我们“喂养”计算机数据,而我们的代码目前只对ab领域感兴趣,如果我们有只包含 ab字段的连续块,那么我们可以包装更多的ab字段,而不是cd字段。 在这种情况下, cd字段将成为计算机此刻无法消化的数据,但它将被混合到ab字段之间的存储区域中。 如果我们希望计算机尽快使用数据,那么我们现在只需要把相关的数据提供给它,所以在这些情况下值得分解。

用于顺序访问的SIMD SoA

转向vector化,并假定顺序访问,计算机可以使用数据的最快速率通常是使用SIMD并行的。 在这种情况下,我们最终可能会有这样的表示:

 struct Foo1 { char* a4n; char* b4n; }; 

…仔细注意alignment和填充(对于AVX来说,尺寸/alignment方式应该是16或32字节的倍数,或者对于未来的AVX-512来说应该是64的倍数),以便使用更快的alignment移动到XMM / YMM寄存器AVX指令在未来)。

用于随机/多字段访问的AoSoA

不幸的是,如果ab经常一起访问,上述表示可能开始丧失很多潜在的好处,特别是随机访问模式。 在这种情况下,更优化的表示可以看起来像这样:

 struct Foo1 { char a4x32[32]; char b4x32[32]; }; 

…我们现在正在汇总这个结构。 这使得ab字段不再如此分散,允许将32个ab字段组合成一个64字节的高速caching行并快速访问。 我们现在也可以将128或256个ab元素装入XMM / YMM寄存器。

剖析

通常我会尽量避免在性能问题上提供一般性的智慧build议,但是我注意到这个似乎避免了那些掌握了剖析器的人通常会提到的细节。 所以我很抱歉,如果光顾这一点,或者如果一个探查器已经被积极使用,但我认为这个问题是值得的。

作为一个轶事,我经常做得更好(我不应该!)优化生产代码,这些代码比我的计算机体系结构方面的知识水平要高很多(我和很多来自打卡的人一起工作时代,一眼就能理解汇编代码),而且经常会被调用来优化他们的代码(这真的很奇怪)。 这是一个简单的原因:我“欺骗”,并使用探查器(VTune)。 我的同事往往没有(他们有过敏,认为他们理解的热点就像一个分析器,并认为分析是浪费时间)。

当然,理想的是find一个既掌握计算机架构专业知识又掌握剖析器的人,但缺乏这样一个人,剖析师可以给予更大的优势。 优化仍然是一个生产力思维模式,这取决于最有效的优先级,而最有效的优先级是优化真正最重要的部分。 分析器为我们详细地分析了花了多less时间和在哪里,以及有用的度量标准,如高速caching未命中和分支错误预测,哪怕是最先进的人类通常无法预测的地方,接近于剖析器可以揭示的准确度。 此外,通过追踪热点并研究其存在的原因,分析通常是发现计算机体系结构如何以更快的速度运行的关键。 对我来说,剖析是进一步了解计算机架构实际工作的最终切入点,而不是我想象它如何工作。 只有在这方面经历过的Mysticial才开始变得越来越有意义。

界面devise

其中一个可能开始变得明显的事情是有很多优化的可能性。 这类问题的答案将是战略而不是绝对方法。 在尝试了一些东西之后,仍然有很多东西需要被发现,并且仍然会在你需要的时候朝着越来越多的最佳解决scheme迭代。

在一个复杂的代码库中遇到的困难之一是在接口中留下足够的空间来尝试和尝试不同的优化技术,以迭代和迭代更快的解决scheme。 如果界面留下空间来寻求这种优化,那么我们可以整天优化,如果我们正确地衡量事物,即使有一个反复的思维模式,也会得到一些奇妙的结果。

为了在实施中经常留出足够的呼吸空间,甚至尝试和探索更快的技术,通常需要接口devise接受批量数据。 This is especially true if the interfaces involve indirect function calls (ex: through a dylib or a function pointer) where inlining is no longer an effective possibility. In such scenarios, leaving room to optimize without cascading interface breakages often means designing away from the mindset of receiving simple scalar parameters in favor of passing pointers to whole chunks of data (possibly with a stride if there are various interleaving possibilities). So while this is straying into a pretty broad territory, a lot of the top priorities in optimizing here are going to boil down to leaving enough breathing room to optimize implementations without cascading changes throughout your codebase, and having a profiler in hand to guide you the right way.

TL; DR

Anyway, some of these strategies should help guide you the right way. There are no absolutes here, only guides and things to try out, and always best done with a profiler in hand. Yet when processing data of this enormous scale, it's always worth remembering the image of the hungry monster, and how to most effectively feed it these appropriately-sized and packed spoonfuls of relevant data.

Let's say, you have a memory bus that's a little bit older and can deliver 10 GB/s. Now take a CPU at 2.5 GHz, and you see that you would need to handle at least four bytes per cycle to saturate the memory bus. As such, when you use the definition of

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

and use all four variables in each pass through the data, your code will be CPU bound. You can't gain any speed by a denser packing.

Now, this is different when each pass only performs a trivial operation on one of the four values. In that case, you are better off with a struct of arrays:

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

You've stated the common and ambiguous C/C++ tag.

Assuming C++, make the data private and add getters/ setters. No, that will not cause a performance hit – providing the optimizer is turned on.

You can then change the implementation to use the alternatives without any change to your calling code – and therefore more easily finesse the implementation based on the results of the bench tests.

For the record, I'd expect the struct with bit fields as per @dbush to be most likely the fastest given your description.

Note all this is around keeping the data in cache – you may also want to see if the design of the calling algorithm can help with that.

Getting back to the question asked :

used in a tight loop;

their values are read a billion times/s, and that is the bottleneck of the program;

the whole program consists of a big array of billions of Foos;

This is a classic example of when you should write platform specific high performance code that takes time to design for each implementation platform, but the benefits outweigh that cost.

As it's the bottleneck of the entire program you don't look for a general solution, but recognize that this needs to have multiple approaches tested and timed against real data, as the best solution will be platform specific .

It is also possible, as it is a large array of billion of foos, that the OP should consider using OpenCL or OpenMP as potential solutions so as to maximize the exploitation of available resources on the runtime hardware. This is a little dependent on what you need from the data, but it's probably the most important aspect of this type of problem – how to exploit available parallelism.

But there is no single right answer to this question, IMO.

The most efficient, performance / execution, is to use the processor's word size. Don't make the processor perform extra work of packing or unpacking.

Some processors have more than one efficient size. Many ARM processors can operate in 8/32 bit mode. This means that the processor is optimized for handling 8 bit quantities or 32-bit quantities. For a processor like this, I recommend using 8-bit data types.

Your algorithm has a lot to do with the efficiency. If you are moving data or copying data you may want to consider moving data 32-bits at a time (4 8-bit quantities). The idea here is to reduce the number of fetches by the processor.

For performance, write your code to make use of registers , such as using more local variables. Fetching from memory into registers is more costly than using registers directly.

Best of all, check out your compiler optimization settings. Set your compile for the highest performance (speed) settings. Next, generate assembly language listings of your functions. Review the listing to see how the compiler generated code. Adjust your code to improve the compiler's optimization capabilities.

If what you're after is efficiency of space, then you should consider avoiding struct s altogether. The compiler will insert padding into your struct representation as necessary to make its size a multiple of its alignment requirement, which might be as much as 16 bytes (but is more likely to be 4 or 8 bytes, and could after all be as little as 1 byte).

If you use a struct anyway, then which to use depends on your implementation. If @dbush's bitfield approach yields one-byte structures then it's hard to beat that. If your implementation is going to pad the representation to at least four bytes no matter what, however, then this is probably the one to use:

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

Or I guess I would probably use this variant:

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

Since we're supposing that your struct is taking up a minimum of four bytes, there is no point in packing the data into smaller space. That would be counter-productive, in fact, because it would also make the processor do the extra work packing and unpacking the values within.

For handling large amounts of data, making efficient use of the CPU cache provides a far greater win than avoiding a few integer operations. If your data usage pattern is at least somewhat systematic (eg if after accessing one element of your erstwhile struct array, you are likely to access a nearby one next) then you are likely to get a boost in both space efficiency and speed by packing the data as tightly as you can. Depending on your C implementation (or if you want to avoid implementation dependency), you might need to achieve that differently — for instance, via an array of integers. For your particular example of four fields, each requiring two bits, I would consider representing each "struct" as a uint8_t instead, for a total of 1 byte each.

也许这样的事情:

 #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]; 

The field name macros and access macros provide a more legible — and adjustable — way to access the individual fields than would direct manipulation of the array (but be aware that these particular macros evaluate some of their arguments more than once). Every bit is used, giving you about as good cache usage as it is possible to achieve through choice of data structure alone.

I did video decompression for a while. The fastest thing to do is something like this:

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

and set up some macros. 也许:

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

In practice you should try to be moving 32 bit longs or 64 bit long long because thats the native MOVE size on most modern processors.

Using a struct will always create the overhead in your compiled code of extra instructions from the base address of you struct to the field. So get away from that if you really want to tighten your loop.

Edit: Above example gives you 4 bit values. If you really just need values of 0..3 then you can do the same things to pull out your 2 bit numbers so,,,GETA might look like this:

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

And if you are really moving billions of things things, and I don't doubt it, just fill up a 32bit variable and shift and mask your way through it.

希望这可以帮助。