在写入该值之前检查变量是否包含特定值是否合理?

cia*_*mej 25 c++ java optimization performance caching

if (var != X)
  var = X;
Run Code Online (Sandbox Code Playgroud)

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

如果var是volatile变量怎么办?

我对C++和Java答案感兴趣,因为volatile变量在两种语言中都有不同的语义.Java的JIT编译也可以带来改变.

如果我们总是用X覆盖var,那么if语句会引入分支和附加读取,所以它很糟糕.另一方面,如果var == X然后使用此优化,我们只执行读取,并且我们不执行写入,这可能会对缓存产生一些影响.显然,这里有一些权衡.我想知道它在实践中的样子.有没有人对此做过任何测试?

编辑:

我最感兴趣的是它在多处理器环境中的样子.在一个微不足道的情况下,首先检查变量似乎没有多大意义.但是,当处理器/核心之间必须保持高速缓存一致性时,额外检查可能实际上是有益的.我只是想知道它会产生多大的影响?处理器本身也不应该做这样的优化吗?如果var == X再次分配它值X不应该"弄脏"缓存.但我们可以依靠这个吗?

Dre*_*ann 9

在写入该值之前检查变量是否包含特定值是否合理?

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

当分配比不平等比较返回更加昂贵时false.

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

**对于"大"的某些定义*

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

这是一个相当安全的"不",大多数问题同时包含"优化"和"始终".

C++标准很少提及优化,但从不要求优化.

如果var是一个volatile变量怎么办?

然后它可能会执行if,虽然volatile没有达到大多数人的假设.


Bee*_*ope 9

是的,肯定有这种情况是明智的,正如你所说的那样,volatile变量就是其中之一 - 即使是单线程访问!

从硬件和编译器/ 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;
            }
        }
Run Code Online (Sandbox Code Playgroud)

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

    benchmark arrayType    ns linear runtime
  CopyNoCheck      SAME   470 =
  CopyNoCheck DIFFERENT   460 =
    CopyCheck      SAME  1378 ===
    CopyCheck DIFFERENT  1856 ====
Run Code Online (Sandbox Code Playgroud)

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

当阵列相同时,检查速度大约慢3倍,慢4倍,然后它们不同.鉴于它是完美的预测,我对支票有多糟糕感到惊讶.我怀疑罪魁祸首主要是JIT - 具有更复杂的循环体,它可能展开的次数更少,而其他优化可能不适用.

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

这是代码:

        // 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);
            }
        }
Run Code Online (Sandbox Code Playgroud)

以下是结果:

arrayType     benchmark    us linear runtime
     SAME   CopyCheckAI  2.85 =======
     SAME CopyNoCheckAI 10.21 ===========================
DIFFERENT   CopyCheckAI 11.33 ==============================
DIFFERENT CopyNoCheckAI 11.19 =============================
Run Code Online (Sandbox Code Playgroud)

桌子已经转过来了.首先检查比通常的方法快3.5倍.一切都慢得多 - 在检查的情况下,我们每循环支付~3 ns,在最坏的情况下〜10 ns(上面的时间在我们身上,覆盖整个1000元素阵列的副本).易失性写入确实更昂贵.在不同的情况下,大约有1 ns的过度包含在每次迭代时重置数组(这就是为什么即使简单对于DIFFERENT来说稍微慢一些).我怀疑"check"案例中的很多开销实际上是边界检查.

这都是单线程的.如果你实际上有一个针对volatile的跨核争用,那么对于简单方法来说结果会更糟,并且对于检查案例来说就差不多了(缓存行只会处于共享状态 - 没有需要一致的交通).

我也只测试了"每个元素相等"与"每个元素不同"的极端情况.这意味着"检查"算法中的分支始终是完美预测的.如果你有相同和不同的混合,你不会只得到相同和不同情况的时间的加权组合 - 你做得更糟,由于误预测(在硬件层面,也许在JIT层面) ,不能再为最常用的分支进行优化).

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


ale*_*der 8

一般来说,答案是否定的.因为如果你有简单的数据类型,编译器就能够执行任何必要的优化.对于具有重型运算符的类型=运算符=的责任是选择分配新值的最佳方式.

  • 进行检查可能有意义的情况是在多处理器环境中大量引用所讨论的目标变量,并且您希望避免不必要地"弄脏"高速缓存行.但是在"正常"编程中很少有理由可行. (6认同)

Voo*_*Voo 5

在某些情况下,即使是一个简单的分配,例如指针化变量也可能比读取和分支更昂贵(特别是如果可预测的话).

为什么?多线程.如果多个线程只读取相同的值,则它们都可以在缓存中共享该值.但是一旦你写入它,就必须使高速缓存行无效并在下次想要读取时获取新值,或者你必须获得更新的值以保持缓存的连贯性.这两种情况都会导致核心之间的流量增加,并增加读取的延迟.

如果分支是非常不可预测的,虽然它可能仍然较慢.