确定一个数字甚至使用 Java 的最有效方法是什么,为什么?
它会使用模数或减法,还是我实际上没有想到的其他方式?
可以想象我可以通过一个简单的测试类来确定这一点——我可以——但这真的不能解释为什么,不是吗?
我不是为了更快地处理这么多项目的崇高目标而进行一些疯狂的性能调整。但我很好奇是否应该首选一种方法而不是另一种方法作为常见做法。就像我们不会使用&
代替一样&&
,为什么%
在我们可以使用的时候使用&
?
确定一个数字甚至使用 Java 的最有效方法是什么,为什么?
它会使用模数或减法,还是我实际上没有想到的其他方式?
可以想象我可以通过一个简单的测试类来确定这一点——我可以——但这真的不能解释为什么,不是吗?
我不是为了更快地处理这么多项目的崇高目标而进行一些疯狂的性能调整。但我很好奇是否应该首选一种方法而不是另一种方法作为常见做法。就像我们不会使用&
代替一样&&
,为什么%
在我们可以使用的时候使用&
?
如果检查这两种方法的热点7生成的程序集:
public static boolean isEvenBit(int i) {
return (i & 1) == 0;
}
public static boolean isEvenMod(int i) {
return i % 2 == 0;
}
您会看到,虽然 mod 已经过优化并且基本上是按位执行and
,但它有一些额外的指令,因为这两个操作并不严格等价*。其他 JVM 可能会以不同的方式对其进行优化。该程序集发布在下面以供参考。
我还运行了一个微型基准测试来证实我们的观察结果:isEventBit 稍微快一些(但两者都在大约 2纳秒内运行,因此可能不会对整个典型程序产生太大影响):
Benchmark Mode Samples Score Error Units
c.a.p.SO16969220.isEvenBit avgt 10 1.869 ± 0.069 ns/op
c.a.p.SO16969220.isEvenMod avgt 10 2.554 ± 0.142 ns/op
# {method} 'isEvenBit' '(I)Z' in 'javaapplication4/Test1'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x00000000026c2580: sub rsp,0x18
0x00000000026c2587: mov QWORD PTR [rsp+0x10],rbp ;*synchronization entry
; - javaapplication4.Test1::isEvenBit@-1 (line 66)
0x00000000026c258c: and edx,0x1
0x00000000026c258f: mov eax,edx
0x00000000026c2591: xor eax,0x1 ;*ireturn
; - javaapplication4.Test1::isEvenBit@11 (line 66)
0x00000000026c2594: add rsp,0x10
0x00000000026c2598: pop rbp
0x00000000026c2599: test DWORD PTR [rip+0xfffffffffdb6da61],eax # 0x0000000000230000
; {poll_return}
0x00000000026c259f: ret
# {method} 'isEvenMod' '(I)Z' in 'javaapplication4/Test1'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x00000000026c2780: sub rsp,0x18
0x00000000026c2787: mov QWORD PTR [rsp+0x10],rbp ;*synchronization entry
; - javaapplication4.Test1::isEvenMod@-1 (line 63)
0x00000000026c278c: mov r10d,edx
0x00000000026c278f: and r10d,0x1 ;*irem
; - javaapplication4.Test1::isEvenMod@2 (line 63)
0x00000000026c2793: mov r11d,r10d
0x00000000026c2796: neg r11d
0x00000000026c2799: test edx,edx
0x00000000026c279b: cmovl r10d,r11d
0x00000000026c279f: test r10d,r10d
0x00000000026c27a2: setne al
0x00000000026c27a5: movzx eax,al
0x00000000026c27a8: xor eax,0x1 ;*ireturn
; - javaapplication4.Test1::isEvenMod@11 (line 63)
0x00000000026c27ab: add rsp,0x10
0x00000000026c27af: pop rbp
0x00000000026c27b0: test DWORD PTR [rip+0xfffffffffdb6d84a],eax # 0x0000000000230000
; {poll_return}
0x00000000026c27b6: ret
* 正如评论中指出的那样,%
并不是真正的模数;这是剩下的。所以(i % 2) != (i & 1)
如果i < 0
. 代码中的额外指令isEvenMod
将结果的符号设置为i
(然后将其与零进行比较,因此浪费了努力)。
另一种方法是运行微基准测试并分析每个变体所花费的时间。结果如下:
Benchmark Mean Units Time vs. baseline
baseline 10.330 nsec/op 0.000
bitAnd 12.075 nsec/op 1.745
bitShift 12.309 nsec/op 1.979
modulo 12.309 nsec/op 4.529
(基线是一个只返回的方法i == 0
)
结论:
i & 1
-----> 大约需要 1.75nsi << 31
--> 大约需要 2.00nsi % 2
-----> 大约需要 4.50ns换句话说,i % 2
比 慢 2 倍i & 1
。
注释:使用 jmh 完成的基准测试。基线很高,因为我生成随机数以确保该方法没有被优化掉。测试在带有热点 7 的 i7 @ 2.8GHz(即一个周期 = 0.35ns)上运行。
TL;DR按位和版本似乎是最快的。基准和示例结果如下。
这应该比模数更快,因为它只有两个步骤可以直接在硬件中处理:
if ((n & 1) == 0) {
// even number here
}
这是一个微基准,可以证明我和 aasylias 的观点:
// setup
int runs = 10;
int numbers = 200000000; // 200.000.000
int[] randomNumbers = new int[numbers];
Random random = new Random();
for (int i = 0; i < randomNumbers.length; i++) {
randomNumbers[i] = random.nextInt();
}
int even = 0;
int odd = 0;
// bitwiseAnd
long andStart = System.currentTimeMillis();
for (int i = 0; i < runs; i++) {
for (int number : randomNumbers) {
if ((number & 1) == 0)
even++;
else
odd++;
}
}
long andDone = System.currentTimeMillis();
long andDuration = andDone - andStart;
System.out.println("Even " + even + ", odd " + odd);
// reset variables
even = 0;
odd = 0;
// Modulo
long moduloStart = System.currentTimeMillis();
for (int i = 0; i < runs; i++) {
for (int number : randomNumbers) {
if (number % 2 == 0)
even++;
else
odd++;
}
}
long moduloDone = System.currentTimeMillis();
long moduloDuration = moduloDone - moduloStart;
// Done with modulo
System.out.println("Even " + even + ", odd " + odd);
// reset variables
even = 0;
odd = 0;
// Shift
long shiftStart = System.currentTimeMillis();
for (int i = 0; i < runs; i++) {
for (int number : randomNumbers) {
if ((number << 31) == 0)
even++;
else
odd++;
}
}
long shiftDone = System.currentTimeMillis();
long shiftDuration = shiftDone - shiftStart;
// Done with shift
System.out.println("Even " + even + ", odd " + odd);
System.out.println("Modulo Time " + moduloDuration);
System.out.println("Bitwise & Time " + andDuration);
System.out.println("Shift Time " + shiftDuration);
按位总是快一点(即使你用模块切换代码块)。样本输出:
Even 999999530, odd 1000000470
Even 999999530, odd 1000000470
Even 999999530, odd 1000000470
Modulo Time 17731
Bitwise & Time 9672
Shift Time 10638
if ((i & 1) == 0) {
// Even
}