是的,这绝对存在明智的情况,正如您所建议的那样,易失性变量就是其中一种情况 - 即使对于单线程访问也是如此!
从硬件和编译器/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情况下可能无效)。AtomicIntegerArray
sun.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