【问题标题】:Bit fields: Set vs test-and-set (for performance)位域:设置与测试和设置(用于性能)
【发布时间】:2009-06-08 11:50:14
【问题描述】:

我有大量这样的 C 结构实例:

struct mystruct
{
    /* ... */
    unsigned flag: 1;
    /* ... */
};
  • flag 最初为 0,但在退出某个函数时必须为 1。

最简单的实现是:

void set_flag(struct mystruct *sp)
{
    sp->flag = 1U;
}

但是这样做对性能的可能影响是什么:

void set_flag(struct mystruct *sp)
{
    if (sp->flag == 0U)
    {
        sp->flag = 1U;
    }
}

我希望避免写入主内存。第一个版本总是进行写操作,第二个版本只有在未设置标志时才执行写操作,但在绝大多数情况下,标志已经被设置了。

还有哪些其他因素(例如分支预测)可能会影响性能?

到目前为止,我已经看到了速度的小幅提升,我希望随着数据集变得更大,它会变得更加显着。

对于大型数据集,此更改是否存在使程序变慢的风险,如果是,在什么情况下可能会发生这种情况?

【问题讨论】:

  • 只是出于兴趣,您对上述内容的最终结论是什么?

标签: c optimization bit-fields


【解决方案1】:

设置前的测试确实会有所不同,但效果如何取决于您的用例。

在任何一种情况下(例如,仅写入或测试和设置),数据都将在缓存行中结束。

但是,如果您的缓存行被标记为脏(例如已修改)或干净,则会有所不同。脏的缓存行必须写回主存,而干净的缓存行可以被遗忘并填充新数据。

现在考虑一下,您的代码会破坏大量数据,并且您只访问每个数据块一次或两次。如果是这样可以假设大多数内存访问是缓存未命中。如果在发生缓存未命中且大多数缓存行都脏的时候,大部分缓存行都是脏的,会发生什么?

在将新数据加载到行之前,必须将它们写回主存储器。这比忘记缓存行的内容要慢。此外,它还将使缓存和主内存之间的内存带宽增加一倍。

这对于一个 CPU 内核来说可能没有什么不同,因为现在内存很快,但是另一个 CPU(希望)也会做一些其他的工作。如果总线不忙于将缓存线移入和移出,您可以确定其他 CPU 内核执行所有操作的速度会快一点。

简而言之:保持缓存行干净将使带宽需求减半,并使缓存未命中更便宜。

关于分支:当然:代价高昂,但缓存未命中更糟糕!此外,如果你很幸运,CPU 将使用它的乱序执行功能来抵消缓存未命中与分支的成本。

如果您真的想从这段代码中获得最佳性能,并且如果您的大多数访问都是缓存未命中,那么您有两种选择:

  • 绕过缓存:x86 架构为此目的提供非临时加载和存储。它们隐藏在 SSE 指令集中的某个地方,可以通过内部函数从 c 语言中使用。

  • (仅供专家使用):使用内联汇编程序的某些行将测试和设置函数替换为使用 CMOV(条件移动)指令的汇编程序。这不仅可以保持缓存行干净,而且可以避免分支。现在 CMOV 是一条慢指令,只有在无法预测分支时才会优于分支。所以你会更好地对你的代码进行基准测试。

【讨论】:

  • 这是迄今为止唯一能真正理解缓存如何工作的答案。好样的,尼尔斯!如果 OP 恰好在 IA64 或 ARM 上,那么编译器可以并且希望将使用谓词指令进行存储,而不是分支,因此根本不会出现分支预测问题。
【解决方案2】:

这是一个有趣的问题,Nils 关于缓存行的回答绝对是很好的建议。

我想强调分析代码以衡量实际性能的重要性——您能衡量一下该标志在您遇到的数据中设置的频率吗?根据答案,性能可能会发生很大变化。

只是为了好玩,我使用您的代码在一个包含不同比例的 1 的 5000 万个元素数组上运行了 set 与 test-then-set 的小比较。这是一张图表:


(来源:natekohl.net

当然,这只是一个玩具示例。但请注意非线性性能——这是我没有预料到的——当数组几乎完全被 1 填充时,测试然后设置变得比普通设置更快。

【讨论】:

    【解决方案3】:

    这些是我对您的要求的解释,

    • 你已经单独初始化了标志
    • 它只设置一次(为 1),之后不会重置
    • 但是,这组尝试将在同一个标​​志上进行多次
    • 而且,您有很多这些标志实例(每个都需要相同类型的处理)

    假设,

    • 空间优化的权重远低于时间优化,

    我建议以下几点。

    • 首先,在 32 位系统上,如果您担心访问时间,使用 32 位整数会有所帮助
    • 如果您跳过对标志“字”的检查,写入速度会非常快。但是,鉴于您有大量的标志,如果尚未设置,您将继续检查和设置,最好保持条件检查。
    • 但是,话虽如此,如果您的平台执行并行操作(例如,磁盘写入通常可以与您的代码执行并行发送),那么跳过检查是值得的。

    【讨论】:

      【解决方案4】:

      当移动到更大的数据集时,这种优化可能不会导致速度下降。

      读取值时的缓存抖动将相同,分支预测惩罚也将相同,这些是此处优化的关键因素。

      分支预测存储每个分支指令的历史记录,因此只要您使用不同地址的指令(例如内联函数)对它们进行分支,它就不会关心您有多少实例。如果你有一个单一的功能实体(不是内联的),你将有一个所有的分支指令,这将抑制分支预测,使其更频繁地错过并增加惩罚。

      【讨论】:

      • 你确定分支预测惩罚是一样的吗?如果引入了很多短期实例,则需要更频繁地更新和刷新标志,这就是我所关心的。
      【解决方案5】:

      你总是可以配置文件,但我很确定第一个版本更快且不那么晦涩。

      【讨论】:

        【解决方案6】:

        这两种方法都需要将数据加载到缓存中,因此您唯一的节省就是读/写和写之间的区别。

        我看不出此更改如何使您的代码在使用更大的数据集时变慢,因此您在这方面可能足够安全。

        这对我来说有点像过早的乐观。 (除非您的分析已将此确定为瓶颈)

        与所有与性能相关的事情一样,确定代码更改效果的最佳方法是对其进行测量。您应该能够相对轻松地创建大量测试数据。

        【讨论】:

        • 是的,这个功能绝对是个瓶颈。
        【解决方案7】:

        如果您真的担心时间性能,请将标志更改为完整的 int 而不是位域。然后设置它只是一个写而不是像位域那样的读写。

        但正如已经指出的那样,这有点微优化的味道。

        【讨论】:

        • 不管是什么类型,它仍然需要加载到缓存/寄存器中才能对其进行任何操作
        • @Glen:向内存地址写入 1 不需要知道之前有什么。位域的问题是单个位不可寻址。代码需要先读取byte/short/int/whatever,修改后再写回,确保同一地址的其他位不受影响。
        • @laalto,如果我理解正确的话,单词也不能在主内存中单独寻址。您必须读取、修改和写回整个缓存行才能更新标志。这是我想避免的写法。
        【解决方案8】:

        设置前的测试没有任何意义 - 没有测试的代码更干净,也更快。

        附带说明 - 像这样内联函数是有意义的,因为函数调用的开销大于函数体,尽管优化编译器应该不加思索地这样做。

        【讨论】:

        • 你确定吗?如果标志已经设置,我相信首先测试可以避免写回主存储器。这就是我介绍测试的原因。
        • 除非你正在处理一些奇特的架构,否则读取和写入内存的成本是相同的——它通常是同一个 MOV,只是操作数不同,通常需要一个周期。测试将添加另一个 MOV 和条件跳转,因此您最终会得到 2 个(测试阳性)或 3 个(测试阴性)指令而不是一个。
        • 即使在普通硬件上,也存在读取比写入快得多的情况。假设在多核 x86 机器上,许多线程同时(在不同的内核上)运行短循环,在每次重复时,检查是否设置了易失性“退出”标志。如果没有代码写入该标志,则该标志可以保存在高速缓存中。另一方面,每次一个处理器写入标志时,即使写入的值已经存在,每个其他线程都必须从缓存中重新加载它。不必要的写入的代价可能是读取成本的 20 倍。
        【解决方案9】:

        既然没人说,那我就说吧。

        你为什么要使用位域?布局因编译器而异,因此它们对接口无用。它们可能会或可能不会更节省空间;编译器可能只是决定将它们推入一个 32 位字段,以便有效地填充事物。不能保证它们会更快,实际上它们可能会更慢。

        我已禁止在工作中使用它们。除非有人能给我一个令人信服的理由,说明他们提供了任何额外的能力,否则不值得和他们一起玩。

        【讨论】:

        • 位域有很多充分的理由。在这张海报的情况下,可以使用一个位域来更好地利用内存或内存带宽。将更多信息放在一个单词中可能意味着更少的往返主内存。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-08-01
        • 2015-02-02
        • 1970-01-01
        • 1970-01-01
        • 2014-06-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多