0%

两数之合

数组 [2,2,3,4,9,3] 给一个元素 6 找出数组中两个元素之和等于它,并返回数组坐标。思路:使用mapkey用来记录arr[i]的值,value用来记录对应的下标,使用 containsKey 或者 get 进行逻辑判断 arr[?] = target - arr[??]

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class TestFindArrayIndex {

@Benchmark
public int[] findIndexOfArray1() {
int[] arr = new int[] {2,2,3,4,9,3}; int target = 6;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (map.containsKey(target - arr[i])) {
return new int[] {map.get(target - arr[i]),i};
}
map.put(arr[i], i);
}
return null;
}

@Benchmark
public int[] findIndexOfArray2() {
int[] arr = new int[] {2,2,3,4,9,3}; int target = 6;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (map.get(arr[i]) != null) {
return new int[]{map.get(arr[i]), i};
}
map.put(target - arr[i], i);
}
return null;
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(TestFindArrayIndex.class.getSimpleName())
.output("Benchmark.log")
.forks(1)
.build();
new Runner(opt).run();
}
}

给出了两个性能差不多的方案,使用 JMH 性能测试,更多例子参考 ,影响基准测试的因素比较多,包括代码预热、编译器动态优化、资源回收(GC)、文件缓存、电源、其他程序,JVM的VM选项等等,详细参考

引入组件

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.20</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.20</version>
</dependency>

注解介绍

@BenchmarkMode 基准测试类型
public enum Mode {
Throughput("thrpt", "Throughput, ops/time"),
AverageTime("avgt", "Average time, time/op"),
SampleTime("sample", "Sampling time"),
SingleShotTime("ss", "Single shot invocation time"),
All("all", "All benchmark modes");
}

Throughput: 整体吞吐量,如:1秒内可以执行多少次调用”。
AverageTime: 调用的平均时间,如:每次调用平均耗时xxx毫秒。
SampleTime: 随机取样,最后输出取样结果的分布,如:99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内
SingleShotTime: 以上模式都是默认一次 iteration1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。

@Warmup 预热

进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数iterations也就非常好理解了,就是预热轮数。

为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Warmup {
int BLANK_ITERATIONS = -1;
int BLANK_TIME = -1;
int BLANK_BATCHSIZE = -1;

int iterations() default -1; // 预热的次数

int time() default -1; // 每次预热的时间

TimeUnit timeUnit() default TimeUnit.SECONDS; // 时间的单位,默认秒

int batchSize() default -1; // 批处理大小,每次操作调用几次方法
}

@Measurement 度量

@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Measurement {

int BLANK_ITERATIONS = -1;
int BLANK_TIME = -1;
int BLANK_BATCHSIZE = -1;

/** 进行测试的轮次 */
int iterations() default BLANK_ITERATIONS;

/** 每轮进行的时长 */
int time() default BLANK_TIME;

/** 时长单位 */
TimeUnit timeUnit() default TimeUnit.SECONDS;

/** 批处理大小,每次操作调用几次方法 */
int batchSize() default BLANK_BATCHSIZE;
}

@OutputTimeUnit 基准测试结果的时间类型

@Benchmark 方法级注解,表示该方法是需要进行 benchmark 的对象

这两种方案的性能对比

Benchmark                              Mode  Cnt      Score     Error   Units
TestFindArrayIndex.findIndexOfArray1 thrpt 10 16353.697 ± 141.580 ops/ms
TestFindArrayIndex.findIndexOfArray2 thrpt 10 15299.888 ± 675.373 ops/ms

参考文章