Java - Microbenchmark: Hướng Dẫn Cho Người Mới Bắt Đầu

Xin chào các nhà phù thủy Java tương lai! ? Hôm nay, chúng ta sẽ bắt đầu hành trình thú vị vào thế giới microbenchmarking trong Java. Đừng lo nếu bạn chưa từng viết một dòng mã trước đây - chúng ta sẽ bắt đầu từ đầu và học tập cùng nhau. Vậy, hãy lấy ly cà phê (hoặc trà, nếu bạn thích) và hãy bắt đầu!

Java - Microbenchmark

Microbenchmarking Là Gì?

Trước khi chúng ta đi sâu vào chi tiết của microbenchmarking trong Java, hãy hiểu rõ điều gì là microbenchmarking.

Hãy tưởng tượng bạn là một đầu bếp cố gắng hoàn thiện một công thức nấu ăn. Bạn sẽ chỉ thử món ăn cuối cùng để biết nó ngon không phải không? Bạn sẽ thử mỗi nguyên liệu, thử thời gian nấu nướng khác nhau và thử các kỹ thuật khác nhau. Đó chính là điều gì microbenchmarking trong lập trình - nó là cách để đo lường hiệu suất của các phần nhỏ, cô lập trong mã của bạn.

Tại Sao Microbenchmarking Java Quan Trọng?

Bây giờ, bạn có thể hỏi, "Tại sao tôi nên quan tâm đến benchmarking?" Đểwego, hãy để tôi kể cho bạn một câu chuyện nhỏ.

Khi tôi là một nhà phát triển nhân viên mới, một lần tôi đã viết một chương trình hoạt động hoàn hảo... trên máy tính của tôi. Nhưng khi chúng ta triển khai nó lên máy chủ của công ty, nó chạy chậm hơn một con rùa mang một chiếc túi xách nặng! Đó là khi tôi hiểu được tầm quan trọng của benchmarking. Nó giúp chúng ta:

  1. Xác định các góc nghẽn hiệu suất
  2. So sánh các cách thực hiện khác nhau
  3. Đảm bảo mã của chúng ta chạy hiệu quả trên các hệ thống khác nhau

Các Kỹ Thuật Microbenchmarking Java

Hãy xem qua một số kỹ thuật microbenchmarking phổ biến trong Java:

1. Đo Thời Gian Thủ Công

Cách đơn giản nhất để benchmark là đo thời gian thủ công. Dưới đây là một ví dụ cơ bản:

public class SimpleTimingExample {
public static void main(String[] args) {
long startTime = System.nanoTime();

// Mã của bạn ở đây
for (int i = 0; i < 1000000; i++) {
Math.sqrt(i);
}

long endTime = System.nanoTime();
long duration = (endTime - startTime);
System.out.println("Thời gian thực thi: " + duration + " nanoseconds");
}
}

Trong ví dụ này, chúng ta sử dụng System.nanoTime() để đo lường thời gian mất để tính căn bậc hai của các số từ 0 đến 999,999.

2. Sử Dụng JMH (Java Microbenchmark Harness)

Mặc dù việc đo thời gian thủ công đơn giản, nhưng nó không phải lúc nào cũng chính xác. Đó là khi JMH ra đời. JMH là một công cụ Java để xây dựng, chạy và phân tích nano/micro/milli/macro benchmarks.

Để sử dụng JMH, bạn cần thêm nó vào dự án của mình. Nếu bạn đang sử dụng Maven, hãy thêm các phụ thuộc này vào pom.xml của bạn:

<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
</dependencies>

Bây giờ, hãy viết một benchmark JMH đơn giản:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
@Warmup(iterations = 3)
@Measurement(iterations = 3)
public class JMHExample {

@Benchmark
public void benchmarkMathSqrt() {
Math.sqrt(143);
}

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

new Runner(opt).run();
}
}

Benchmark này đo lường thời gian trung bình mất để tính căn bậc hai của 143. Hãy phân tích các annotation:

  • @BenchmarkMode: Chỉ định điều gì cần đo lường (thời gian trung bình trong trường hợp này)
  • @OutputTimeUnit: Chỉ định đơn vị cho kết quả
  • @State: Định nghĩa phạm vi mà các đối tượng "state" sẽ được chia sẻ
  • @Fork: Số lần fork một benchmark duy nhất
  • @Warmup@Measurement: Định nghĩa số lần lặp đón nóng và đo lường

Các Thuật Toán Của Java Collections

Trong khi chúng ta đang nói về benchmarking, hãy ra điều hướng nhanh để nói về các thuật toán của Java Collections. Đây là những công cụ rất hữu ích có thể ảnh hưởng lớn đến hiệu suất của chương trình của bạn.

Dưới đây là bảng một số thuật toán phổ biến:

Thuật Toán Mô Tả Câu Hỏi Sử Dụng
Collections.sort() Sắp xếp một danh sách Khi bạn cần sắp xếp các phần tử
Collections.binarySearch() Tìm kiếm trong một danh sách đã sắp xếp Tìm một phần tử trong một danh sách lớn, đã sắp xếp
Collections.reverse() Đảo ngược thứ tự của danh sách Khi bạn cần đảo ngược thứ tự của các phần tử
Collections.shuffle() Xáo trộn ngẫu nhiên danh sách Xáo trộn ngẫu nhiên thứ tự của các phần tử
Collections.fill() Thay thế tất cả các phần tử bằng phần tử cụ thể Khởi tạo danh sách với giá trị cụ thể

Hãy benchmark hiệu suất của việc sắp xếp một danh sách bằng cách sử dụng Collections.sort():

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
@Warmup(iterations = 3)
@Measurement(iterations = 3)
public class SortingBenchmark {

@Param({"100", "1000", "10000"})
private int listSize;

private List<Integer> list;

@Setup
public void setup() {
list = new ArrayList<>(listSize);
Random rand = new Random();
for (int i = 0; i < listSize; i++) {
list.add(rand.nextInt());
}
}

@Benchmark
public void benchmarkCollectionsSort() {
Collections.sort(list);
}

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

new Runner(opt).run();
}
}

Benchmark này đo lường thời gian mất để sắp xếp các danh sách có kích thước khác nhau (100, 1000 và 10000 phần tử). Chạy điều này sẽ giúp bạn biết được thời gian sắp xếp tăng lên theo kích thước của danh sách.

Kết Luận

Và thế là xong, các bạn! Chúng ta đã vừa vặt vào bề mặt của microbenchmarking trong Java. Hãy nhớ, việc benchmark không chỉ là việc viết mã nhanh mà còn là việc hiểu rõ hiệu suất của mã của bạn và đưa ra quyết định thông minh.

Khi bạn tiếp tục hành trình với Java, hãy giữ benchmark trong hộp công cụ của bạn. Nó như một bút định hướng đáng tin cậy sẽ giúp bạn đoán đường trong những mũi tên biển cả của hiệu suất phần mềm.

Chúc các bạn mãi mãi có mã chất lượng và benchmarks luôn có ý nghĩa! ??‍??‍?

Credits: Image by storyset