28
if (var != X)
  var = X;

这是明智的还是不明智的?编译器会始终优化 if 语句吗?是否有任何用例可以从 if 语句中受益?

如果var是 volatile 变量怎么办?

我对 C++ 和 Java 的答案都感兴趣,因为 volatile 变量在两种语言中都有不同的语义。Java 的 JIT 编译也可以产生影响。

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

编辑:

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

4

8 回答 8

9

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

从硬件和编译器/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作为我的 microbenchmark 工具的结果是:

    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——循环体复杂得多,展开的次数可能更少,其他优化可能不适用。

让我们切换到 volatile 案例。在这里,我使用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);
            }
        }

结果如下:

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 个元素数组的副本)。易失性写入确实更昂贵。DIFFERENT 案例中包含大约 1 ns 的开销来在每次迭代时重置数组(这就是为什么即使是简单的 DIFFERENT 也会稍微慢一些)。我怀疑“检查”案例中的很多开销实际上是边界检查。

这都是单线程的。如果您实际上在 volatile 上存在跨核争用,那么简单方法的结果会更糟,并且与上面的检查用例一样好(缓存行只会处于共享状态 - 不需要一致性流量)。

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

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

于 2013-04-20T08:07:11.137 回答
9

在写入该值之前检查变量是否具有特定值是一种明智的优化吗?

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

当分配比不等式比较返回的成本要高得多时false

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

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

编译器会始终优化 if 语句吗?

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

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

如果 var 是 volatile 变量怎么办?

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

于 2013-04-19T22:55:39.710 回答
8

一般来说,答案是否定的。因为如果您有简单的数据类型,编译器将能够执行任何必要的优化。并且对于重度 operator= 的类型, operator= 负责选择分配新值的最佳方式。

于 2013-04-19T22:51:46.913 回答
5

在某些情况下,即使是对指针大小的变量进行简单的赋值也可能比读取和分支更昂贵(尤其是在可预测的情况下)。

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

如果分支非常不可预测,但它可能仍然更慢。

于 2013-04-20T01:08:44.383 回答
4

在 C++ 中,分配一个 SIMPLE 变量(即,一个普通的整数或浮点变量)肯定并且总是比检查它是否已经具有该值然后在它没有该值时设置它更快。如果这在 Java 中也不是真的,我会感到非常惊讶,但我不知道 Java 中的事情有多复杂或简单——我已经写了几百行,实际上并没有研究字节码和 JITed 字节码是如何实现的作品。

显然,如果变量很容易检查,但设置起来很复杂,比如类和其他类似的东西,那么可能会有一个值。您会发现这种情况的典型情况是在某些“值”是某种索引或哈希的代码中,但如果它不匹配,则需要大量工作。一个示例是在任务切换中:

if (current_process != new_process_to_run)
     current_process == new_process_to_run; 

因为在这里,“进程”是一个复杂的对象来改变,但!=可以在进程的ID上完成。

无论对象是简单还是复杂,编译器几乎肯定不会理解您在这里尝试做什么,因此它可能不会优化它 - 但编译器有时比您想象的更聪明,有时更愚蠢,所以我不会打赌。

volatile应该始终强制编译器读取和写入变量的值,无论它“认为”是否有必要,所以它肯定会读取变量并写入变量。当然,如果变量是volatile它可能意味着它可以改变或代表某些硬件,所以你也应该格外小心自己如何对待它......额外读取 PCI-X 卡可能会导致几个总线周期(总线周期比处理器速度慢一个数量级!),这可能对性能的影响更大。但是然后写入硬件寄存器可能(例如)导致硬件做一些意想不到的事情,并且首先检查我们是否拥有该值可能会使其更快,因为“某些操作重新开始”或类似的东西。

于 2013-04-19T23:07:27.933 回答
1

在 Objective-C 中,您可能会遇到将对象地址分配给指针变量可能需要“保留”对象(增加引用计数)的情况。在这种情况下,查看分配的值是否与指针变量中当前的值相同是有意义的,以避免执行相对昂贵的递增/递减操作。

其他使用引用计数的语言可能也有类似的情况。

但是,当将 anint或 a分配boolean给一个简单的变量(在别处提到的多处理器缓存场景之外)时,测试很少值得。大多数处理器中的存储速度至少与加载/测试/分支一样快。

于 2013-04-19T23:42:33.203 回答
1

如果您涉及读写锁定语义,那将是明智的,只要读取通常比写入更具破坏性。

于 2013-04-19T22:58:52.333 回答
0

In java the answer is always no. All assignments you can do in Java are primitive. In C++, the answer is still pretty much always no - if copying is so much more expensive than an equality check, the class in question should do that equality check itself.

于 2013-04-19T23:03:05.110 回答