在写入变量值之前检查该变量是否包含该值是一种明智的优化?

2022-09-01 14:29:15
if (var != X)
  var = X;

这是否合理?编译器是否总是优化 if 语句?是否有任何用例可以从 if 语句中受益?

如果是易失性变量怎么办?var

我对C++和Java答案都感兴趣,因为可变变量在这两种语言中具有不同的语义。Java的JIT编译也可以有所作为。

if 语句引入了分支和额外的读取,如果我们总是用 X 覆盖 var,就不会发生这种情况,所以这很糟糕。另一方面,如果使用这种优化,我们只执行读取,而不执行写入,这可能会对缓存产生一些影响。显然,这里有一些权衡。我想知道它在实践中是什么样子的。有没有人对此进行过任何测试?var == X

编辑:

我最感兴趣的是它在多处理器环境中的外观。在微不足道的情况下,首先检查变量似乎没有多大意义。但是,当必须在处理器/内核之间保持缓存一致性时,额外的检查实际上可能是有益的。我只是想知道它能产生多大的影响?处理器本身难道不应该进行这样的优化吗?如果再分配一次值,则不应“弄脏”缓存。但是我们能指望这一点吗?var == XX


答案 1

是的,这绝对存在明智的情况,正如您所建议的那样,易失性变量就是其中一种情况 - 即使对于单线程访问也是如此!

从硬件和编译器/JIT 的角度来看,易失性写入都是昂贵的。在硬件级别,这些写入可能比普通写入高 10-100 倍,因为必须刷新写入缓冲区(在 x86 上,详细信息因平台而异)。在编译器/JIT 级别,易失性写入会抑制许多常见的优化。

然而,投机只能让你到目前为止 - 证据总是在基准测试中。这是一个微型基准,可以尝试您的两种策略。基本思想是将值从一个数组复制到另一个数组(几乎是System.arraycopy),具有两个变体 - 一个无条件复制,另一个首先检查值是否不同。

以下是简单、非易失性情况的复制例程(此处为完整源代码):

        // no check
        for (int i=0; i < ARRAY_LENGTH; i++) {
            target[i] = source[i];
        }

        // check, then set if unequal
        for (int i=0; i < ARRAY_LENGTH; i++) {
            int x = source[i];
            if (target[i] != x) {
                target[i] = x;
            }
        }

使用上面的代码复制1000的数组长度,使用Caliper作为我的微板标记线束,结果是:

    benchmark arrayType    ns linear runtime
  CopyNoCheck      SAME   470 =
  CopyNoCheck DIFFERENT   460 =
    CopyCheck      SAME  1378 ===
    CopyCheck DIFFERENT  1856 ====

这还包括每次运行大约 150ns 的开销,以便每次重置目标阵列。跳过检查要快得多 - 每个元素大约0.47 ns(或者在我们消除设置开销后每个元素大约0.32 ns,所以在我的盒子上几乎正好是1个周期)。

当数组相同时,检查速度大约慢 3 倍,并且速度比它们不同慢 4 倍。我对支票的糟糕程度感到惊讶,因为它是完全可以预测的。我怀疑罪魁祸首很大程度上是JIT - 具有更复杂的循环体,它可能被展开的次数更少,并且其他优化可能不适用。

让我们切换到易失性情况。在这里,我使用了易失性元素数组,因为Java没有任何具有易失性元素的本机数组类型。在内部,这个类只是直接写入数组 using ,这允许易失性写入。生成的程序集与正常阵列访问基本相似,除了易失性方面(以及可能的范围检查消除,这在AIA情况下可能无效)。AtomicIntegerArraysun.misc.Unsafe

代码如下:

        // no check
        for (int i=0; i < ARRAY_LENGTH; i++) {
            target.set(i, source[i]);
        }

        // check, then set if unequal
        for (int i=0; i < ARRAY_LENGTH; i++) {
            int x = source[i];
            if (target.get(i) != x) {
                target.set(i, x);
            }
        }

结果如下:

arrayType     benchmark    us linear runtime
     SAME   CopyCheckAI  2.85 =======
     SAME CopyNoCheckAI 10.21 ===========================
DIFFERENT   CopyCheckAI 11.33 ==============================
DIFFERENT CopyNoCheckAI 11.19 =============================

形势已经逆转。首先检查比通常的方法快约3.5倍。总体而言,一切都要慢得多 - 在检查情况下,我们每个循环支付约3 ns,而在最坏的情况下,则为约10 ns(上面的时间在我们身上,并覆盖了整个1000个元素数组的副本)。易失性写入确实更昂贵。在每次迭代时,不同情况下包含大约1 ns的开销来重置数组(这就是为什么即使是简单对于不同来说也会稍微慢一些)。我怀疑“检查”情况下的很多开销实际上是边界检查。

这都是单线程的。如果您确实对易失性进行了跨内核争用,那么对于简单方法来说,结果会更糟,并且与上面的检查情况一样好(缓存行将处于共享状态 - 不需要一致性流量)。

我也只测试了“每个元素相等”与“每个元素不同”的极端情况。这意味着“检查”算法中的分支始终是完美预测的。如果你有一个相等和不同的混合,你不会得到一个相同和不同情况的时间的加权组合 - 由于错误预测(无论是在硬件级别,还是在JIT级别,这不能再针对始终采用的分支进行优化),你做得更糟。

因此,它是否合理,即使对于易失性,也取决于特定的上下文 - 相等和不相等值的混合,周围的代码等。在单线程场景中,我通常不会单独为易失性执行此操作,除非我怀疑大量集合是多余的。然而,在重多线程结构中,读取然后执行易失性写入(或其他昂贵的操作,如CAS)是一种最佳实践,您将看到高质量的代码,例如结构。java.util.concurrent


答案 2

在写入变量值之前检查该变量是否包含该值是一种明智的优化?

是否有任何用例可以从 if 语句中受益?

这是当分配比返回的不等式比较昂贵得多的时候。false

例如,一个大 * ,可能需要许多堆分配来复制。std::set

**对于“大”的某些定义*

编译器是否总是优化 if 语句?

这是一个相当安全的“不”,就像大多数同时包含“优化”和“总是”的问题一样。

C++标准很少提及优化,但从未要求优化。

如果 var 是一个易失性变量呢?

然后它可能会执行,尽管不稳定性不能达到大多数人的假设。if


推荐