在 Java 中,可以比 & & 更快吗?

在这个代码中:

if (value >= x && value <= y) {

value >= xvalue <= y像真和假一样没有特定的模式时,使用 ABC2操作符会比使用 &&

具体来说,我在考虑 &&如何懒惰地计算右侧表达式(即只有在 LHS 为真的情况下) ,这意味着一个条件,而在 Java &中,在这个上下文中保证严格计算两个(布尔)子表达式。这两种方法的值结果是相同的。

但是,虽然 >=<=操作员将使用一个简单的比较指令,&&必须涉及一个分支,和 该分支容易出现分支预测失败-根据这个非常著名的问题: 为什么处理排序的数组比处理未排序的数组更快?

因此,强制表达式没有惰性组件肯定会更具确定性,而且不容易出现预测失败。对吧?

备注:

  • 显然,如果代码看起来像这样,那么我的问题的答案就是 没有: if(value >= x && verySlowFunction())。我专注于“足够简单”的 RHS 表达式。
  • 这里有一个条件分支(if语句)。我不能完全向自己证明这是不相关的,替代配方可能是更好的例子,如 boolean b = value >= x && value <= y;
  • 这一切都落入了可怕的微观优化的世界。是的,我知道: (——) ... ... 不过有趣吗?

更新 解释一下我为什么感兴趣: 我一直在关注马丁 · 汤普森在他的 机械同情博客上写的系统,在他来之后,关于 Aeron 的 谈了谈。其中一个关键信息是我们的硬件拥有所有这些神奇的东西,而我们软件开发人员悲剧性地未能利用它。别担心,我不会对我所有的代码都使用 s/& &/&/: ... ... 但是在这个网站上有很多关于通过删除分支来改进分支预测的问题,而且我突然想到,条件布尔运算符是测试条件的 在核心

当然,@StephenC 提出了一个奇妙的观点,即将代码弯曲成奇怪的形状可以使 JIT 不那么容易发现常见的优化——如果不是现在,那么将来也是如此。上面提到的非常著名的问题是特殊的,因为它使预测的复杂性远远超出了实际的优化。

我非常清楚,在大多数情况下(或 几乎全部) ,&&是最清晰,最简单,最快,最好的事情做-虽然我非常感谢的人谁张贴的答案证明了这一点!我真的很感兴趣,看看在任何人的经验中是否真的存在这样的情况: “ &能更快吗?”可能是 是的

更新2 : (针对问题过于宽泛的建议。我不想对这个问题做重大修改,因为它可能会损害下面的一些答案,这些答案的质量非常好!)也许我们需要一个野外的例子,它来自于 番石榴朗马斯类(非常感谢@maaartinus 找到它) :

public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}

看到第一个 &了吗?如果你检查链接,下一个方法被称为 lessThanBranchFree(...),这暗示我们在避免分支的领域-和番石榴真正被广泛使用: 每节省周期导致海平面明显下降。那么让我们这样来回答这个问题: 这种使用 ABC0(其中 &&将是更正常的)是一个真正的优化?

9347 次浏览

使用 &&&仍然需要求值一个条件,因此不太可能节省任何处理时间——考虑到只需求值一个表达式就可以求值两个表达式,这甚至可能会增加处理时间。

如果在某些非常罕见的情况下,使用 & over &&可以节省一纳秒,那么您已经浪费了比使用 & over &&可以节省的更多时间来考虑这种差异。

剪辑

我很好奇,决定做一些基准测试。

我选了这门课:

public class Main {


static int x = 22, y = 48;


public static void main(String[] args) {
runWithOneAnd(30);
runWithTwoAnds(30);
}


static void runWithOneAnd(int value){
if(value >= x & value <= y){


}
}


static void runWithTwoAnds(int value){
if(value >= x && value <= y){


}
}
}

用 NetBeans 做了一些侧写测试。我没有使用任何打印语句来节省处理时间,只是知道两者的计算结果都是 true

第一个测试:

The first profiling test

第二个测试:

The second profiling test

第三个测试:

The third profiling test

从分析测试中可以看出,与使用两个 &&相比,仅使用一个 &实际运行时间要长2-3倍。这确实有些奇怪,因为我确实期望从只有一个 &更好的性能。

我不知道为什么。在这两种情况下,都必须对这两个表达式求值,因为它们都为 true。我怀疑 JVM 在后台进行了一些特殊的优化,以提高速度。

这个故事的寓意是: 传统是好的,过早的优化是坏的。


编辑2

考虑到@SvetlinZarev 的注释和其他一些改进,我重写了基准测试代码。下面是修改后的基准代码:

public class Main {


static int x = 22, y = 48;


public static void main(String[] args) {
oneAndBothTrue();
oneAndOneTrue();
oneAndBothFalse();
twoAndsBothTrue();
twoAndsOneTrue();
twoAndsBothFalse();
System.out.println(b);
}


static void oneAndBothTrue() {
int value = 30;
for (int i = 0; i < 2000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}


static void oneAndOneTrue() {
int value = 60;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}


static void oneAndBothFalse() {
int value = 100;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}


static void twoAndsBothTrue() {
int value = 30;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}


static void twoAndsOneTrue() {
int value = 60;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}


static void twoAndsBothFalse() {
int value = 100;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}


//I wanted to avoid print statements here as they can
//affect the benchmark results.
static StringBuilder b = new StringBuilder();
static int times = 0;


static void doSomething(){
times++;
b.append("I have run ").append(times).append(" times \n");
}
}

以下是性能测试:

测试一:

enter image description here

测试2:

enter image description here

测试3:

enter image description here

这也考虑到了不同的值和不同的条件。

当两个条件都为真时,使用一个 &需要更多的时间运行,大约多60% 或2毫秒。当其中一个或两个条件都为假时,那么一个 &运行得更快,但它只运行快大约0.30 -0.50毫秒。因此,在大多数情况下,&将比 &&运行得更快,但性能差异仍然可以忽略不计。

你想要的是这样的东西:

x <= value & value <= y
value - x >= 0 & y - value >= 0
((value - x) | (y - value)) >= 0  // integer bit-or

有趣的是,人们几乎想看看字节码。 但很难说,我希望这是个 C 题。

我对这个问题的答案也很好奇,所以我为此编写了以下(简单)测试:

private static final int max = 80000;
private static final int size = 100000;
private static final int x = 1500;
private static final int y = 15000;
private Random random;


@Before
public void setUp() {
this.random = new Random();
}


@After
public void tearDown() {
random = null;
}


@Test
public void testSingleOperand() {
int counter = 0;
int[] numbers = new int[size];
for (int j = 0; j < size; j++) {
numbers[j] = random.nextInt(max);
}


long start = System.nanoTime(); //start measuring after an array has been filled
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] >= x & numbers[i] <= y) {
counter++;
}
}
long end = System.nanoTime();
System.out.println("Duration of single operand: " + (end - start));
}


@Test
public void testDoubleOperand() {
int counter = 0;
int[] numbers = new int[size];
for (int j = 0; j < size; j++) {
numbers[j] = random.nextInt(max);
}


long start = System.nanoTime(); //start measuring after an array has been filled
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] >= x & numbers[i] <= y) {
counter++;
}
}
long end = System.nanoTime();
System.out.println("Duration of double operand: " + (end - start));
}

最终的结果是,与 & & 的比较总是在速度方面获胜,比 & 快1.5/2毫秒。

编辑: 正如@SvetlinZarev 指出的,我也在测量 Random 得到一个整数所花的时间。更改为使用预填充的随机数数组,这导致单个操作数测试的持续时间剧烈波动; 几次运行之间的差异高达6-7毫秒。

对于这类问题,您应该运行一个微基准测试。

基准的实现方式如下

// boolean logical AND
bh.consume(value >= x & y <= value);

还有

// conditional AND
bh.consume(value >= x && y <= value);

还有

// bitwise OR, as suggested by Joop Eggen
bh.consume(((value - x) | (y - value)) >= 0)

根据基准名称为 value, x and y设置值。

吞吐量基准测试的结果(5次预热和10次测量迭代)是:

Benchmark                                 Mode  Cnt    Score    Error   Units
Benchmark.isBooleanANDBelowRange          thrpt   10  386.086 ▒ 17.383  ops/us
Benchmark.isBooleanANDInRange             thrpt   10  387.240 ▒  7.657  ops/us
Benchmark.isBooleanANDOverRange           thrpt   10  381.847 ▒ 15.295  ops/us
Benchmark.isBitwiseORBelowRange           thrpt   10  384.877 ▒ 11.766  ops/us
Benchmark.isBitwiseORInRange              thrpt   10  380.743 ▒ 15.042  ops/us
Benchmark.isBitwiseOROverRange            thrpt   10  383.524 ▒ 16.911  ops/us
Benchmark.isConditionalANDBelowRange      thrpt   10  385.190 ▒ 19.600  ops/us
Benchmark.isConditionalANDInRange         thrpt   10  384.094 ▒ 15.417  ops/us
Benchmark.isConditionalANDOverRange       thrpt   10  380.913 ▒  5.537  ops/us

对于评估本身来说,结果并没有那么不同。只要没有发现对代码片段的性能影响,我就不会尝试对其进行优化。根据代码中的位置,热点编译器可能决定进行一些优化。这可能不包括在上述基准中。

参考文献:

布尔逻辑 AND -如果两个操作数值都是 true,则结果值为 true; 否则,结果为 false
条件 AND -类似于 &,但是只有当它的左边操作数的值为 true时才计算它的右边操作数
按位 OR -结果值是操作数值的按位包含 OR

好的,那么你想知道它在底层是如何工作的... ... 那么让我们来看看字节码吧!

编辑: 最后添加了为 AMD64生成的汇编代码。查看一些有趣的注释。
编辑2(重新: OP 的“更新2”) : 为 番石榴 isPowerOfTwo添加了高斯代码以及。

Java 源代码

我写了这两个快速方法:

public boolean AndSC(int x, int value, int y) {
return value >= x && value <= y;
}


public boolean AndNonSC(int x, int value, int y) {
return value >= x & value <= y;
}

如您所见,除了 AND 操作符的类型之外,它们完全相同。

Java 字节码

这是生成的字节码:

  public AndSC(III)Z
L0
LINENUMBER 8 L0
ILOAD 2
ILOAD 1
IF_ICMPLT L1
ILOAD 2
ILOAD 3
IF_ICMPGT L1
L2
LINENUMBER 9 L2
ICONST_1
IRETURN
L1
LINENUMBER 11 L1
FRAME SAME
ICONST_0
IRETURN
L3
LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L3 0
LOCALVARIABLE x I L0 L3 1
LOCALVARIABLE value I L0 L3 2
LOCALVARIABLE y I L0 L3 3
MAXSTACK = 2
MAXLOCALS = 4


// access flags 0x1
public AndNonSC(III)Z
L0
LINENUMBER 15 L0
ILOAD 2
ILOAD 1
IF_ICMPLT L1
ICONST_1
GOTO L2
L1
FRAME SAME
ICONST_0
L2
FRAME SAME1 I
ILOAD 2
ILOAD 3
IF_ICMPGT L3
ICONST_1
GOTO L4
L3
FRAME SAME1 I
ICONST_0
L4
FRAME FULL [test/lsoto/AndTest I I I] [I I]
IAND
IFEQ L5
L6
LINENUMBER 16 L6
ICONST_1
IRETURN
L5
LINENUMBER 18 L5
FRAME SAME
ICONST_0
IRETURN
L7
LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L7 0
LOCALVARIABLE x I L0 L7 1
LOCALVARIABLE value I L0 L7 2
LOCALVARIABLE y I L0 L7 3
MAXSTACK = 3
MAXLOCALS = 4

AndSC(&&)方法按照预期生成 条件跳转:

  1. 它将 valuex加载到堆栈上,如果 value较低,则跳转到 L1。否则它会继续运行下一行。
  2. 它将 valuey加载到堆栈上,如果 value更大,则跳转到 L1。否则它会继续运行下一行。
  3. 碰巧是 return true以防两次跳跃都没有发生。
  4. 然后我们有标记为 L1的线,它是 return false

然而,AndNonSC(&)方法生成 条件跳转!

  1. 它将 valuex加载到堆栈上,如果 value较低,则跳转到 L1。因为现在它需要保存结果来与 AND 的其他部分进行比较,所以它必须执行“ save true”或“ save false”,所以它不能同时执行这两个指令。
  2. 它将 valuey加载到堆栈上,如果 value更大,则跳转到 L1。再一次,它需要保存 truefalse,这是两个不同的行,这取决于比较的结果。
  3. 现在完成了 都有比较,代码实际执行 AND 操作——如果两个操作都为真,则跳转(第三次)返回 true; 否则继续执行到下一行返回 false。

(初步)结论

虽然我对 Java 字节码没有多少经验,我可能忽略了一些东西,但在我看来,&在每种情况下实际上都会执行 更糟而不是 &&: 它会生成更多的指令来执行,包括更多的条件跳转来预测,可能会失败。

正如有人提议的那样,重写代码以用算术运算取代比较,可能是使 &成为更好选择的一种方法,但代价是代码变得不那么清晰。
恕我直言,99% 的场景都不值得这么麻烦(不过,对于需要极其优化的1% 循环来说,这可能非常值得)。

编辑: AMD64总成

正如评论中提到的,相同的 Java 字节码可能会导致不同系统中的不同机器代码,所以尽管 Java 字节码可能会给我们提示哪个 AND 版本执行得更好,但得到编译器生成的实际 ASM 才是真正找到答案的唯一方法。
我打印了这两种方法的 AMD64 ASM 指令; 下面是相关的行(剥离入口点等)。

注意: 除非另有说明,否则使用 java 1.8.0 _ 91编译的所有方法。

使用默认选项的 AndSC方法

  # {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest'
...
0x0000000002923e3e: cmp    %r8d,%r9d
0x0000000002923e41: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
0x0000000002923e4b: movabs $0x108,%rsi
0x0000000002923e55: jl     0x0000000002923e65
0x0000000002923e5b: movabs $0x118,%rsi
0x0000000002923e65: mov    (%rax,%rsi,1),%rbx
0x0000000002923e69: lea    0x1(%rbx),%rbx
0x0000000002923e6d: mov    %rbx,(%rax,%rsi,1)
0x0000000002923e71: jl     0x0000000002923eb0  ;*if_icmplt
; - AndTest::AndSC@2 (line 22)


0x0000000002923e77: cmp    %edi,%r9d
0x0000000002923e7a: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
0x0000000002923e84: movabs $0x128,%rsi
0x0000000002923e8e: jg     0x0000000002923e9e
0x0000000002923e94: movabs $0x138,%rsi
0x0000000002923e9e: mov    (%rax,%rsi,1),%rdi
0x0000000002923ea2: lea    0x1(%rdi),%rdi
0x0000000002923ea6: mov    %rdi,(%rax,%rsi,1)
0x0000000002923eaa: jle    0x0000000002923ec1  ;*if_icmpgt
; - AndTest::AndSC@7 (line 22)


0x0000000002923eb0: mov    $0x0,%eax
0x0000000002923eb5: add    $0x30,%rsp
0x0000000002923eb9: pop    %rbp
0x0000000002923eba: test   %eax,-0x1c73dc0(%rip)        # 0x0000000000cb0100
;   {poll_return}
0x0000000002923ec0: retq                      ;*ireturn
; - AndTest::AndSC@13 (line 25)


0x0000000002923ec1: mov    $0x1,%eax
0x0000000002923ec6: add    $0x30,%rsp
0x0000000002923eca: pop    %rbp
0x0000000002923ecb: test   %eax,-0x1c73dd1(%rip)        # 0x0000000000cb0100
;   {poll_return}
0x0000000002923ed1: retq

使用 -XX:PrintAssemblyOptions=intel选项的方法 AndSC

  # {method} {0x00000000170a0810} 'AndSC' '(III)Z' in 'AndTest'
...
0x0000000002c26e2c: cmp    r9d,r8d
0x0000000002c26e2f: jl     0x0000000002c26e36  ;*if_icmplt
0x0000000002c26e31: cmp    r9d,edi
0x0000000002c26e34: jle    0x0000000002c26e44  ;*iconst_0
0x0000000002c26e36: xor    eax,eax            ;*synchronization entry
0x0000000002c26e38: add    rsp,0x10
0x0000000002c26e3c: pop    rbp
0x0000000002c26e3d: test   DWORD PTR [rip+0xffffffffffce91bd],eax        # 0x0000000002910000
0x0000000002c26e43: ret
0x0000000002c26e44: mov    eax,0x1
0x0000000002c26e49: jmp    0x0000000002c26e38

使用默认选项的 AndNonSC方法

  # {method} {0x0000000016da0908} 'AndNonSC' '(III)Z' in 'AndTest'
...
0x0000000002923a78: cmp    %r8d,%r9d
0x0000000002923a7b: mov    $0x0,%eax
0x0000000002923a80: jl     0x0000000002923a8b
0x0000000002923a86: mov    $0x1,%eax
0x0000000002923a8b: cmp    %edi,%r9d
0x0000000002923a8e: mov    $0x0,%esi
0x0000000002923a93: jg     0x0000000002923a9e
0x0000000002923a99: mov    $0x1,%esi
0x0000000002923a9e: and    %rsi,%rax
0x0000000002923aa1: cmp    $0x0,%eax
0x0000000002923aa4: je     0x0000000002923abb  ;*ifeq
; - AndTest::AndNonSC@21 (line 29)


0x0000000002923aaa: mov    $0x1,%eax
0x0000000002923aaf: add    $0x30,%rsp
0x0000000002923ab3: pop    %rbp
0x0000000002923ab4: test   %eax,-0x1c739ba(%rip)        # 0x0000000000cb0100
;   {poll_return}
0x0000000002923aba: retq                      ;*ireturn
; - AndTest::AndNonSC@25 (line 30)


0x0000000002923abb: mov    $0x0,%eax
0x0000000002923ac0: add    $0x30,%rsp
0x0000000002923ac4: pop    %rbp
0x0000000002923ac5: test   %eax,-0x1c739cb(%rip)        # 0x0000000000cb0100
;   {poll_return}
0x0000000002923acb: retq

使用 -XX:PrintAssemblyOptions=intel选项的方法 AndNonSC

  # {method} {0x00000000170a0908} 'AndNonSC' '(III)Z' in 'AndTest'
...
0x0000000002c270b5: cmp    r9d,r8d
0x0000000002c270b8: jl     0x0000000002c270df  ;*if_icmplt
0x0000000002c270ba: mov    r8d,0x1            ;*iload_2
0x0000000002c270c0: cmp    r9d,edi
0x0000000002c270c3: cmovg  r11d,r10d
0x0000000002c270c7: and    r8d,r11d
0x0000000002c270ca: test   r8d,r8d
0x0000000002c270cd: setne  al
0x0000000002c270d0: movzx  eax,al
0x0000000002c270d3: add    rsp,0x10
0x0000000002c270d7: pop    rbp
0x0000000002c270d8: test   DWORD PTR [rip+0xffffffffffce8f22],eax        # 0x0000000002910000
0x0000000002c270de: ret
0x0000000002c270df: xor    r8d,r8d
0x0000000002c270e2: jmp    0x0000000002c270c0
  • 首先,生成的 ASM 代码根据我们是选择默认的 AT & T 语法还是 Intel 语法而有所不同。
  • 使用 AT & T 语法:
    • ASM 代码实际上是 AndSC方法的 更久,每个字节码 IF_ICMP*转换为两个汇编跳转指令,共有4个条件跳转。
    • 同时,对于 AndNonSC方法,编译器生成一个更直接的代码,其中每个字节码 IF_ICMP*只被翻译成一个汇编跳转指令,保持原来的3个条件跳转的计数。
  • 英特尔语法:
    • AndSC的 ASM 代码更短,只有2个条件跳转(不包括最后的非条件 jmp)。实际上它只是两个 CMP,两个 JL/E 和一个 XOR/MOV 取决于结果。
    • AndNonSC的 ASM 代码现在比 AndSC的还长!它只有一个条件跳转(用于第一个比较) ,使用寄存器直接比较第一个结果和第二个结果,没有更多的跳转。

ASM 代码分析后的结论

  • 在 AMD64机器语言级别,&操作符似乎生成的 ASM 代码具有较少的条件跳转,这可能更适合于高预测失败率(例如随机 value)。
  • 另一方面,&&操作符似乎用较少的指令生成 ASM 代码(无论如何使用 -XX:PrintAssemblyOptions=intel选项) ,这可能更适合于具有预测友好输入的 真的很长循环,在这种情况下,每次比较的 CPU 周期数越少,从长远来看就会有所不同。

正如我在一些评论中所说的,这在不同的系统之间会有很大的不同,所以如果我们讨论的是分支预测优化,那么唯一真正的答案就是: 它取决于您的 JVM 实现、编译器、 CPU 和输入数据


附录: 番石榴的 isPowerOfTwo方法

在这里,Guava 的开发者想出了一个简单的方法来计算给定数字是否是2的幂次方:

public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}

引用观点:

这种使用 &(其中 &&将是更正常的)是一个真正的优化?

为了查明是否是这样,我在测试类中添加了两个类似的方法:

public boolean isPowerOfTwoAND(long x) {
return x > 0 & (x & (x - 1)) == 0;
}


public boolean isPowerOfTwoANDAND(long x) {
return x > 0 && (x & (x - 1)) == 0;
}

英特尔公司对番石榴的 ASM 代码

  # {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest'
# this:     rdx:rdx   = 'AndTest'
# parm0:    r8:r8     = long
...
0x0000000003103bbe: movabs rax,0x0
0x0000000003103bc8: cmp    rax,r8
0x0000000003103bcb: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
0x0000000003103bd5: movabs rsi,0x108
0x0000000003103bdf: jge    0x0000000003103bef
0x0000000003103be5: movabs rsi,0x118
0x0000000003103bef: mov    rdi,QWORD PTR [rax+rsi*1]
0x0000000003103bf3: lea    rdi,[rdi+0x1]
0x0000000003103bf7: mov    QWORD PTR [rax+rsi*1],rdi
0x0000000003103bfb: jge    0x0000000003103c1b  ;*lcmp
0x0000000003103c01: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
0x0000000003103c0b: inc    DWORD PTR [rax+0x128]
0x0000000003103c11: mov    eax,0x1
0x0000000003103c16: jmp    0x0000000003103c20  ;*goto
0x0000000003103c1b: mov    eax,0x0            ;*lload_1
0x0000000003103c20: mov    rsi,r8
0x0000000003103c23: movabs r10,0x1
0x0000000003103c2d: sub    rsi,r10
0x0000000003103c30: and    rsi,r8
0x0000000003103c33: movabs rdi,0x0
0x0000000003103c3d: cmp    rsi,rdi
0x0000000003103c40: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
0x0000000003103c4a: movabs rdi,0x140
0x0000000003103c54: jne    0x0000000003103c64
0x0000000003103c5a: movabs rdi,0x150
0x0000000003103c64: mov    rbx,QWORD PTR [rsi+rdi*1]
0x0000000003103c68: lea    rbx,[rbx+0x1]
0x0000000003103c6c: mov    QWORD PTR [rsi+rdi*1],rbx
0x0000000003103c70: jne    0x0000000003103c90  ;*lcmp
0x0000000003103c76: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
0x0000000003103c80: inc    DWORD PTR [rsi+0x160]
0x0000000003103c86: mov    esi,0x1
0x0000000003103c8b: jmp    0x0000000003103c95  ;*goto
0x0000000003103c90: mov    esi,0x0            ;*iand
0x0000000003103c95: and    rsi,rax
0x0000000003103c98: and    esi,0x1
0x0000000003103c9b: mov    rax,rsi
0x0000000003103c9e: add    rsp,0x50
0x0000000003103ca2: pop    rbp
0x0000000003103ca3: test   DWORD PTR [rip+0xfffffffffe44c457],eax        # 0x0000000001550100
0x0000000003103ca9: ret

英特尔 &&版本的暗码

  # {method} {0x0000000017580bd0} 'isPowerOfTwoANDAND' '(J)Z' in 'AndTest'
# this:     rdx:rdx   = 'AndTest'
# parm0:    r8:r8     = long
...
0x0000000003103438: movabs rax,0x0
0x0000000003103442: cmp    rax,r8
0x0000000003103445: jge    0x0000000003103471  ;*lcmp
0x000000000310344b: mov    rax,r8
0x000000000310344e: movabs r10,0x1
0x0000000003103458: sub    rax,r10
0x000000000310345b: and    rax,r8
0x000000000310345e: movabs rsi,0x0
0x0000000003103468: cmp    rax,rsi
0x000000000310346b: je     0x000000000310347b  ;*lcmp
0x0000000003103471: mov    eax,0x0
0x0000000003103476: jmp    0x0000000003103480  ;*ireturn
0x000000000310347b: mov    eax,0x1            ;*goto
0x0000000003103480: and    eax,0x1
0x0000000003103483: add    rsp,0x40
0x0000000003103487: pop    rbp
0x0000000003103488: test   DWORD PTR [rip+0xfffffffffe44cc72],eax        # 0x0000000001550100
0x000000000310348e: ret

在这个特定的示例中,JIT 编译器为 &&版本生成的 很远汇编代码比为 Guava 的 &版本生成的 很远汇编代码要少(在昨天的结果之后,我真的对此感到惊讶)。
与 Guava 相比,&&版本的 JIT 编译字节码减少了25% ,汇编指令减少了50% ,只有两个条件跳转(&版本有四个)。

因此,一切都表明,番石榴的 &方法不如更“自然”的 &&版本有效。

是吗?

如前所述,我正在用 Java8运行上面的示例:

C:\....>java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

但是 如果我切换到 Java7呢?

C:\....>c:\jdk1.7.0_79\bin\java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
C:\....>c:\jdk1.7.0_79\bin\java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*AndTest.isPowerOfTwoAND -XX:PrintAssemblyOptions=intel AndTestMain
.....
0x0000000002512bac: xor    r10d,r10d
0x0000000002512baf: mov    r11d,0x1
0x0000000002512bb5: test   r8,r8
0x0000000002512bb8: jle    0x0000000002512bde  ;*ifle
0x0000000002512bba: mov    eax,0x1            ;*lload_1
0x0000000002512bbf: mov    r9,r8
0x0000000002512bc2: dec    r9
0x0000000002512bc5: and    r9,r8
0x0000000002512bc8: test   r9,r9
0x0000000002512bcb: cmovne r11d,r10d
0x0000000002512bcf: and    eax,r11d           ;*iand
0x0000000002512bd2: add    rsp,0x10
0x0000000002512bd6: pop    rbp
0x0000000002512bd7: test   DWORD PTR [rip+0xffffffffffc0d423],eax        # 0x0000000002120000
0x0000000002512bdd: ret
0x0000000002512bde: xor    eax,eax
0x0000000002512be0: jmp    0x0000000002512bbf
.....

惊喜吧!Java7中的 JIT 编译器为 &方法生成的汇编代码现在只有 条件跳转,而且要短得多!而 &&方法(在这一点上你必须相信我,我不想把结尾弄得乱七八糟!) 保持不变,最多只有两个条件跳跃和几个更少的指令。
看来番石榴的工程师们毕竟知道自己在做什么!(如果他们试图优化 Java7的执行时间,那就是; -)

回到 OP 最新的问题:

这种使用 &(其中 &&将是更正常的)是一个真正的优化?

恕我直言,即使对于这个(非常!)特定的场景: 它取决于您的 JVM 实现、编译器、 CPU 和输入数据

我会从另一个角度来看这个问题。

考虑这两个代码片段,

  if (value >= x && value <= y) {

还有

  if (value >= x & value <= y) {

如果我们假设 valuexy具有基元类型,那么这两个(部分)语句将为所有可能的输入值提供相同的结果。(如果涉及到包装器类型,那么它们并不完全等价,因为 y的隐式 null测试可能在 &版本而不是 &&版本中失败。)

如果 JIT 编译器做得很好,它的优化器将能够推断出这两个语句做同样的事情:

  • 如果一个比另一个快,那么它应该能够使用更快的版本... 在 JIT 编译的代码中

  • 如果没有,那么在源代码级别使用哪个版本并不重要。

  • Since the JIT compiler gathers path statistics before compiling, it can potentially have more information about the execution characteristics that the programmer(!).

  • 如果当前一代的 JIT 编译器(在任何给定的平台上)不能很好地优化以处理这个问题,那么下一代可以很好地处理这个问题... ... 这取决于经验证明是否指向这是一个需要优化的 值得模式。

  • 事实上,如果你编写 Java 代码的方式能够优化这些代码,那么通过选择更“晦涩”的代码版本,你可以选择 抑制当前或未来 JIT 编译器的优化能力。

简而言之,我认为您不应该在源代码级别进行这种微观优化。如果你接受这个参数 1,并遵循它的逻辑结论,哪个版本更快的问题是... moot2

我不认为这是一个证据。

2-除非你是真正编写 Java JIT 编译器的小团体中的一员..。


“非常著名的问题”在两个方面很有趣:

  • 一方面,这是一个需要进行优化的例子,这种优化远远超出了 JIT 编译器的能力。

  • 另一方面,对数组进行排序不一定是正确的... ... 因为排序后的数组处理起来更快。对数组进行排序的成本很可能(远远)大于节省的成本。

我得到的解释是,如果一个系列中的第一个检查为 false,& & 将返回 false,而 & 检查一个系列中的所有项目,而不管有多少是 false。也就是说。

如果(x > 0 & amp

会跑得比

如果(x > 0 & x < = 10 & x

如果 x 大于10,因为单个与号将继续检查其余条件,而双与号将在第一个非真条件后中断。