Pertimbangkan dua cuplikan kode berikut pada array dengan panjang 2:
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
dan
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
Saya akan berasumsi bahwa kinerja dua potong ini harus sama setelah pemanasan yang memadai.
Saya telah memeriksa ini menggunakan kerangka kerja pembandingan mikro JMH seperti yang dijelaskan misalnya di sini dan sini dan mengamati bahwa potongan kedua lebih dari 10% lebih cepat.
Pertanyaan: mengapa Java belum mengoptimalkan cuplikan pertama saya menggunakan teknik loop terbuka dasar?
Secara khusus, saya ingin memahami yang berikut:
- Aku bisa dengan mudah menghasilkan kode yang optimal untuk kasus 2 filter dan masih dapat bekerja dalam kasus nomor lain dari filter (bayangkan pembangun sederhana):
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters)
. Bisakah JITC melakukan hal yang sama dan jika tidak, mengapa? - Dapatkah JITC mendeteksi bahwa ' filter.length == 2 ' adalah kasus yang paling sering dan menghasilkan kode yang optimal untuk kasus ini setelah beberapa pemanasan? Ini harus hampir seoptimal versi yang tidak dikontrol secara manual.
- Dapatkah JITC mendeteksi bahwa contoh tertentu digunakan sangat sering dan kemudian menghasilkan kode untuk contoh khusus ini (yang diketahui bahwa jumlah filter selalu 2)?
Memperbarui: mendapat jawaban bahwa JITC hanya berfungsi pada level kelas. OK mengerti.
Idealnya, saya ingin menerima jawaban dari seseorang dengan pemahaman mendalam tentang cara kerja JITC.
Detail menjalankan patok banding:
- Sudah mencoba versi terbaru Java 8 OpenJDK dan Oracle HotSpot, hasilnya mirip
- Bendera Java yang digunakan: -Xmx4g -Xms4g -server -Xbatch -XX: CICompilerCount = 2 (mendapat hasil yang sama tanpa bendera mewah juga)
- Ngomong-ngomong, saya mendapatkan rasio run time yang sama jika saya menjalankannya beberapa miliar kali dalam satu lingkaran (bukan melalui JMH), yaitu snippet kedua selalu jelas lebih cepat
Output patokan khas:
Mode Benchmark (filterIndex) Cnt Score Unit Kesalahan
LoopUnrollingBenchmark.runBenchmark 0 rata-rata 400 44,202 ± 0,224 ns / op
LoopMengontrolBenchmark.runBenchmark 1 avgt 400 38,347 ± 0,063 ns / op
(Baris pertama sesuai dengan snippet pertama, baris kedua - ke baris kedua.
Kode benchmark lengkap:
public class LoopUnrollingBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkData {
public Filter[] filters;
@Param({"0", "1"})
public int filterIndex;
public int num;
@Setup(Level.Invocation) //similar ratio with Level.TRIAL
public void setUp() {
filters = new Filter[]{new FilterChain1(), new FilterChain2()};
num = new Random().nextInt();
}
}
@Benchmark
@Fork(warmups = 5, value = 20)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public int runBenchmark(BenchmarkData data) {
Filter filter = data.filters[data.filterIndex];
int sum = 0;
int num = data.num;
if (filter.isOK(num)) {
++sum;
}
if (filter.isOK(num + 1)) {
++sum;
}
if (filter.isOK(num - 1)) {
++sum;
}
if (filter.isOK(num * 2)) {
++sum;
}
if (filter.isOK(num * 3)) {
++sum;
}
if (filter.isOK(num * 5)) {
++sum;
}
return sum;
}
interface Filter {
boolean isOK(int i);
}
static class Filter1 implements Filter {
@Override
public boolean isOK(int i) {
return i % 3 == 1;
}
}
static class Filter2 implements Filter {
@Override
public boolean isOK(int i) {
return i % 7 == 3;
}
}
static class FilterChain1 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
}
static class FilterChain2 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
}
private static Filter[] createLeafFilters() {
Filter[] filters = new Filter[2];
filters[0] = new Filter1();
filters[1] = new Filter2();
return filters;
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
@Setup(Level.Invocation)
: tidak yakin itu membantu (lihat javadoc).
final
, tetapi JIT tidak melihat bahwa semua instance kelas akan mendapatkan array dengan panjang 2. Untuk melihat itu, ia harus masuk ke dalam createLeafFilters()
metode dan menganalisis kode yang cukup dalam untuk mengetahui bahwa array akan selalu 2 panjang. Mengapa Anda yakin pengoptimal JIT akan menyelami kode Anda?