位字段:设置与测试和设置(用于性能)

问题描述 投票:6回答:9

我有大量的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
9个回答
10
投票

设置之前的测试确实有所不同,但它取决于您的用例。

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

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

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

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

这对于曾经的CPU核心来说可能没有什么区别,因为这些天内存很快,但另一个CPU(希望)也会做其他工作。如果总线没有忙于移入和移出缓存线,您可以确定其他CPU核心将执行更快的速度。

简而言之:保持缓存行清洁将使带宽需求减半,并使缓存失误更便宜。

关于分支:当然:这是昂贵的,但缓存失误更糟糕!此外,如果您很幸运,CPU将使用它的乱序执行功能来抵消缓存未命中以及分支的成本。

如果你真的希望从这段代码中获得最佳性能,并且如果你的大部分访问都是缓存未命中,你有两个选择:

  • 绕过缓存:x86体系结构具有非临时加载和存储用于此目的。它们隐藏在SSE指令集的某个地方,可以通过内在函数从c语言中使用。
  • (仅适用于专家):使用一些内联汇编程序行代替使用CMOV(条件移动)指令的汇编程序替换测试和设置函数。这不仅可以保持缓存行清洁,还可以避免分支。现在CMOV是一个慢速指令,如果无法预测分支,它只会胜过分支。因此,您将更好地对代码进行基准测试。

3
投票

这是一个有趣的问题,Nils关于缓存行的答案肯定是很好的建议。

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

只是为了好玩,我使用你的代码在5000万元素阵列上运行set和then-set的一些比较,这个阵列填充了不同比例的1。这是一张图:

comparison of set vs. test-then-set (来源:natekohl.net

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


2
投票

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

  • 你有单独初始化的标志
  • 它只设置一次(到1),之后不复位
  • 但是,这一集合尝试将在同一旗帜上进行多次
  • 而且,你有很多这些标志实例(每个都需要相同的处理)

假如说,

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

我建议做以下事情。

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

1
投票

移动到更大的数据集时,此优化可能不会导致速度降低。

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

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


0
投票

您可以随时进行配置,但我非常确定第一个版本更快且更不明显。


0
投票

这两种方法都要求将数据加载到缓存中,因此您的唯一保存将是读/写和写入之间的差异。

我没有看到这种变化如何通过更大的数据集使你的代码更慢,所以你可能在这方面足够安全。

它闻起来有点像过早优化我。 (除非您的分析已将此识别为瓶颈)

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


0
投票

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

但正如已经指出的那样,这种微观优化的气味。


0
投票

设置之前的测试没有任何意义 - 没有测试的代码更清晰,也更快一些。

作为旁注 - 这样内联函数是有意义的,因为函数调用的开销比函数体大,尽管优化编译器应该没有经过深思熟虑。


0
投票

由于没有其他人说,我会的。

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

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

© www.soinside.com 2019 - 2024. All rights reserved.