在Java中,&能比&&快吗?
In Java, can & be faster than &&?
在此代码中:
if (value >= x && value <= y) {
当 value >= x
和 value <= y
在没有特定模式的情况下既可能是真又可能是假时,使用 &
运算符会比使用 [=16= 更快吗? ]?
具体来说,我正在考虑 &&
如何延迟计算右侧表达式(即仅当 LHS 为真时),这意味着有条件,而在 Java &
在这种情况下保证对两个(布尔)子表达式进行严格评估。无论哪种方式,值结果都是相同的。
但是,虽然 >=
或 <=
运算符将使用简单的比较指令,但 &&
必须涉及分支,并且 该分支易于分支预测失败 - 根据这个非常著名的问题:Why is it faster to process a sorted array than an unsorted array?
因此,强制表达式没有惰性成分肯定会更具确定性,并且不易受到预测失败的影响。对吧?
备注:
- 如果代码如下所示,显然我的问题的答案是否:
if(value >= x && verySlowFunction())
。我专注于 "sufficiently simple" RHS 表达式。
- 无论如何那里都有一个条件分支(
if
语句)。我不能完全向自己证明那是无关紧要的,替代公式可能是更好的例子,比如 boolean b = value >= x && value <= y;
- 这一切都属于可怕的微优化世界。是的,我知道 :-) ... 有趣吗?
更新
只是为了解释我为什么感兴趣:我一直在盯着 Martin Thompson 在他关于 Aeron 的 Mechanical Sympathy blog, after he came and did a talk 中所写的系统。其中一个关键信息是我们的硬件拥有所有这些神奇的东西,而我们软件开发人员不幸地未能利用它。别担心,我不会对我所有的代码进行 s/&&/\&/ :-) ...但是这个网站上有很多关于通过删除分支来改进分支预测的问题,它发生了对我来说,条件布尔运算符是测试条件的核心。
当然,@StephenC 提出了一个奇妙的观点,即将您的代码弯曲成奇怪的形状可以使 JIT 更不容易发现常见的优化 - 如果不是现在,那么将来。并且上面提到的非常著名的问题很特别,因为它使预测复杂性远远超出了实际优化。
我非常清楚,在大多数(或几乎所有)情况下,&&
是最清晰、最简单、最快、最好的做法 - 尽管我非常感谢发布答案证明这一点的人!我真的很想知道在任何人的经历中是否真的有任何案例 "Can &
be faster?" 的答案可能是 Yes...
更新 2:
(解决问题过于宽泛的建议。我不想对这个问题进行重大更改,因为它可能会影响下面的一些答案,这些答案质量非常好!) 也许需要一个野外的例子;这是来自 Guava LongMath class(非常感谢@maaartinus 找到这个):
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
先看到那个 &
?如果你检查 link,next 方法被调用 lessThanBranchFree(...)
,这暗示我们处于分支避免区域 - Guava 确实被广泛使用:保存的每个周期都会导致海平面明显下降。那么让我们这样提出问题:&
(&&
更正常)的这种使用是真正的优化吗?
使用 &
或 &&
仍然需要评估条件,因此它不太可能节省任何处理时间 - 考虑到您在评估两个表达式时,它甚至可能会增加它只需要评估一个。
如果在某些非常罕见的情况下,使用 &
而不是 &&
来节省一纳秒是没有意义的,那么与使用 &
超过&&
.
编辑
我很好奇并决定 运行 一些基准。
我做了这个class:
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
.
第一次测试:
第二次测试:
第三次测试:
正如您在分析测试中看到的那样,与使用两个 &&
相比,仅使用一个 &
实际上需要 2-3 倍的时间才能达到 运行。这确实有点奇怪,因为我确实期望只有一个 &
.
会有更好的性能
我不是 100% 确定为什么。在这两种情况下,都必须评估两个表达式,因为它们都为真。我怀疑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");
}
}
下面是性能测试:
测试 1:
测试 2:
测试 3:
这也考虑了不同的值和不同的条件。
当两个条件都为真时,使用一个 &
需要更多时间到 运行,大约多 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 毫秒。
对于这类问题,您应该 运行 进行微基准测试。我用 JMH 进行了此测试。
基准实现为
// 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
的值。
吞吐量基准测试的结果(五次预热和十次测量迭代)是:
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
评估本身的结果并没有太大不同。只要在那段代码上没有发现性能影响,我就不会尝试优化它。根据代码中的位置,热点编译器可能会决定进行一些优化。上述基准可能未涵盖。
一些参考资料:
boolean logical AND - 如果两个操作数值都是true
,则结果值为true
;否则,结果为 false
conditional AND - 类似于 &
,但仅当其左侧操作数的值为 true
时才计算其右侧操作数
bitwise OR - 结果值是操作数值的按位或运算
好的,所以你想知道它在较低级别的行为......那么让我们看看字节码吧!
编辑:在末尾添加了为 AMD64 生成的汇编代码。看看一些有趣的笔记。
编辑 2(回复:OP 的“更新 2”):也为 Guava's isPowerOfTwo
method 添加了 asm 代码。
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
(&&
) 方法生成 两个 条件跳转,正如预期的那样:
- 它将
value
和x
加载到堆栈上,如果value
较低则跳转到L1。否则它会保留 运行 下一行。
- 它将
value
和 y
加载到堆栈上,如果 value
更大,则也跳转到 L1。否则它会保留 运行 下一行。
- 这恰好是一个
return true
以防 none 两次跳转。
- 然后我们有标记为 L1 的行,它们是
return false
。
然而,AndNonSC
(&
) 方法会生成 三个 条件跳转!
- 它将
value
和 x
加载到堆栈上,如果 value
较低则跳转到 L1。因为现在它需要保存结果来与 AND 的另一部分进行比较,所以它必须执行“save true
”或“save false
”,它不能同时执行同样的指令。
- 它将
value
和y
加载到堆栈上,如果value
更大则跳转到L1。它再次需要保存 true
或 false
,这是两行不同的内容,具体取决于比较结果。
- 现在两个比较都完成了,代码实际上执行了AND运算——如果两者都为真,它跳转到(第三次)到return 真的;否则它将继续执行到 return 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 [=12=]x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
0x0000000002923e4b: movabs [=12=]x108,%rsi
0x0000000002923e55: jl 0x0000000002923e65
0x0000000002923e5b: movabs [=12=]x118,%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 [=12=]x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
0x0000000002923e84: movabs [=12=]x128,%rsi
0x0000000002923e8e: jg 0x0000000002923e9e
0x0000000002923e94: movabs [=12=]x138,%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 [=12=]x0,%eax
0x0000000002923eb5: add [=12=]x30,%rsp
0x0000000002923eb9: pop %rbp
0x0000000002923eba: test %eax,-0x1c73dc0(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923ec0: retq ;*ireturn
; - AndTest::AndSC@13 (line 25)
0x0000000002923ec1: mov [=12=]x1,%eax
0x0000000002923ec6: add [=12=]x30,%rsp
0x0000000002923eca: pop %rbp
0x0000000002923ecb: test %eax,-0x1c73dd1(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923ed1: retq
方法 AndSC
带有 -XX:PrintAssemblyOptions=intel
选项
# {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 [=14=]x0,%eax
0x0000000002923a80: jl 0x0000000002923a8b
0x0000000002923a86: mov [=14=]x1,%eax
0x0000000002923a8b: cmp %edi,%r9d
0x0000000002923a8e: mov [=14=]x0,%esi
0x0000000002923a93: jg 0x0000000002923a9e
0x0000000002923a99: mov [=14=]x1,%esi
0x0000000002923a9e: and %rsi,%rax
0x0000000002923aa1: cmp [=14=]x0,%eax
0x0000000002923aa4: je 0x0000000002923abb ;*ifeq
; - AndTest::AndNonSC@21 (line 29)
0x0000000002923aaa: mov [=14=]x1,%eax
0x0000000002923aaf: add [=14=]x30,%rsp
0x0000000002923ab3: pop %rbp
0x0000000002923ab4: test %eax,-0x1c739ba(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923aba: retq ;*ireturn
; - AndTest::AndNonSC@25 (line 30)
0x0000000002923abb: mov [=14=]x0,%eax
0x0000000002923ac0: add [=14=]x30,%rsp
0x0000000002923ac4: pop %rbp
0x0000000002923ac5: test %eax,-0x1c739cb(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923acb: retq
方法 AndNonSC
带有 -XX:PrintAssemblyOptions=intel
选项
# {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
s) .
- 另一方面,
&&
运算符似乎生成的 ASM 代码指令较少(无论如何使用 -XX:PrintAssemblyOptions=intel
选项),这可能对 非常长 具有预测友好输入的循环,其中每次比较的 CPU 循环次数较少可以在长 运行. 中产生差异
正如我在一些评论中所说,这在系统之间会有很大差异,所以如果我们谈论分支预测优化,唯一真正的答案是:它取决于您的 JVM 实现、您的编译器、您的 CPU 和您的输入数据 .
附录:番石榴的isPowerOfTwo
方法
在这里,Guava 的开发人员想出了一种计算给定数字是否为 2 的幂的巧妙方法:
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
引用OP:
is this use of &
(where &&
would be more normal) a real optimization?
为了确定是否是,我在测试中添加了两个类似的方法class:
public boolean isPowerOfTwoAND(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
public boolean isPowerOfTwoANDAND(long x) {
return x > 0 && (x & (x - 1)) == 0;
}
Intel Guava 版本的 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
英特尔&&
版本的asm代码
# {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%,并且只有两个条件跳转(&
版本有四个)。
所以一切都表明 Guava 的 &
方法效率低于更“自然”的 &&
版本。
...或者是?
如前所述,我运行将上述示例与 Java 8:
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)
但是如果我切换到Java 7会怎么样?
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编译器为&
方法生成的汇编代码,现在只有一个条件跳转,而且更短!而 &&
方法(在这个方法上你必须相信我,我不想把结尾弄得乱七八糟!)保持大致相同,有两个条件跳转和更少的指令,最重要的。
毕竟,Guava 的工程师似乎知道他们在做什么! (如果他们试图优化 Java 7 执行时间,即 ;-)
所以回到 OP 的最新问题:
is this use of &
(where &&
would be more normal) a real optimization?
恕我直言 答案是相同的,即使对于这个(非常!)特定场景也是如此:这取决于您的 JVM 实现、您的编译器、您的 CPU 和你的输入数据.
我将从不同的角度来看这个问题。
考虑这两个代码片段,
if (value >= x && value <= y) {
和
if (value >= x & value <= y) {
如果我们假设 value
、x
、y
具有原始类型,那么这两个(部分)语句将为所有可能的输入值提供相同的结果。 (如果涉及包装器类型,那么它们并不完全相同,因为 y
的隐式 null
测试可能会在 &
版本而不是 &&
版本中失败。 )
如果 JIT 编译器做得很好,它的优化器将能够推断出这两个语句做同样的事情:
如果一个比另一个更快,那么它应该能够使用更快的版本...... 在 JIT 编译代码中.
如果不是,那么在源代码层面使用哪个版本都无所谓
由于 JIT 编译器在编译之前会收集路径统计信息,因此它可能有更多关于程序员 (!) 执行特征的信息。
如果当前一代的 JIT 编译器(在任何给定平台上)没有优化到足以处理这个问题,下一代可能会做得很好......取决于经验证据是否指向这是一个值得优化的模式。
确实,如果您以为此优化的方式编写 Java 代码,有机会 通过选择更多 "obscure"版本的代码,你可能会抑制当前或未来JIT编译器的优化能力。
简而言之,我认为您不应该在源代码级别进行这种微优化。如果你接受这个论点1,并按照它得出合乎逻辑的结论,那么哪个版本更快的问题是......没有实际意义2。
1 - 我不认为这接近证明。
2 - 除非你是真正编写 Java JIT 编译器的小社区中的一员......
"Very Famous Question" 有两个方面很有趣:
一方面,这是一个例子,其中产生差异所需的优化类型远远超出了 JIT 编译器的能力。
另一方面,对数组进行排序不一定是正确的……只是因为排序后的数组可以更快地处理。对数组进行排序的成本很可能(远)大于节省的成本。
向我解释的方式是,如果系列中的第一个检查为假,&& 将 return 为假,而 & 将检查系列中的所有项目,而不管有多少是假的。 IE。
if (x>0 && x <=10 && x
运行 会比
快吗
if (x>0 & x <=10 & x
如果 x 大于 10,因为单 & 号将继续检查其余条件,而双 & 号将在第一个非真条件后中断。
在此代码中:
if (value >= x && value <= y) {
当 value >= x
和 value <= y
在没有特定模式的情况下既可能是真又可能是假时,使用 &
运算符会比使用 [=16= 更快吗? ]?
具体来说,我正在考虑 &&
如何延迟计算右侧表达式(即仅当 LHS 为真时),这意味着有条件,而在 Java &
在这种情况下保证对两个(布尔)子表达式进行严格评估。无论哪种方式,值结果都是相同的。
但是,虽然 >=
或 <=
运算符将使用简单的比较指令,但 &&
必须涉及分支,并且 该分支易于分支预测失败 - 根据这个非常著名的问题:Why is it faster to process a sorted array than an unsorted array?
因此,强制表达式没有惰性成分肯定会更具确定性,并且不易受到预测失败的影响。对吧?
备注:
- 如果代码如下所示,显然我的问题的答案是否:
if(value >= x && verySlowFunction())
。我专注于 "sufficiently simple" RHS 表达式。 - 无论如何那里都有一个条件分支(
if
语句)。我不能完全向自己证明那是无关紧要的,替代公式可能是更好的例子,比如boolean b = value >= x && value <= y;
- 这一切都属于可怕的微优化世界。是的,我知道 :-) ... 有趣吗?
更新 只是为了解释我为什么感兴趣:我一直在盯着 Martin Thompson 在他关于 Aeron 的 Mechanical Sympathy blog, after he came and did a talk 中所写的系统。其中一个关键信息是我们的硬件拥有所有这些神奇的东西,而我们软件开发人员不幸地未能利用它。别担心,我不会对我所有的代码进行 s/&&/\&/ :-) ...但是这个网站上有很多关于通过删除分支来改进分支预测的问题,它发生了对我来说,条件布尔运算符是测试条件的核心。
当然,@StephenC 提出了一个奇妙的观点,即将您的代码弯曲成奇怪的形状可以使 JIT 更不容易发现常见的优化 - 如果不是现在,那么将来。并且上面提到的非常著名的问题很特别,因为它使预测复杂性远远超出了实际优化。
我非常清楚,在大多数(或几乎所有)情况下,&&
是最清晰、最简单、最快、最好的做法 - 尽管我非常感谢发布答案证明这一点的人!我真的很想知道在任何人的经历中是否真的有任何案例 "Can &
be faster?" 的答案可能是 Yes...
更新 2: (解决问题过于宽泛的建议。我不想对这个问题进行重大更改,因为它可能会影响下面的一些答案,这些答案质量非常好!) 也许需要一个野外的例子;这是来自 Guava LongMath class(非常感谢@maaartinus 找到这个):
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
先看到那个 &
?如果你检查 link,next 方法被调用 lessThanBranchFree(...)
,这暗示我们处于分支避免区域 - Guava 确实被广泛使用:保存的每个周期都会导致海平面明显下降。那么让我们这样提出问题:&
(&&
更正常)的这种使用是真正的优化吗?
使用 &
或 &&
仍然需要评估条件,因此它不太可能节省任何处理时间 - 考虑到您在评估两个表达式时,它甚至可能会增加它只需要评估一个。
如果在某些非常罕见的情况下,使用 &
而不是 &&
来节省一纳秒是没有意义的,那么与使用 &
超过&&
.
编辑
我很好奇并决定 运行 一些基准。
我做了这个class:
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
.
第一次测试:
第二次测试:
第三次测试:
正如您在分析测试中看到的那样,与使用两个 &&
相比,仅使用一个 &
实际上需要 2-3 倍的时间才能达到 运行。这确实有点奇怪,因为我确实期望只有一个 &
.
我不是 100% 确定为什么。在这两种情况下,都必须评估两个表达式,因为它们都为真。我怀疑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");
}
}
下面是性能测试:
测试 1:
测试 2:
测试 3:
这也考虑了不同的值和不同的条件。
当两个条件都为真时,使用一个 &
需要更多时间到 运行,大约多 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 毫秒。
对于这类问题,您应该 运行 进行微基准测试。我用 JMH 进行了此测试。
基准实现为
// 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
的值。
吞吐量基准测试的结果(五次预热和十次测量迭代)是:
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
评估本身的结果并没有太大不同。只要在那段代码上没有发现性能影响,我就不会尝试优化它。根据代码中的位置,热点编译器可能会决定进行一些优化。上述基准可能未涵盖。
一些参考资料:
boolean logical AND - 如果两个操作数值都是true
,则结果值为true
;否则,结果为 false
conditional AND - 类似于 &
,但仅当其左侧操作数的值为 true
时才计算其右侧操作数
bitwise OR - 结果值是操作数值的按位或运算
好的,所以你想知道它在较低级别的行为......那么让我们看看字节码吧!
编辑:在末尾添加了为 AMD64 生成的汇编代码。看看一些有趣的笔记。
编辑 2(回复:OP 的“更新 2”):也为 Guava's isPowerOfTwo
method 添加了 asm 代码。
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
(&&
) 方法生成 两个 条件跳转,正如预期的那样:
- 它将
value
和x
加载到堆栈上,如果value
较低则跳转到L1。否则它会保留 运行 下一行。 - 它将
value
和y
加载到堆栈上,如果value
更大,则也跳转到 L1。否则它会保留 运行 下一行。 - 这恰好是一个
return true
以防 none 两次跳转。 - 然后我们有标记为 L1 的行,它们是
return false
。
然而,AndNonSC
(&
) 方法会生成 三个 条件跳转!
- 它将
value
和x
加载到堆栈上,如果value
较低则跳转到 L1。因为现在它需要保存结果来与 AND 的另一部分进行比较,所以它必须执行“savetrue
”或“savefalse
”,它不能同时执行同样的指令。 - 它将
value
和y
加载到堆栈上,如果value
更大则跳转到L1。它再次需要保存true
或false
,这是两行不同的内容,具体取决于比较结果。 - 现在两个比较都完成了,代码实际上执行了AND运算——如果两者都为真,它跳转到(第三次)到return 真的;否则它将继续执行到 return 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 [=12=]x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
0x0000000002923e4b: movabs [=12=]x108,%rsi
0x0000000002923e55: jl 0x0000000002923e65
0x0000000002923e5b: movabs [=12=]x118,%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 [=12=]x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
0x0000000002923e84: movabs [=12=]x128,%rsi
0x0000000002923e8e: jg 0x0000000002923e9e
0x0000000002923e94: movabs [=12=]x138,%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 [=12=]x0,%eax
0x0000000002923eb5: add [=12=]x30,%rsp
0x0000000002923eb9: pop %rbp
0x0000000002923eba: test %eax,-0x1c73dc0(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923ec0: retq ;*ireturn
; - AndTest::AndSC@13 (line 25)
0x0000000002923ec1: mov [=12=]x1,%eax
0x0000000002923ec6: add [=12=]x30,%rsp
0x0000000002923eca: pop %rbp
0x0000000002923ecb: test %eax,-0x1c73dd1(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923ed1: retq
方法 AndSC
带有 -XX:PrintAssemblyOptions=intel
选项
# {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 [=14=]x0,%eax
0x0000000002923a80: jl 0x0000000002923a8b
0x0000000002923a86: mov [=14=]x1,%eax
0x0000000002923a8b: cmp %edi,%r9d
0x0000000002923a8e: mov [=14=]x0,%esi
0x0000000002923a93: jg 0x0000000002923a9e
0x0000000002923a99: mov [=14=]x1,%esi
0x0000000002923a9e: and %rsi,%rax
0x0000000002923aa1: cmp [=14=]x0,%eax
0x0000000002923aa4: je 0x0000000002923abb ;*ifeq
; - AndTest::AndNonSC@21 (line 29)
0x0000000002923aaa: mov [=14=]x1,%eax
0x0000000002923aaf: add [=14=]x30,%rsp
0x0000000002923ab3: pop %rbp
0x0000000002923ab4: test %eax,-0x1c739ba(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923aba: retq ;*ireturn
; - AndTest::AndNonSC@25 (line 30)
0x0000000002923abb: mov [=14=]x0,%eax
0x0000000002923ac0: add [=14=]x30,%rsp
0x0000000002923ac4: pop %rbp
0x0000000002923ac5: test %eax,-0x1c739cb(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923acb: retq
方法 AndNonSC
带有 -XX:PrintAssemblyOptions=intel
选项
# {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 个条件跳转计数.
- ASM代码实际上更长为
- 使用英特尔语法:
AndSC
的 ASM 代码更短,只有 2 个条件跳转(不包括末尾的非条件jmp
)。实际上它只是两个 CMP,两个 JL/E 和一个 XOR/MOV 取决于结果。AndNonSC
的 ASM 代码现在比AndSC
的要长! 但是,它只有一次条件跳转(第一次比较),使用寄存器直接比较第一个结果和第二个结果,没有更多的跳转。
ASM代码分析后的结论
- 在 AMD64 机器语言级别,
&
运算符生成的 ASM 代码似乎具有较少的条件跳转,这可能更适合高预测失败率(例如随机value
s) . - 另一方面,
&&
运算符似乎生成的 ASM 代码指令较少(无论如何使用-XX:PrintAssemblyOptions=intel
选项),这可能对 非常长 具有预测友好输入的循环,其中每次比较的 CPU 循环次数较少可以在长 运行. 中产生差异
正如我在一些评论中所说,这在系统之间会有很大差异,所以如果我们谈论分支预测优化,唯一真正的答案是:它取决于您的 JVM 实现、您的编译器、您的 CPU 和您的输入数据 .
附录:番石榴的isPowerOfTwo
方法
在这里,Guava 的开发人员想出了一种计算给定数字是否为 2 的幂的巧妙方法:
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
引用OP:
is this use of
&
(where&&
would be more normal) a real optimization?
为了确定是否是,我在测试中添加了两个类似的方法class:
public boolean isPowerOfTwoAND(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
public boolean isPowerOfTwoANDAND(long x) {
return x > 0 && (x & (x - 1)) == 0;
}
Intel Guava 版本的 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
英特尔&&
版本的asm代码
# {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%,并且只有两个条件跳转(&
版本有四个)。
所以一切都表明 Guava 的 &
方法效率低于更“自然”的 &&
版本。
...或者是?
如前所述,我运行将上述示例与 Java 8:
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)
但是如果我切换到Java 7会怎么样?
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编译器为&
方法生成的汇编代码,现在只有一个条件跳转,而且更短!而 &&
方法(在这个方法上你必须相信我,我不想把结尾弄得乱七八糟!)保持大致相同,有两个条件跳转和更少的指令,最重要的。
毕竟,Guava 的工程师似乎知道他们在做什么! (如果他们试图优化 Java 7 执行时间,即 ;-)
所以回到 OP 的最新问题:
is this use of
&
(where&&
would be more normal) a real optimization?
恕我直言 答案是相同的,即使对于这个(非常!)特定场景也是如此:这取决于您的 JVM 实现、您的编译器、您的 CPU 和你的输入数据.
我将从不同的角度来看这个问题。
考虑这两个代码片段,
if (value >= x && value <= y) {
和
if (value >= x & value <= y) {
如果我们假设 value
、x
、y
具有原始类型,那么这两个(部分)语句将为所有可能的输入值提供相同的结果。 (如果涉及包装器类型,那么它们并不完全相同,因为 y
的隐式 null
测试可能会在 &
版本而不是 &&
版本中失败。 )
如果 JIT 编译器做得很好,它的优化器将能够推断出这两个语句做同样的事情:
如果一个比另一个更快,那么它应该能够使用更快的版本...... 在 JIT 编译代码中.
如果不是,那么在源代码层面使用哪个版本都无所谓
由于 JIT 编译器在编译之前会收集路径统计信息,因此它可能有更多关于程序员 (!) 执行特征的信息。
如果当前一代的 JIT 编译器(在任何给定平台上)没有优化到足以处理这个问题,下一代可能会做得很好......取决于经验证据是否指向这是一个值得优化的模式。
确实,如果您以为此优化的方式编写 Java 代码,有机会 通过选择更多 "obscure"版本的代码,你可能会抑制当前或未来JIT编译器的优化能力。
简而言之,我认为您不应该在源代码级别进行这种微优化。如果你接受这个论点1,并按照它得出合乎逻辑的结论,那么哪个版本更快的问题是......没有实际意义2。
1 - 我不认为这接近证明。
2 - 除非你是真正编写 Java JIT 编译器的小社区中的一员......
"Very Famous Question" 有两个方面很有趣:
一方面,这是一个例子,其中产生差异所需的优化类型远远超出了 JIT 编译器的能力。
另一方面,对数组进行排序不一定是正确的……只是因为排序后的数组可以更快地处理。对数组进行排序的成本很可能(远)大于节省的成本。
向我解释的方式是,如果系列中的第一个检查为假,&& 将 return 为假,而 & 将检查系列中的所有项目,而不管有多少是假的。 IE。
if (x>0 && x <=10 && x
运行 会比
快吗if (x>0 & x <=10 & x
如果 x 大于 10,因为单 & 号将继续检查其余条件,而双 & 号将在第一个非真条件后中断。