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

38

通常我发现自己需要表示一个由非常小的值组成的结构。例如,Foo 有4个值,a、b、c、d,它们的范围从 0 到 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;
};

对于每个 Foo,它们分别使用128、32、8和8位,从稀疏到密集地打包。第一个示例可能是最语言学的一个,但使用它会将程序大小增加16倍,这听起来不太合适。此外,大多数内存将被填充为零并且根本不会被使用,这让我想知道这是否是浪费。另一方面,将它们密集打包会带来额外的读取开销。

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


评论不适合进行长时间的讨论;此对话已被移至聊天室 - George Stocker
2
正如其他几个答案所指出的那样,你没有给我们足够的信息。你似乎自相矛盾,“时间效率是目标”,“但阅读也很重要”。选择你所谓的“最有效”的意思。一旦你这么做了,你就会发现你应该进行基准测试,这应该是你下一步要做的事情。如果你想让我们回答你的问题,请告诉我们需要形成意见的事实。提出这个问题并没有给我们必要的事实,因此是一个与主题无关的问题。 - George Stocker
4
@GeorgeStocker 我不同意将此问题暂停。虽然一些答案是基于个人观点的,但这并非是问题固有的特性。dbush提出了一种技术,而OP没有想到,我发布了基准测试和编写通用代码的想法。其他人还发布了关于CPU的有用信息。我认为有几个好的答案不是主要基于个人观点的,这本身就表明问题并非主要基于个人观点。 - Nir Friedman
@NirFriedman,要获得社区重新开放的共识,您最好的选择是metaC++聊天室 - George Stocker
1
你通常是想要同时获取结构体中的所有值吗?还是你只关心 a 的一次遍历,或者只关心一半结构体中的 d 的一次遍历? - rob mayoff
显示剩余3条评论
15个回答

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等。

通过使结构体更加紧密地打包,可以提高缓存效率。

编辑:

总结评论:

使用位域仍然涉及位操作,但由编译器完成的操作很可能比手动编写的更有效(更不用说它使源代码更简洁,且不容易引入错误)。鉴于您将处理的大量结构体,使用这样的紧凑结构体所获得的减少缓存未命中的好处很可能弥补了结构体施加的位操作的开销。


2
根据编译器的不同,位域通常会引入“读取开销” - 唯一的区别在于编译器生成位操作代码而不是程序员手动操作,并且编译器能够更好地针对特定目标机器进行调整。 - Peter
1
我最喜欢这种方法,但我肯定会进行一些分析和测试,以确保速度没有受到太大影响。 - rost0031
1
嗯,位域在性能方面需要考虑很多因素... - Qix - MONICA WAS MISTREATED
9
如果有“数十亿个 Foo”,从性能方面来说,它可能会在缓存效率上表现出色,缓存未命中比对齐位所需的几条指令昂贵得多。 - Glenn Teitelbaum
4
延伸这点:一个缓存未命中通常会花费约200个时钟周期,即使是从L1/L2缓存未命中到L3缓存,也可能需要大约20个时钟周期。如果您的缓存未命中率为1%,那么大部分时间都将用于缓存未命中。另一方面,进行位操作所需的额外时钟周期可能具有比表面上低的成本,如果提取多个独立变量,则处理器可以每个时钟周期退役多个指令。 - Nir Friedman
显示剩余6条评论

20
仅在空间考虑时才打包它们 - 例如,一个包含1,000,000个结构体的数组。否则,执行移位和掩码所需的代码比数据节省的空间更多。因此,在I-cache中遇到缓存未命中的可能性比D-cache更大。

1
简单而简洁的答案。这通常是位字段的黄金捕捉规则。 - Qix - MONICA WAS MISTREATED
5
这是我发现经常不正确的常识。如果您查看使用埃拉托斯特尼筛法的基准测试,您会发现 vector<bool> 在任何未完全适合缓存的大小上都比 vector<char> 更快。引用 Walter Bright 的话:测量让你超越那些太过自信而不愿意测量的专家。 - Nir Friedman
1
正如我所说,如果数据大小是一个问题,请将它们打包。我的问题是有些人会打包单个结构体,这样做读取-修改-写入的代码比节省的空间还要大得多。你链接的测试是在1亿个元素上进行的。 - stark
3
大多数处理器都有一个“加载字节”指令,其执行时间与“加载整数”相同,因此第二种形式(char,没有位压缩)应始终优于第一种形式(int,没有位压缩)。 - user253751
@NirFriedman 很棒的资源,谢谢。信息实际上从一开始就在那里了,stark可能错过了它,但没关系。 - MaiaVictor
显示剩余9条评论

11

没有一个明确的答案,而且您提供的信息不足以做出“正确”的选择。这是一种权衡。

您说您的“主要目标是时间效率”是不充分的,因为您没有说明I/O时间(例如读取文件数据)是否比计算效率更重要(例如在用户点击“Go”按钮后执行某些计算需要多长时间)。

因此,将数据写入单个字符可能是合适的(以减少读写时间),但要将其解包成四个int数组(以便后续计算更快)。

此外,并不能保证int是32位(您假设第一个打包使用了128位)。int可以是16位。


回答你的问题,时间效率指的是“用户点击Go按钮后计算所需的时间”。该程序是一个黑盒子,您点击播放按钮后它会不断地移动位,直到找到答案。 - MaiaVictor
将数据解包成 int 数组可能不太有用。在使用之前,将单个字段加载到本地临时 int 变量中是一个好主意,除非您需要 8 位整数溢出。 - Peter Cordes

9

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

还有另一种选择:由于值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
}

这与大多数情况下使用位字段并没有太大的不同,但可以极大地减少您的条件逻辑。
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填充结尾。这可以通过一个bitop和0比较来替换多达32个比较。
例如,如果您想在64位变量的所有8组4中设置“1位”,则可以执行uint64_t abcd8 = 0x5555555555555555ULL;,然后设置所有2位,您可以执行abcd8 |= 0xAAAAAAAAAAAAAAAAULL;,使所有值现在都为3。
补充说明: 经过进一步考虑,您可以使用联合作为您的类型,使用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;
  };
};

甚至可以进一步扩展。
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 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指令组合,而使用位域则不会,除非你手动展开它们...在你测试和测量它们之前,没有办法知道答案。

9
适当缓存数据集至关重要。较小的缓存容量总是更好的选择,因为超线程在硬件线程之间竞争性共享每个核心缓存(对于Intel CPU而言)。this answer中的评论包括一些关于缓存未命中成本的数字。
在x86上,将带符号或零扩展的8位值加载到32或64位寄存器中(movzxmovsx)与仅仅将一个字节或32位dword的内容直接移动到寄存器中(mov)速度几乎相同。从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;

位域

将数据打包到位域中会增加一些开销,但仍然可能值得。在一个字节或32/64位的内存块中测试编译时常量的位位置(或多个位)是快速的。如果您实际上需要将某些位域解包成int并将它们传递给非内联函数调用或其他操作,则需要额外的指令进行移位和掩码操作。如果这可以减少缓存未命中的次数,即使只是稍微减少一点,也值得这样做。

测试、设置(为1)或清除(为0)一位或一组位可以通过使用ORAND高效完成,但是将未知的布尔值分配给位域takes more instructions以合并新位和其他字段的位会显著膨胀代码,如果您经常将变量分配给位域,则会更加膨胀。因此,在结构体中使用int foo:6等内容不太可能有帮助,因为您知道foo不需要前两位。如果与单独的字节/短整型/整型相比,您没有节省太多位,则缓存未命中的减少不足以抵消额外指令带来的影响(这些指令会累积成I-cache / uop-cache未命中,以及指令的直接额外延迟和工作)。

x86的BMI1 / BMI2(位操作)指令集扩展将使得将数据从寄存器复制到某些目标位(而不破坏周围位)更加高效。BMI1:Haswell,Piledriver。BMI2:Haswell,Excavator(未发布)。请注意,与SSE / AVX一样,这意味着您需要函数的BMI版本,并为不支持这些指令的CPU提供回退的非BMI版本。据我所知,编译器没有选项来查看这些指令的模式并自动使用它们。它们只能通过内部函数(或asm)使用。

Dbush的回答,将数据打包成位域可能是一个不错的选择,具体取决于你如何使用字段。除非你可以对四个连续的abcd值(类似向量)进行某些有用的操作,否则你第四个选项(将四个独立的abcd值打包到一个结构体中)可能是一个错误。

通用地编写代码,尝试两种方式

对于你的代码广泛使用的数据结构,设置一些东西使你可以从一种实现切换到另一种实现并进行基准测试是有意义的,Nir Friedman的回答,带有getter/setter是一个很好的选择。然而,只使用int临时变量,并将字段作为结构体的单独成员处理应该也能正常工作。对于打包的位域,由编译器生成代码来测试字节的正确位。

如果需要,为SIMD做好准备

如果您有任何检查每个结构体中一个或几个字段的代码,特别是循环遍历连续结构体值的情况,那么cmaster提供的结构数组解决方案将会很有用。x86矢量指令具有最小粒度为一个字节,因此使用每个值占据单独字节的结构数组可以让您快速扫描第一个元素,其中a == something,使用PCMPEQB / PTEST

我仍然不确定我是否理解了分离的abcd推理。即使abcd值很可能一起被访问,这也适用吗?也就是说,如果我访问b,那么我很可能在不久之后读取cd。顺便说一句,回答非常好。 - MaiaVictor
如果在访问b之后通常需要访问cd,那么将它们存储在一起是最好的方法。唯一的例外是如果您按顺序访问它们,并且可以使用向量执行某些聪明的操作(例如,如果您可以执行打包比较以查找c > dFoo)。如果您按顺序访问Foo数组,则无论是一个abcd流还是4个单独的流都无所谓。近期英特尔CPU中的硬件预取器可以跟踪大约10个不同的内存流。 - Peter Cordes
你链接到了我的答案,但是把它称为“dstark的答案”。你指的是哪一个? - dbush
REX前缀将增加指令大小一个字节,但是这样一来,您就可以一次处理两倍数量的值。然而,如果操作可以同时对元素执行多个类似的操作,那么SIMD是更好的选择。AVX2或AVX512将大大增加指令中可以执行的操作数量。 - phuclv
@LưuVĩnhPhúc:我在谈论将单个字段解包到32位(或64位)临时变量。我同意SSE / AVX比SIMD-within-a-(gp)-register更好。 - Peter Cordes

7
首先,精确定义您所说的“最有效”的含义。是最佳内存利用率?还是最佳性能?
然后,以两种方式实施您的算法,并在您打算在实际条件下运行它的实际硬件上进行实际分析。
选择更符合您最初定义的“最有效”的那个。
其他任何事情都只是猜测。无论您选择哪种方法,都可能很好地工作,但如果不在您使用软件的确切条件下实际测量差异,您将永远不知道哪种实现方式会更有效。

我认为可以对某些事情进行相当准确的预测,例如将每个字段分别存储在单独的int8_t中比将每个字段存储在单独的int32_t中要慢。在x86上没有任何开销,其他答案说在ARM上使用8位值是有效的。我认为这并没有真正回答问题。显然,您可以尝试一切,然后进行基准测试,但有时如果您有充分理由相信某些选项会更慢,那么您可以节省开发时间。 - Peter Cordes
@PeterCordes 编译器在x86上有时仍需要对int8_t进行符号/零扩展,尽管在ARM或其他RISC架构上会发生得更多。 - phuclv
@LưuVĩnhPhúc:在x86上,零/符号扩展作为加载的一部分根本没有任何开销(movzx / movsz的成本与mov加载相同)。只有当您将int8_t局部临时变量用作数组索引或将其用作更宽类型的表达式时,才会产生开销。有关详细信息,请参见我的答案。 - Peter Cordes
@PeterCordes 不是的,在x86上,如果你想在eax上进行算术运算,必须将ax/al扩展为eax。在ARM上,加载将自动扩展它。但例如对于一些代码,如char c = ...; int x = somevalue; y = c + x;,则在这两个平台上都需要对c进行符号扩展,如果c不是从内存中加载而是某个中间表达式的结果。 - phuclv
@LưuVĩnhPhúc:没错。不要使用8位本地临时变量(除非你想用它们来处理8位整数溢出)。只在RAM中使用8位值。你的例子是在表达式中使用char类型与更宽类型的一种情况,这就是我说x86 有开销的情况之一。如果c是从char字段加载的int类型(因为编译器会使用movsx而不是mov,没有额外的成本),你就不会有这个开销。我在我的答案中详细解释了这一点。(除非我表述不清楚。如果我的回答可以改进,请告诉我。) - Peter Cordes

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而不是返回引用的函数,这更加复杂。


4

大规模数组和内存不足错误

  1. 整个程序由数十亿个“Foos”组成的大数组组成;

首先,对于#2,如果跨越数GB,则您可能会发现自己或用户(如果其他人运行软件)经常无法成功分配此数组。这里的一个常见错误是认为内存不足错误意味着“没有更多内存可用”,而它们实际上经常意味着操作系统找不到匹配所请求的内存大小的未使用页面的连续集。因此,人们经常感到困惑,例如他们请求分配1 GB块,尽管有30 GB物理内存可用,但它仍然失败。一旦开始分配跨度超过可用典型内存量的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形式中。

数据“消耗”

特别是在这种规模下,即使是你的内存访问是顺序的,按照数据“消耗”(机器加载数据、进行必要的算术运算并存储结果的速度)思考也很有帮助。一个我觉得有用的简单想象图像是将计算机想象成“大嘴巴”。如果我们每次喂给它足够大的一勺数据,而不是小小的茶匙,并且将更多相关的数据紧密地打包到一勺中,它就会更快。

饥饿的计算机

热/冷字段拆分

到目前为止,上述代码假设所有这些字段都是同样“热”的(经常访问),并且一起访问。您可能有一些“冷”字段或仅在成对的关键代码路径中访问的字段。假设您很少访问cd,或者您的代码具有一个关键循环,访问ab,另一个循环访问cd。在这种情况下,将其拆分为两个结构可能会很有帮助:

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

如果我们向计算机“喂”数据,而我们的代码目前只对ab字段感兴趣,那么如果我们有仅包含ab字段的连续块,而不包含cd字段,则可以将更多的内容打包进ab字段的一小部分中。在这种情况下,cd字段是计算机暂时无法处理的数据,但会混合到ab字段之间的内存区域中。如果我们希望计算机尽可能快地消耗数据,我们应该只向其提供目前感兴趣的相关数据,因此在这些情况下拆分结构是值得的。

逐行访问的SIMD SoA

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

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

…需要特别注意对齐和填充(对于AVX,大小/对齐应为16或32字节的倍数,甚至对于未来的AVX-512也是64字节),以便使用更快的对齐移动到XMM/YMM寄存器(将来可能使用AVX指令)。

随机/多字段访问的AoSoA

不幸的是,如果经常同时访问ab,尤其是在随机访问模式下,上述表示法往往会失去很多潜在好处。在这种情况下,更优化的表示方法如下:

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

现在我们正在聚合这个结构,使ab字段不再相互分散,允许32个ab字段组合成一个64字节的缓存行,并快速访问。现在,我们还可以将128或256个ab元素装入XMM/YMM寄存器。

性能分析

通常情况下,在性能问题中我会避免给出一般性的建议,但是我注意到这个问题似乎缺少了性能分析工具提供的详细信息。如果已经在积极使用分析工具,那么请忽略这一部分,否则我会为您介绍一些基础知识。

作为一个例子,我经常能够优化比我更懂计算机体系结构的人编写的生产代码(我曾与许多来自打孔卡时代的人们共事,他们可以轻松看懂汇编代码),并常常被叫来优化他们的代码(这感觉非常奇怪)。原因很简单:我“作弊”地使用了分析工具(VTune),而我的同事们经常不用(他们对此过敏,并认为他们了解热点问题就像分析工具一样,并且认为使用分析工具是浪费时间)。

当然,最理想的情况是找到既有计算机体系结构专业知识又懂得如何使用分析工具的人,但是如果缺乏其中之一,分析工具可以给我们带来更大的优势。优化仍然需要基于最有效的优先级排序的生产力思维模式,而最有效的优先级排序是优化那些真正最重要的部分。分析工具可以详细地解析花费了多少时间以及在哪里花费的时间,还可以提供有用的指标,如缓存未命中和分支预测错误。即使是最高级别的人通常也无法像分析工具一样准确地预测这些指标。此外,性能分析往往是发现计算机体系结构如何更快地运行的关键,通过追踪热点并研究它们为什么存在,加快了对计算机体系结构的理解。对我来说,性能分析是更好地理解计算机体系结构实际工作方式的终极入口,而不是我想象的工作方式。只有这样,像Mysticial这样经验丰富的人的写作才会变得越来越清晰明了。

接口设计

这里可能开始显现出许多优化的可能性。这种问题的答案将是关于策略而不是绝对方法的。在试用某些方法并在需要时迭代向更优解决方案的过程中,还有许多东西需要后来者去发现和改进。

在复杂的代码库中,一个困难就是要为接口留出足够的空间来进行实验和尝试不同的优化技术,以便不断迭代向更快的解决方案。如果接口留出了这些优化的空间,则即使有试错心态,我们也可以整天进行优化,并通过正确的测量手段获得

通常,在实现中留出足够的空间来尝试和探索更快的技术,往往需要接口设计能够批量接收数据。特别是当接口涉及间接函数调用(例如通过dylib或函数指针)时,内联不再是有效的可能性。在这种情况下,为了避免接口断裂,留出优化空间通常意味着设计远离接收简单标量参数的思维模式,而是选择传递整个数据块的指针(如果存在各种交错可能性,则可能带有步幅)。因此,虽然这涉及到一个相当广泛的领域,但在优化方面的许多重点都将归结为留出足够的空间来优化实现,而不会在整个代码库中引起级联更改,并随时准备好使用分析器指导您正确的方向。
总之,这些策略中的一些应该有助于指导您正确的方向。这里没有绝对的东西,只有指南和要尝试的事情,并且最好在手头拥有分析器的情况下完成。然而,当处理如此巨大规模的数据时,始终值得记住饥饿的怪物形象,以及如何以最有效的方式向其提供适当大小和包装的相关数据。

1
写得非常好,抽象而简洁 - 我希望所有的SO答案都能这么好。谢谢Ike! - J.J
1
我现在才注意到这个答案。谢谢。 - MaiaVictor

4

使用int进行编码。

将字段视为int类型。

在您的所有代码中,除了声明之外,将使用blah.x。大多数情况都可以通过整数提升来解决。

完成后,有3个等效的包含文件:一个使用int,一个使用char和一个使用位域。

然后进行性能分析。不必担心这个阶段,因为这是过早的优化,只有您选择的包含文件会发生变化。


3
您已经提到了常见且含糊不清的C/C++标签。
假设是C ++,请将数据设置为私有并添加getter / setter。 不,这不会造成性能损失 - 前提是优化器已打开。
然后,您可以更改实现以使用替代方法,而无需更改调用代码 - 因此更容易根据bench测试结果微调实现。
就记录而言,根据@dbush的描述,我预计具有位字段的结构体最可能是最快的。
请注意,所有这些都围绕着保持数据在高速缓存中 - 您还可以查看调用算法的设计是否有助于解决这个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接