Java 为什么封装在函数中的简单检查的效率是直接检查的两倍?
下面是两个版本的代码,它们计算数组中三元组的数量加起来等于零。一个使用函数调用进行实际测试,另一个在函数体中执行测试 它在性能时间方面表现出有趣的行为。使用函数调用的变量执行速度快两倍。为什么?Java 为什么封装在函数中的简单检查的效率是直接检查的两倍?,java,performance,function-call,Java,Performance,Function Call,下面是两个版本的代码,它们计算数组中三元组的数量加起来等于零。一个使用函数调用进行实际测试,另一个在函数体中执行测试 它在性能时间方面表现出有趣的行为。使用函数调用的变量执行速度快两倍。为什么? /** * Find triples of integers, which add up to zero */ public class SumCheck { public static void main(String[] args) { int a = 1000000
/**
* Find triples of integers, which add up to zero
*/
public class SumCheck {
public static void main(String[] args) {
int a = 1000000;
int b = 3000;
int[] input = new int[b];
for (int i = 0; i < b; i++) {
input[i] = StdRandom.uniform(-a, a);
}
double startTime2 = System.currentTimeMillis() / 1000.0;
int counter2 = count2(input);
double endTime2 = System.currentTimeMillis() / 1000.0;
System.out.printf("%d +(%.0f seconds)\n", counter2, endTime2 - startTime2);
double startTime = System.currentTimeMillis() / 1000.0;
int counter = count(input);
double endTime = System.currentTimeMillis() / 1000.0;
System.out.printf("%d +(%.0f seconds)\n", counter, endTime - startTime);
}
private static int count(int[] a) {
int counter = 0;
for (int i = 0; i < a.length; i++) {
for (int j = i + 1; j < a.length; j++) {
for (int k = j + 1; k < a.length; k++) {
if (a[i] + a[j] + a[k] == 0)
counter++;
}
}
}
return counter;
}
// same as count function but comparison is being done through a function call
private static int count2(int[] a) {
int counter = 0;
for (int i = 0; i < a.length; i++) {
for (int j = i + 1; j < a.length; j++) {
for (int k = j + 1; k < a.length; k++) {
counter = counter + check(a, i, j, k);
}
}
}
return counter;
}
private static int check(int[] a, int i, int j, int k) {
if (a[i] + a[j] + a[k] == 0) {
return 1;
}
return 0;
}
}
/**
*求整数的三元组,它们相加为零
*/
公共类SumCheck{
公共静态void main(字符串[]args){
INTA=1000000;
int b=3000;
int[]输入=新的int[b];
对于(int i=0;i
特别是,其中一次运行产生以下时间:
12秒,
33秒。不是。 如果您修改
if(a[i]+a[j]+a[k]==0){
柜台++
}
到
计数器=计数器+(a[i]+b[i]+c[i]==0?1:0)
i、 e.手动将检查插入count2
以创建count
,两种变体所用的时间完全相同
那么,为什么添加1或0会比if和increment快呢
有一个类似的案例和一个非常详细的答案。
它声称java正在将两个整数的三元表达式转换为cpu上的一个特殊操作CMOV。与正常的if-else结构相比,如果跳转不能成功,CMOV将获胜。我们可以通过使分支预测器的工作更容易来检查这篇论文:而不是使用a=1000000
,
我们使用a=0
。现在,if将一直采用,而且两种方法都同样快速
然而,如果我们使用一个更大的a,a=800_000_000
,则存在一个位数的情况,a[i]+a[j]+a[k]==0
,但如果增量仍然大约是系数2,则会更慢。这让我非常惊讶,因为如果3000**3个分支中只有10个被执行,那么分支预测器应该能够很好地预测这些未执行的分支,并且10个异常值不会让过程慢那么多。然而,我对java和java基准的了解还不够深入。我试着按照一个已经链接的问题的相同步骤进行研究,这使我得出结论,我们正在处理一个不同的情况。因为我是JMH的新手,这个案例似乎需要更仔细的处理,所以我将尝试发布足够的代码,以便更容易发现最终的缺陷。我在这个过程中遇到的最重要和最相关的一点信息是,除非测量循环本身,否则应该避免在基准测试中使用循环
这里是一个更好的基准测试的尝试,我添加了一个内联版本和base
方法来粗略估计随机数生成开销:
package org.sample;
import java.util.*;
import java.util.concurrent.*;
import org.openjdk.jmh.annotations.*;
@State(Scope.Thread)
public class MyBenchmark {
public static int counter1=0;
public static int counter2=0;
public static int counter3=0;
@Benchmark
public static int[] base() {
int[] input=new int[3];
for(int i=0;i<3;i++){
input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
}
return input;
}
@Benchmark
public static int ifInc() {
int[] input=new int[3];
for(int i=0;i<3;i++){
input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
}
if (input[0] + input[1] + input[2] == 0){
counter1++;
}
return counter1;
}
@Benchmark
public static int method() {
int[] input=new int[3];
for(int i=0;i<3;i++){
input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
}
counter2 = counter2 + check(input, 0, 1, 2);
return counter2;
}
@Benchmark
public static int inline() {
int[] input=new int[3];
for(int i=0;i<3;i++){
input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
}
counter3 = counter3 + (input[0]+input[1]+input[2] == 0 ? 1 : 0);
return counter3;
}
public static int check(int[] a, int i, int j, int k) {
if (a[i] + a[j] + a[k] == 0) {
return 1;
}
return 0;
}
}
这是一种摆脱开销的尝试,同时仍然避免循环优化,结果有些一致:
package org.sample;
import java.util.*;
import java.util.concurrent.*;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
@State(Scope.Thread)
public class MyLoopBenchmark {
Random random=new Random();
static int a = 1000000;
static int b = 300;
static int counter1 = 0;
static int counter2 = 0;
static int counter3 = 0;
static int[] input = new int[b];
@Setup(Level.Iteration)
public void prepare() {
for (int q = 0; q < b; q++) {
input[q] = a-random.nextInt(a*2);
}
}
public static int ifInc(int i,int j,int k) {
if (i+j+k == 0){
counter1++;
}
return counter1;
}
public static int method(int i,int j,int k) {
counter2 = counter2 + check(i, j, k);
return counter2;
}
public static int inline(int i,int j,int k) {
counter3 = counter3 + (i+j+k == 0 ? 1 : 0);
return counter3;
}
public static int check(int i, int j, int k) {
if (i + j + k == 0) {
return 1;
}
return 0;
}
@Benchmark
public void measureIf(Blackhole bh) {
for (int i = 0; i < input.length; i++) {
for (int j = 0; j < input.length; j++) {
for (int k = 0; k < input.length; k++) {
bh.consume(ifInc(input[i],input[j],input[k]));
}
}
}
}
@Benchmark
public void measureMethod(Blackhole bh) {
for (int i = 0; i < input.length; i++) {
for (int j = 0; j < input.length; j++) {
for (int k = 0; k < input.length; k++) {
bh.consume(method(input[i],input[j],input[k]));
}
}
}
}
@Benchmark
public void measureInline(Blackhole bh) {
for (int i = 0; i < input.length; i++) {
for (int j = 0; j < input.length; j++) {
for (int k = 0; k < input.length; k++) {
bh.consume(inline(input[i],input[j],input[k]));
}
}
}
}
}
一些测量整个循环的尝试表明,包含循环的ifInc
方法在第一次迭代中速度较慢,直到出现令人印象深刻且不可预测的优化,而其他两个循环的时间更为恒定。如果没有一些明确定义的条件,对看似不重要的更改甚至是这种完美设计的基准测试都非常敏感的结果会产生误导。JIT(准时制)编译器正在做的事情有点不可预测。也许它已经编译了这个方法,因为它经常被使用阅读这篇关于如何编写微基准测试的文章。如果你调用check
方法的次数足够多,它将被内联,因此你不太可能发现2count
方法与正确的基准测试之间存在差异。它还将有助于监控编译哪个方法以及何时编译。谢谢!我知道,这不是一个完美的基准测试,但仅仅是简单地交换这两个执行就表明,双重差异仍然存在。因此,问题可能在于Java优化代码的方式,这似乎很奇怪。在C语言中,必须使用inline
对函数进行优化(通过将函数体嵌入调用函数体中),这恰恰相反。
package org.sample;
import java.util.*;
import java.util.concurrent.*;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
@State(Scope.Thread)
public class MyLoopBenchmark {
Random random=new Random();
static int a = 1000000;
static int b = 300;
static int counter1 = 0;
static int counter2 = 0;
static int counter3 = 0;
static int[] input = new int[b];
@Setup(Level.Iteration)
public void prepare() {
for (int q = 0; q < b; q++) {
input[q] = a-random.nextInt(a*2);
}
}
public static int ifInc(int i,int j,int k) {
if (i+j+k == 0){
counter1++;
}
return counter1;
}
public static int method(int i,int j,int k) {
counter2 = counter2 + check(i, j, k);
return counter2;
}
public static int inline(int i,int j,int k) {
counter3 = counter3 + (i+j+k == 0 ? 1 : 0);
return counter3;
}
public static int check(int i, int j, int k) {
if (i + j + k == 0) {
return 1;
}
return 0;
}
@Benchmark
public void measureIf(Blackhole bh) {
for (int i = 0; i < input.length; i++) {
for (int j = 0; j < input.length; j++) {
for (int k = 0; k < input.length; k++) {
bh.consume(ifInc(input[i],input[j],input[k]));
}
}
}
}
@Benchmark
public void measureMethod(Blackhole bh) {
for (int i = 0; i < input.length; i++) {
for (int j = 0; j < input.length; j++) {
for (int k = 0; k < input.length; k++) {
bh.consume(method(input[i],input[j],input[k]));
}
}
}
}
@Benchmark
public void measureInline(Blackhole bh) {
for (int i = 0; i < input.length; i++) {
for (int j = 0; j < input.length; j++) {
for (int k = 0; k < input.length; k++) {
bh.consume(inline(input[i],input[j],input[k]));
}
}
}
}
}
Benchmark Mode Cnt Score Error Units
MyLoopBenchmark.measureIf avgt 25 123,262 ? 0,660 ms/op
MyLoopBenchmark.measureInline avgt 25 139,877 ? 0,447 ms/op
MyLoopBenchmark.measureMethod avgt 25 140,355 ? 0,482 ms/op