Operator yang tersisa di int menyebabkan java.util.Objects.requireNonNull?

12

Saya mencoba mendapatkan kinerja sebanyak mungkin dari beberapa metode internal.

Kode Java adalah:

List<DirectoryTaxonomyWriter> writers = Lists.newArrayList();
private final int taxos = 4;

[...]

@Override
public int getParent(final int globalOrdinal) throws IOException {
    final int bin = globalOrdinal % this.taxos;
    final int ordinalInBin = globalOrdinal / this.taxos;
    return this.writers.get(bin).getParent(ordinalInBin) * this.taxos + bin; //global parent
}

Di profiler saya, saya melihat ada pengeluaran CPU 1% java.util.Objects.requireNonNull, tapi saya bahkan tidak menyebutnya. Saat memeriksa bytecode, saya melihat ini:

 public getParent(I)I throws java/io/IOException 
   L0
    LINENUMBER 70 L0
    ILOAD 1
    ALOAD 0
    INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;)Ljava/lang/Object;
    POP
    BIPUSH 8
    IREM
    ISTORE 2

Jadi kompiler menghasilkan cek (tidak berguna) ini. Saya bekerja pada primitif, yang tidak bisa nulllagian, jadi mengapa kompiler menghasilkan baris ini? Apakah ini bug? Atau perilaku 'normal'?

(Saya mungkin bekerja dengan bitmask, tapi saya hanya ingin tahu)

[MEMPERBARUI]

  1. Operator tampaknya tidak ada hubungannya dengan itu (lihat jawaban di bawah)

  2. Menggunakan compiler eclipse (versi 4.10) saya mendapatkan hasil yang lebih masuk akal ini:

    getParent publik (I) saya melempar java / io / IOException 
       L0
        LINENUMBER 77 L0
        ILOAD 1
        ICONST_4
        IREM
        ISTORE 2
       L1
        LINENUMBER 78 L

Jadi itu lebih masuk akal.

RobAu
sumber
@Lino yakin, tapi itu tidak benar-benar relevan untuk jalur 70 dengan menyebabkanINVOKESTATIC
RobAu
Kompiler apa yang Anda gunakan? Normal javactidak menghasilkan ini.
apangin
Kompiler apa yang Anda gunakan? Versi Java, Openjdk / Oracle / dll.? Sunting: sial, @apangin lebih cepat, maaf
lugiorgi
1
Ini dikompilasi dari Intellij 2019.3, dengan java 11, openjdk version "11.0.6" 2020-01-14di ubuntu 64 bit.
RobAu

Jawaban:

3

Kenapa tidak?

Asumsi

class C {
    private final int taxos = 4;

    public int test() {
        final int a = 7;
        final int b = this.taxos;
        return a % b;
    }
}

panggilan seperti c.test()di mana cdinyatakan sebagai C harus membuang ketika cadalah null. Metode Anda setara dengan

    public int test() {
        return 3; // `7 % 4`
    }

saat Anda bekerja dengan konstanta saja. Dengan testtidak statis, pemeriksaan harus dilakukan. Biasanya, ini akan dilakukan secara implisit ketika suatu bidang diakses atau metode non-statis dipanggil, tetapi Anda tidak melakukannya. Jadi diperlukan pemeriksaan eksplisit. Satu kemungkinan adalah menelepon Objects.requireNonNull.

Bytecode

Jangan lupa bahwa bytecode pada dasarnya tidak relevan untuk kinerja. Tugasnya javacadalah untuk menghasilkan beberapa bytecode yang pelaksanaannya sesuai dengan kode sumber Anda. Ini tidak dimaksudkan untuk melakukan setiap optimasi, sebagai kode dioptimalkan biasanya lebih lama dan lebih sulit untuk menganalisis, sementara bytecode adalah benar-benar kode sumber untuk compiler mengoptimalkan JIT. Jadi javacdiharapkan tetap sederhana ....

Penampilan

Di profiler saya, saya melihat ada 1% pengeluaran CPU di java.util.Objects.requireNonNull

Saya akan menyalahkan profiler terlebih dahulu. Membuat profil Java cukup sulit dan Anda tidak pernah dapat mengharapkan hasil yang sempurna.

Anda mungkin harus mencoba membuat metode ini statis. Anda tentu harus membaca artikel ini tentang cek kosong .

maaartinus
sumber
1
Terima kasih @maaartinus atas jawaban wawasan Anda. Saya pasti akan membaca artikel Anda yang ditautkan.
RobAu
1
“Dengan tes yang tidak statis, pemeriksaan harus dilakukan” Sebenarnya, tidak ada alasan untuk menguji apakah thisitu non- null. Seperti yang Anda katakan pada diri sendiri, panggilan seperti c.test()harus gagal ketika cadalah nulldan itu harus segera gagal, bukannya memasuki metode. Jadi di dalam test(), thistidak pernah bisa null(kalau tidak akan ada bug JVM). Jadi tidak perlu diperiksa. Perbaikan sebenarnya harus mengubah bidang taxosmenjadi static, karena tidak ada gunanya menyimpan memori dalam setiap contoh untuk konstanta waktu kompilasi. Kemudian, apakah test()ini statictidak relevan.
Holger
2

Yah sepertinya pertanyaan saya 'salah' karena tidak ada hubungannya dengan operator, melainkan bidang itu sendiri. Masih tidak tahu kenapa ..

   public int test() {
        final int a = 7;
        final int b = this.taxos;
        return a % b;
    }

Yang berubah menjadi:

  public test()I
   L0
    LINENUMBER 51 L0
    BIPUSH 7
    ISTORE 1
   L1
    LINENUMBER 52 L1
    ALOAD 0
    INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;)Ljava/lang/Object;
    POP
    ICONST_4
    ISTORE 2
   L2
    LINENUMBER 53 L2
    BIPUSH 7
    ILOAD 2
    IREM
    IRETURN
RobAu
sumber
1
Mungkinkah kompiler takut dengan thisreferensi itu null? Apakah ini mungkin?
atalantus
1
Tidak itu tidak masuk akal, kecuali jika kompiler mengkompilasi bidang ke suatu Integercara, dan ini adalah hasil dari autoboxing?
RobAu
1
Tidak ALOAD 0referensi this? Jadi masuk akal (tidak juga) bahwa kompiler menambahkan nullcheck
Lino
1
Jadi kompiler sebenarnya menambahkan cek kosong untuk this? Hebat: /
RobAu
1
Saya akan mencoba membuat sepotong kode minimal dengan baris perintah javacuntuk memverifikasi besok; dan jika itu juga menunjukkan perilaku ini, saya pikir itu mungkin javac-bug?
RobAu
2

Pertama, inilah contoh minimal dari perilaku ini:

/**
 * OS:              Windows 10 64x
 * javac version:   13.0.1
 */
public class Test {
    private final int bar = 5;

    /**
     * public int foo();
     *   Code:
     *     0: iconst_5
     *     1: ireturn
     */
    public int foo() {
        return bar;
    }

    /**
     * public int foo2();
     *   Code:
     *     0: aload_0
     *     1: invokestatic  #13     // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
     *     4: pop
     *     5: iconst_5
     *     6: ireturn
     */
    public int foo2() {
        return this.bar;
    }
}

Perilaku ini karena bagaimana kompiler Java mengoptimalkan konstanta waktu kompilasi .

Perhatikan bahwa dalam kode byte foo()tidak ada referensi objek diakses untuk mendapatkan nilai bar. Itu karena itu adalah konstanta waktu kompilasi dan dengan demikian JVM dapat dengan mudah menjalankan iconst_5operasi untuk mengembalikan nilai ini.

Saat berubah barmenjadi konstanta waktu non-kompilasi (baik dengan menghapus finalkata kunci atau tidak menginisialisasi dalam deklarasi tetapi di dalam konstruktor) Anda akan mendapatkan:

/**
 * OS:              Windows 10 64x
 * javac version:   13.0.1
 */
public class Test2 {
    private int bar = 5;

    /**
     * public int foo();
     *   Code:
     *     0: aload_0
     *     1: getfield      #7
     *     4: ireturn
     */
    public int foo() {
        return bar;
    }

    /**
     * public int foo2();
     *   Code:
     *     0: aload_0
     *     1: getfield      #7
     *     4: ireturn
     */
    public int foo2() {
        return this.bar;
    }
}

di mana aload_0mendorong referensi dari thiske operan stack untuk kemudian mendapatkan barbidang obyek ini.

Di sini kompiler cukup pintar untuk memperhatikan bahwa aload_0( thisreferensi jika fungsi anggota) secara logis tidak bisa null.

Sekarang apakah kasus Anda sebenarnya adalah optimasi kompiler yang hilang?

Lihat jawaban @maaartinus.

atalantus
sumber