是的,在某些情况下肯定是明智的,并且正如您所建议的那样,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; } }使用Caliper作为我的微基准测试工具,使用上述代码复制数组长度为1000的结果是:
benchmark arrayType ns linear runtime CopyNoCheck SAME 470 = CopyNoCheck DIFFERENT 460 = CopyCheck SAME 1378 === CopyCheck DIFFERENT 1856 ====
每次运行还需要大约150 ns的开销,才能每次重置目标阵列。跳过检查要快得多-每个元素大约0.47 ns(或者在除去设置开销后每个元素大约0.32
ns,所以我的盒子上几乎恰好1个周期)。
当阵列相同时,检查速度要慢大约3倍,而不同时则要慢4倍。鉴于支票被完美预测,我对支票有多糟糕感到惊讶。我怀疑罪魁祸首很大程度上是JIT-
循环主体更加复杂,展开的次数可能更少,并且其他优化可能不适用。
让我们切换到易失性案例。在这里,我用作
AtomicIntegerArray易失性元素的数组,因为Java没有任何带有易失性元素的本机数组类型。在内部,此类仅使用进行直接写入数组
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有跨核心的争用,那么对于简单方法而言,结果将是非常糟糕的得多,并且与上述检查情况一样好(缓存行仅处于共享状态-
否所需的一致性流量)。
我也只测试了“每个元素相等”与“每个元素不同”的极端。这意味着“检查”算法中的分支总是可以完美预测的。如果混合使用相等和不同,那么您将不会仅获得SAME和DIFFERENT情况下时间的加权组合-
由于预测错误(在硬件级别,甚至在JIT级别),您的情况会更糟,无法再针对始终采用的分支进行优化)。
因此,即使对于volatile,它是否明智也取决于特定的上下文-
相等和不相等的值的混合,周围的代码等等。通常我不会在单线程情况下仅针对volatile进行此操作,除非我怀疑大量的集合是多余的。但是,在高度多线程的结构中,读取然后进行易失性写(或其他昂贵的操作,如CAS)是一种最佳做法,您会看到它是诸如
java.util.concurrent结构之类的高质量代码。



