我已经读过一些关于HotSpot如何拥有一些"内在函数"注入代码,特别是Java标准Math libs(from here)
所以我决定尝试一下,看看HotSpot可以直接做出多大的反对(特别是因为我听说min / max可以编译成无分支的asm)。
public static final int max ( final int a, final int b )
{
if ( a > b )
{
return a;
}
return b;
}
这是我的实施。从另一个SO问题我已经读过,使用三元运算符使用额外的寄存器,我还没有发现在执行if块和使用三元运算符之间存在显着差异(即返回(a> b)? a:b)。
分配一个8Mb的int数组(即200万个值)并随机化它,我做了以下测试:
try ( final Benchmark bench = new Benchmark( "millis to max" ) )
{
int max = Integer.MIN_VALUE;
for ( int i = 0; i < array.length; ++i )
{
max = OpsMath.max( max, array[i] );
// max = Math.max( max, array[i] );
}
}
我在try-with-resources块中使用Benchmark对象。完成后,它会调用对象上的close()并打印该块完成所需的时间。测试是通过在上面的代码中注释/调出最大调用来单独完成的。
&#39;最大&#39;被添加到基准块之外的列表中并稍后打印,以避免JVM优化整个块。
每次测试运行时,数组都会随机化。
运行测试6次,它会得到以下结果:
Java标准数学:
millis to max 9.242167
millis to max 2.1566199999999998
millis to max 2.046396
millis to max 2.048616
millis to max 2.035761
millis to max 2.001044
第一次运行后相当稳定,再次运行测试会得到类似的结果。
OpsMath:
millis to max 8.65418
millis to max 1.161559
millis to max 0.955851
millis to max 0.946642
millis to max 0.994543
millis to max 0.9469069999999999
同样,第一次运行后结果非常稳定。
问题是:为什么?那里有很大的不同。我不明白为什么。即使我实现我的max()方法完全,如Math.max()(即返回(a&gt; = b)?a:b)我仍然会得到更好的结果!这毫无意义。
规格:
CPU:Intel i5 2500,3,3Ghz。 Java版本:JDK 8(公共3月18日发布),x64。 Debian Jessie(测试版)x64。
我还没有尝试使用32位JVM。
编辑:根据要求进行自包含测试。添加了一行以强制JVM预加载Math和OpsMath类。这消除了OpsMath测试第一次迭代的18ms成本。
// Constant nano to millis.
final double TO_MILLIS = 1.0d / 1000000.0d;
// 8Mb alloc.
final int[] array = new int[(8*1024*1024)/4];
// Result and time array.
final ArrayList<Integer> results = new ArrayList<>();
final ArrayList<Double> times = new ArrayList<>();
// Number of tests.
final int itcount = 6;
// Call both Math and OpsMath method so JVM initializes the classes.
System.out.println("initialize classes " +
OpsMath.max( Math.max( 20.0f, array.length ), array.length / 2.0f ));
final Random r = new Random();
for ( int it = 0; it < itcount; ++it )
{
int max = Integer.MIN_VALUE;
// Randomize the array.
for ( int i = 0; i < array.length; ++i )
{
array[i] = r.nextInt();
}
final long start = System.nanoTime();
for ( int i = 0; i < array.length; ++i )
{
max = Math.max( array[i], max );
// OpsMath.max() method implemented as described.
// max = OpsMath.max( array[i], max );
}
// Calc time.
final double end = (System.nanoTime() - start);
// Store results.
times.add( Double.valueOf( end ) );
results.add( Integer.valueOf( max ) );
}
// Print everything.
for ( int i = 0; i < itcount; ++i )
{
System.out.println( "IT" + i + " result: " + results.get( i ) );
System.out.println( "IT" + i + " millis: " + times.get( i ) * TO_MILLIS );
}
Java Math.max结果:
IT0 result: 2147477409
IT0 millis: 9.636998
IT1 result: 2147483098
IT1 millis: 1.901314
IT2 result: 2147482877
IT2 millis: 2.095551
IT3 result: 2147483286
IT3 millis: 1.9232859999999998
IT4 result: 2147482828
IT4 millis: 1.9455179999999999
IT5 result: 2147482475
IT5 millis: 1.882047
OpsMath.max结果:
IT0 result: 2147482689
IT0 millis: 9.003616
IT1 result: 2147483480
IT1 millis: 0.882421
IT2 result: 2147483186
IT2 millis: 1.079143
IT3 result: 2147478560
IT3 millis: 0.8861169999999999
IT4 result: 2147477851
IT4 millis: 0.916383
IT5 result: 2147481983
IT5 millis: 0.873984
总体结果仍然相同。我尝试过将数组随机化一次,并在同一个数组上重复测试,整体结果更快,但Java Math.max和OpsMath.max之间的差异相同。
答案 0 :(得分:11)
很难说为什么Math.max
比Ops.max
慢,但很容易说出为什么这个基准强烈支持对条件移动的分支:在{{ 1}} - 迭代,
n
不等于Math.max( array[i], max );
是max
大于所有先前元素的概率。显然,随着array[n-1]
的增长和给定
n
在大多数情况下,它可以忽略不计。条件移动指令对分支概率不敏感,它总是花费相同的时间来执行。条件移动指令比分支预测更快,如果分支很难预测。另一方面,如果可以高概率地预测分支,则分支预测更快。目前,我不确定条件移动的速度与分支的最佳和最差情况相比。 1
在你的情况下,除了前几个分支之外的所有分支都是可以预测的。从大约final int[] array = new int[(8*1024*1024)/4];
开始,使用条件移动没有意义,因为相对保证可以正确预测分支并且可以与其他指令并行执行(我猜你每次迭代只需要一个周期)。
这似乎发生在算法计算最小值/最大值或进行一些低效排序(良好的分支可预测性意味着每步的低熵)。
1 条件移动和预测分支都需要一个周期。前者的问题是它需要两个操作数,这需要额外的指令。最后,当分支单元空闲时,关键路径可能变得更长和/或ALU饱和。通常,但并非总是如此,在实际应用中可以很好地预测分支;这就是为什么分支预测首先被发明的原因。
关于时间条件移动与分支预测最佳和最差情况的血腥细节,请参阅下面的评论中的讨论。我的my own benchmark表明,当分支预测遇到最坏情况时,条件移动明显快于分支预测,但我不能忽略contradictory results。我们需要一些解释才能确切地发挥作用。更多的基准和/或分析可能有所帮助。
答案 1 :(得分:3)
当我在旧的(1.6.0_27)JVM上使用Math.max
运行(适当修改的)代码时,热循环如下所示:
0x00007f4b65425c50: mov %r11d,%edi ;*getstatic array
; - foo146::bench@81 (line 40)
0x00007f4b65425c53: mov 0x10(%rax,%rdx,4),%r8d
0x00007f4b65425c58: mov 0x14(%rax,%rdx,4),%r10d
0x00007f4b65425c5d: mov 0x18(%rax,%rdx,4),%ecx
0x00007f4b65425c61: mov 0x2c(%rax,%rdx,4),%r11d
0x00007f4b65425c66: mov 0x28(%rax,%rdx,4),%r9d
0x00007f4b65425c6b: mov 0x24(%rax,%rdx,4),%ebx
0x00007f4b65425c6f: rex mov 0x20(%rax,%rdx,4),%esi
0x00007f4b65425c74: mov 0x1c(%rax,%rdx,4),%r14d ;*iaload
; - foo146::bench@86 (line 40)
0x00007f4b65425c79: cmp %edi,%r8d
0x00007f4b65425c7c: cmovl %edi,%r8d
0x00007f4b65425c80: cmp %r8d,%r10d
0x00007f4b65425c83: cmovl %r8d,%r10d
0x00007f4b65425c87: cmp %r10d,%ecx
0x00007f4b65425c8a: cmovl %r10d,%ecx
0x00007f4b65425c8e: cmp %ecx,%r14d
0x00007f4b65425c91: cmovl %ecx,%r14d
0x00007f4b65425c95: cmp %r14d,%esi
0x00007f4b65425c98: cmovl %r14d,%esi
0x00007f4b65425c9c: cmp %esi,%ebx
0x00007f4b65425c9e: cmovl %esi,%ebx
0x00007f4b65425ca1: cmp %ebx,%r9d
0x00007f4b65425ca4: cmovl %ebx,%r9d
0x00007f4b65425ca8: cmp %r9d,%r11d
0x00007f4b65425cab: cmovl %r9d,%r11d ;*invokestatic max
; - foo146::bench@88 (line 40)
0x00007f4b65425caf: add $0x8,%edx ;*iinc
; - foo146::bench@92 (line 39)
0x00007f4b65425cb2: cmp $0x1ffff9,%edx
0x00007f4b65425cb8: jl 0x00007f4b65425c50
除了奇怪的REX前缀(不知道那是什么),这里有一个循环,已经展开8次,大部分是你所期望的---加载,比较和条件移动。有趣的是,如果您将参数的顺序交换为max
,那么它会输出另一种8深cmovl
链。我想它并不知道如何生成一个3-deep树的cmovl
s或8个单独的cmovl
链,以便在循环完成后合并。
使用明确的OpsMath.max
,它变成了有条件的和无条件的分支的大鼠,它已经展开了8次。我不打算发布循环;它不漂亮。基本上,上面的每个mov/cmp/cmovl
都会分为负载,比较和条件跳转到mov
和jmp
发生的位置。有趣的是,如果您将参数的顺序交换为max
,那么它会输出一个8深cmovle
链。 编辑 :正如@maaartinus所指出的那样,对于某些机器来说,分支机构的老鼠实际上更快,因为分支预测器在它们上面发挥作用,这些都是预测良好的分支。 / p>
我会毫不犹豫地从这个基准中得出结论。你有基准建设问题;你必须比你运行批次更多次,如果你想为Hotspot最快的代码计算时间,你必须对代码进行不同的分析。除了包装代码之外,您还不能衡量max
的速度,以及Hotspot对您尝试做什么或其他任何有价值的东西的理解程度。 max
的两种实现都会导致代码完全过快,任何类型的直接测量都无法在较大程序的上下文中发挥作用。
答案 2 :(得分:1)
使用JDK 8:
java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode)
在Ubuntu 13.10上
我运行了以下内容:
import java.util.Random;
import java.util.function.BiFunction;
public class MaxPerformance {
private final BiFunction<Integer, Integer, Integer> max;
private final int[] array;
public MaxPerformance(BiFunction<Integer, Integer, Integer> max, int[] array) {
this.max = max;
this.array = array;
}
public double time() {
long start = System.nanoTime();
int m = Integer.MIN_VALUE;
for (int i = 0; i < array.length; ++i) m = max.apply(m, array[i]);
m = Integer.MIN_VALUE;
for (int i = 0; i < array.length; ++i) m = max.apply(array[i], m);
// total time over number of calls to max
return ((double) (System.nanoTime() - start)) / (double) array.length / 2.0;
}
public double averageTime(int repeats) {
double cumulativeTime = 0;
for (int i = 0; i < repeats; i++)
cumulativeTime += time();
return (double) cumulativeTime / (double) repeats;
}
public static void main(String[] args) {
int size = 1000000;
Random random = new Random(123123123L);
int[] array = new int[size];
for (int i = 0; i < size; i++) array[i] = random.nextInt();
double tMath = new MaxPerformance(Math::max, array).averageTime(100);
double tAlt1 = new MaxPerformance(MaxPerformance::max1, array).averageTime(100);
double tAlt2 = new MaxPerformance(MaxPerformance::max2, array).averageTime(100);
System.out.println("Java Math: " + tMath);
System.out.println("Alt 1: " + tAlt1);
System.out.println("Alt 2: " + tAlt2);
}
public static int max1(final int a, final int b) {
if (a >= b) return a;
return b;
}
public static int max2(final int a, final int b) {
return (a >= b) ? a : b; // same as JDK implementation
}
}
我得到了以下结果(每次调用max的平均纳秒数):
Java Math: 15.443555810000003
Alt 1: 14.968298919999997
Alt 2: 16.442204045
所以从长远来看,第二种实施方式看起来最快,但幅度相对较小。
为了进行稍微更科学的测试,计算每个调用独立于前一个调用的元素对的最大值是有意义的。这可以通过使用两个随机数组来完成,而不是像这个基准测试中那样:
import java.util.Random;
import java.util.function.BiFunction;
public class MaxPerformance2 {
private final BiFunction<Integer, Integer, Integer> max;
private final int[] array1, array2;
public MaxPerformance2(BiFunction<Integer, Integer, Integer> max, int[] array1, int[] array2) {
this.max = max;
this.array1 = array1;
this.array2 = array2;
if (array1.length != array2.length) throw new IllegalArgumentException();
}
public double time() {
long start = System.nanoTime();
int m = Integer.MIN_VALUE;
for (int i = 0; i < array1.length; ++i) m = max.apply(array1[i], array2[i]);
m += m; // to avoid optimizations!
return ((double) (System.nanoTime() - start)) / (double) array1.length;
}
public double averageTime(int repeats) {
// warm up rounds:
double tmp = 0;
for (int i = 0; i < 10; i++) tmp += time();
tmp *= 2.0;
double cumulativeTime = 0;
for (int i = 0; i < repeats; i++)
cumulativeTime += time();
return cumulativeTime / (double) repeats;
}
public static void main(String[] args) {
int size = 1000000;
Random random = new Random(123123123L);
int[] array1 = new int[size];
int[] array2 = new int[size];
for (int i = 0; i < size; i++) {
array1[i] = random.nextInt();
array2[i] = random.nextInt();
}
double tMath = new MaxPerformance2(Math::max, array1, array2).averageTime(100);
double tAlt1 = new MaxPerformance2(MaxPerformance2::max1, array1, array2).averageTime(100);
double tAlt2 = new MaxPerformance2(MaxPerformance2::max2, array1, array2).averageTime(100);
System.out.println("Java Math: " + tMath);
System.out.println("Alt 1: " + tAlt1);
System.out.println("Alt 2: " + tAlt2);
}
public static int max1(final int a, final int b) {
if (a >= b) return a;
return b;
}
public static int max2(final int a, final int b) {
return (a >= b) ? a : b; // same as JDK implementation
}
}
哪位给了我:
Java Math: 15.346468170000005
Alt 1: 16.378737519999998
Alt 2: 20.506475350000006
您的测试设置方式会对结果产生巨大影响。在这种情况下,JDK版本似乎是最快的。与前一个案例相比,这一时间相对较大。
有人提到卡尺。好吧,如果你阅读the wiki,他们对微基准测试的第一件事就是不这样做:这是因为一般来说很难得到准确的结果。我认为这是一个明显的例子。