12/08/2018, 13:05

Introduce and guide JMH library - Measure Performance tool of Java code

Giới thiệu và hướng dẫn sử dụng JMH tool Nhân tiện tìm hiểu và so sánh về performance của stream API trong Java 8 so với vòng lặp forLoop, forEach Mình sẽ giới thiệu, giải thích các khái niệm của JMH library. Tool phổ biến được sử dụng trong các bài viết về so sánh, kiểm tra tốc độ và ...

Giới thiệu và hướng dẫn sử dụng JMH tool

Nhân tiện tìm hiểu và so sánh về performance của stream API trong Java 8 so với vòng lặp forLoop, forEach Mình sẽ giới thiệu, giải thích các khái niệm của JMH library. Tool phổ biến được sử dụng trong các bài viết về so sánh, kiểm tra tốc độ và performance.

JMH sử dụng trong trường hợp bạn muốn kiểm tra, đo tốc độ thực thi của 1 method hay 1 thuật toán , so sánh giữa các cách implement khác nhau, các thuật toán khác nhau. Hoặc kiểm tra, so sánh tốc độ giữa các thư viện của bên thứ 3.

JMH là tool được phát triển bởi các developer của Oracle.

Maven dependency:

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-core</artifactId>
	<version>1.11.2</version>
</dependency>
<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-generator-annprocess</artifactId>
	<version>1.11.2</version>
	<scope>provided</scope>
</dependency>

Simple example for fist run

public class BasicTest {

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(BasicTest.class.getSimpleName())
                .warmupIterations(1)
                .measurementIterations(5)
                .forks(1)
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    public void sayGoodbye() {
    }
}

1. Measure mode

Mode.Throughput: đo bằng cách lặp, gọi liên tục benchmark method trong khoảng thời gian giới hạn ( default là 20 lần gọi) và tính thời gian thực hiện method của mỗi lần gọi. Đây là mode default.

Mode.AverageTime: đo thời gian thực hiện trung bình của 1 benchmark method

Mode.SampleTime: vẫn thực hiện bằng cách chạy benchmark method trong khoảng thời gian giới hạn. Nhưng thay vì đo tổng thời gian thì đo thời gan cần cho việc gọi method.

Mode.SingleShotTime: chỉ chạy method 1 lần để đo đạc

Mode.All: chạy tất cả mode test cho 1 benchmark method.

Hoặc chỉ đinh ra các mode muốn chạy cho 1 method bằng cách @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime})

2. Unit time

@OutputTimeUnit: sử dụng annotation này để chỉ ra đơn vị thời gian sẽ được sử dụng để hiện thị kết quả đo TimeUnit.MICROSECONDS TimeUnit.SECONDS

3. Number of iteration and number of call method

JMH coi 1 iteration là 1 lần lặp để thực hiện đo đạc. Trong mỗi iteration method sẽ được gọi nhiều lần hoặc 1 lần tùy vào Mode và config của iteration. Warmup: giai đoạn làm nóng, config số lần đo trước khi thực hiện các iteration. Để thiết lập số lần iteration có 2 cách. Dùng annotation hoặc các method tương ứng:

  • Sử dụng method .warmupIterations() .measurementIterations()

    EX:

Options opt = new OptionsBuilder()
                    .include(BasicTest.class.getSimpleName())
                    .warmupIterations(5)
                    .measurementIterations(5)
                    .forks(1)
                    .build();
  • Sử dụng annotation @Measurement , @Warmup:
    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @Measurement(iterations = 5)
    @Warmup(iterations = 5)
    public void sayGoodbye() {
        // this method was intentionally left blank.
    }

Các option của Measurement và Warmup gồm:

  • iterations: thiết lập số lần đo lặp lại
  • batchSize: số lần gọi benchmark method trong 1 lần đo. Option này chỉ có giá trị khi config với SingleShotTime mode.Với các mode khác nó sẽ bị bỏ qua.
  • timeUnit: đơn vị thời gian sử dụng trong qua trình đo
  • time: thời gian cho mỗi một lần đo (một iteration)

Ex:

@Benchmark
@BenchmarkMode(Mode.SingleShotTime)
@Measurement(iterations = 5, batchSize = 2)
@Warmup(iterations = 0)
public void sayGoodbye() {

}

4. State object

Trong 1 vài trường hợp đo, chúng ta cần có 1 đối tượng mà giá trị của nó có thể được giữ lại giữa các lần gọi hoặc để test đồng bộ giữa các luồng khác nhau. JMH giúp chúng ta thực hiện điều này bằng cách sử dụng với annotation @State. Việc khởi tạo các state object này sẽ được thực hiện bởi 1 trong các luồng chạy benchmark. Khi tạo một class để sử dụng làm state object cần lưu ý tuân theo các rule sau:

  • Nên có constructor không tham số.
  • Nên là public class
  • Inner class thì nên là static
  • Class được đánh dâu với @State annotation

Ex:

@State(Scope.Benchmark)
    public static class BenchmarkState {
        volatile double x = Math.PI;
    }

    @Benchmark
    public void measureShared(BenchmarkState state) {
        state.x++;
    }

Scope of state object: Scope.Thread: Đây là scope default. Mỗi 1 instance của class sẽ được tạo và gán cho 1 thread khi chạy. Tương đương unshare object. Scope.Benchmark: một instance sẽ được share giữa các thread. Thường được sử dụng khi test, đo multithread

5. Setup TearDown - các method khởi tạo và dọn dẹp

Tương tự như trong Junit test, JMH cũng support các method Setup và TearDown Chú y các method này chỉ có y nghĩa và ảnh hưởng với các state object. Vì vậy nếu khai báo các method này trong class mà không có @State thì sẽ bị lỗi runtine Ex:

@State(Scope.Thread)
public class BasicTest {

	// remaining code
}

Tùy vào level mà các method Setup và TearDown được gọi ở các thời điểm khác nhau:

  • Level.Trial: Đây là level mặc định. Chạy 1 lần trước khi bắt đầu/sau toàn bộ 1 benchmark ( 1 nhóm các iteration được config như mô tả ở phần trên )
  • Level.Iteration: Chạy trước/sau 1 lần interation (1 nhóm các lần gọi benchmark method)
  • Level.Invocation: chạy trước/sau mỗi một lân gọi method. Level này không khuyên dùng vì nó sẽ ảnh hưởng đến thời gian tính toán của benchmark method

Ex:

 @Setup(Level.Trial)
    public void start()
    {
        System.out.println("Starting");
        callNumber = 0;
    }

6. Dead code

Dead code là 1 đoạn code mà chương trình không bao giờ chạy vào hoặc là đoạn code mà nếu nó được thực thi nhưng lại không có ảnh hưởng gì đến đầu ra của chương trình. Khi compiler có thể nhận biết đoạn code thừa và loại bỏ chúng (Dead code elimination - loại bỏ dead code). Trong 1 vài trường hợp có thể code cần test và đo tốc độ bị rơi vào trường hợp dead code và compiler sẽ loại bỏ nó. Điều này sẽ ảnh hưởng đến kết quả đo.

Để chặn việc loại bỏ dead code. JMH cung cấp 2 cách để làm điều này:

  • Luôn luôn trả về kết quả cho hàm cần test. Tức là các hàm cần test sẽ là hàm có giá trị trả về, không phải là hàm void
  • Sử dụng class BlackHole làm argument của method và đưa tất cả các result vào nó. Chú ý 1 vài trường hợp khi sử dụng blackHole sẽ gây ra ảnh hưởng đến kết quả đo

Ex:

@State(Scope.Thread)
public class DeadCodeExample {

    private double x = Math.PI;

    @Benchmark
    public void measureWrong() {
        // Kết quả không được sử dụng, vì vậy đoạn tính toán này(dead code) sẽ được loại bỏ
        Math.log(x);
    }

    @Benchmark
    public double measureRight() {
        // Trả về kết quả tính toán để chặn loại bỏ dead code
        return Math.log(x);
    }

    @Benchmark
    public void measureRight2(Blackhole blackhole) {
        // Sử dụng black hole để sử dụng kết quả.
        blackhole.consume(Math.log(x));
    }

    @Benchmark
    public void measureRight3(Blackhole blackhole) {
	// Black hole có thể sử dụng với nhiều kết quả
        blackhole.consume(Math.log(x1));
        blackhole.consume(Math.log(x2));
    }
}

7. Constant folding

Constant folding: là quá trình ngược với loại bỏ dead code. Nếu JVM nhận ra rằng kết quả tính toán của 1 đoạn code, 1 method là giống nhau trong các lần chạy thì JVM có thể tối ưu nó. Khi sử dụng JMH việc gọi các method được lặp lại nhiều lần, các lần gọi sau lần gọi thứ nhất đã được tối ưu và không giống với việc thực thi method trong điều khiện bình thường. Ngăn chặn điều này bằng cách: luôn đọc tham số đầu vào từ 1 biến không phải là final của 1 state oject khi test 1 method.

Example:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ConstantFoldExample {
    private double x = Math.PI;
    private final double wrongX = Math.PI;

    @Benchmark
    public double measureWrong() {
        return Math.log(wrongX);
    }

    @Benchmark
    public double measureRight() {
        return Math.log(x);
    }
}

Kết luận: Để kết quả đo chính xác chúng ta nên luôn đọc test input from state object và trả về kết quả của method tính toán

8. Fork

Mặc định JMH chia ( gán ) 1 xử lý cho mỗi một lần đo, thử trial (1 tập hợp các iteration). Mỗi một trial sẽ chạy như 1 JVM độc lập. Thông thường rất hiếm trường hợp cần thay đổi giá trị này. Nhưng JMH cung cấp 2 cách để config số lượng process cho một lần đo

  • Sử dụng method forks() của đối tượng OptionsBuilder
  • Sử dụng annotation @Fork cho test method

Ex:

new OptionsBuilder().fork(1)

hoặc

@Benchmark
    @Fork(1)
    public void measureRight2(Blackhole blackhole) {
        blackhole.consume(Math.log(x));
    }

Chú ý: khi 1 method được đánh dấu là benchmark và nó tạo ra 1 exception thì quá trình đo sẽ dừng lại.

Với những khái niệm và hướng dẫn ở trên, bạn đã đủ khả năng để viết và tạo ra chương trình test cho hầu hết trường hợp thông thường. Muốn tìm hiểu các cách thức test, đo nâng cao hơn bạn nên đọc thêm các ví dụ của JMH ở link:

http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

**Reference links: **

JMH API document: http://javadox.com/org.openjdk.jmh/jmh-core/0.9/org/openjdk/jmh/annotations/package-summary.html

JMH Home page: http://openjdk.java.net/projects/code-tools/jmh/

Other document: http://java-performance.info/jmh/

0