Jumlah nilai maksimum di enum Bagian II

Bagian Satu, Teoritis  | Bagian dua, praktis



Kami terus mencari jumlah nilai maksimum yang mungkin dalam penghitungan.
Kali ini kami akan fokus pada sisi praktis masalah dan melihat bagaimana IDE, kompiler, dan JVM akan merespons pencapaian kami.

Kandungan


  Metode Ekstrak
  Javac Tools
  Metode
  Dynamic Class-File Konstanta
    Kesulitan Mendadak
    Masa Depan yang Cerah
  Tidak Aman
  Uji
    Javac dan Alih Kinerja
  Kesimpulan
  Sumber Daya Tambahan


Alat


Javac merawat kami: ia memotong karakter yang tidak disukai dari pengidentifikasi dan melarang mewarisi darinya java.lang.Enum, jadi untuk eksperimen kami memerlukan alat lain.

Kami akan menguji hipotesis menggunakan asmtools , assembler dan disassembler untuk JVM, dan menghasilkan file kelas pada skala industri menggunakan perpustakaan ASM .

Untuk memudahkan pemahaman, esensi dari apa yang terjadi akan diduplikasi dalam pseudocode java-like.


Javac


Sebagai titik awal, adalah logis untuk mengambil hasil terbaik, dapat dicapai tanpa trik, dengan bantuan hanya satu javac. Semuanya sederhana di sini - kami membuat file sumber dengan pencacahan dan menambahkan elemen untuk itu sampai javac menolak untuk mengkompilasi itu dengan β€œkode terlalu besar” kutukan.

Cukup lama, sejak Java 1.7, angka ini disimpan pada level 2_746 elemen. Tetapi di suatu tempat setelah Java 11, ada perubahan dalam algoritma untuk menyimpan nilai-nilai di kolam konstan dan jumlah maksimum menurun menjadi 2_743. Ya, ya, hanya karena mengubah urutan elemen-elemen dalam kumpulan konstan!

Kami akan fokus pada nilai-nilai terbaik.


Metode ekstrak


Karena salah satu faktor pembatas terkait dengan ukuran bytecode di blok inisialisasi statis, kami akan mencoba membuat yang terakhir semudah mungkin.

Ingat bagaimana tampilannya pada contoh enumerasi FizzBuzzdari bagian pertama. Komentar memberikan instruksi perakitan yang sesuai.

statis {}
static  {
    Fizz = new FizzBuzz("Fizz", 0);
    //  0: new           #2                  // class FizzBuzz
    //  3: dup
    //  4: ldc           #22                 // String Fizz
    //  6: iconst_0
    //  7: invokespecial #24                 // Method "<init>":(Ljava/lang/String;I)V
    // 10: putstatic     #25                 // Field Fizz:LFizzBuzz;
    Buzz = new FizzBuzz("Buzz", 1);
    // 13: new           #2                  // class FizzBuzz
    // 16: dup
    // 17: ldc           #28                 // String Buzz
    // 19: iconst_1
    // 20: invokespecial #24                 // Method "<init>":(Ljava/lang/String;I)V
    // 23: putstatic     #30                 // Field Buzz:LFizzBuzz;
    FizzBuzz = new FizzBuzz("FizzBuzz", 2);
    // 26: new           #2                  // class FizzBuzz
    // 29: dup
    // 30: ldc           #32                 // String FizzBuzz
    // 32: iconst_2
    // 33: invokespecial #24                 // Method "<init>":(Ljava/lang/String;I)V
    // 36: putstatic     #33                 // Field FizzBuzz:LFizzBuzz;

    $VALUES = new FizzBuzz[] {
    // 39: iconst_3
    // 40: anewarray     #2                  // class FizzBuzz
        Fizz, 
    // 43: dup
    // 44: iconst_0
    // 45: getstatic     #25                 // Field Fizz:LFizzBuzz;
    // 48: aastore
        Buzz, 
    // 49: dup
    // 50: iconst_1
    // 51: getstatic     #30                 // Field Buzz:LFizzBuzz;
    // 54: aastore
        FizzBuzz
    // 55: dup
    // 56: iconst_2
    // 57: getstatic     #33                 // Field FizzBuzz:LFizzBuzz;
    // 60: aastore
    };
    // 61: putstatic     #1                  // Field $VALUES:[LFizzBuzz;
    // 64: return
}


Hal pertama yang terlintas dalam pikiran adalah menempatkan penciptaan dan pengisian array $VALUESke dalam metode yang terpisah.

$VALUES = createValues();

Mengembangkan ide ini, pembuatan instance elemen enumerasi dapat ditransfer ke metode yang sama:

static  {
    FizzBuzz[] localValues = createValues();

    int index = 0;
    Fizz = localValues[index++];
    Buzz = localValues[index++];
    FizzBuzz = localValues[index++];

    $VALUES = localValues;
}

private static FizzBuzz[] createValues() {
    return new FizzBuzz[] {
        new FizzBuzz("Fizz", 0), 
        new FizzBuzz("Buzz", 1), 
        new FizzBuzz("FizzBuzz", 2)
    };
}

Sudah lebih baik, tetapi setiap penangkapan elemen array dan kenaikan indeks selanjutnya menghabiskan biaya 6 byte, yang terlalu mahal bagi kami. Keluarkan dalam metode terpisah.


private static int valueIndex;

static  {
    $VALUES = createValues();

    valueIndex = 0;
    Fizz = nextValue();
    Buzz = nextValue();
    FizzBuzz = nextValue();
}

private static FizzBuzz nextValue() {
    return $VALUES[valueIndex++];
}

Dibutuhkan 11 byte untuk menginisialisasi $VALUES, valueIndexdan kembali dari blok inisialisasi statis , dan 65_524 byte lainnya tetap untuk menginisialisasi bidang. Inisialisasi setiap bidang membutuhkan 6 byte, yang memungkinkan kami membuat enumerasi elemen 10_920.

Pertumbuhan hampir empat kali lipat dibandingkan javac pasti harus dirayakan oleh pembuatan kode!

Kode sumber generator: ExtractMethodHugeEnumGenerator.java
Contoh kelas yang dihasilkan : ExtractMethodHugeEnum.class

Konstanta File-Dinamis Kelas


Sudah waktunya untuk mengingat tentang JEP 309 dan konstanta dinamis misteriusnya .

Intisari inovasi secara singkat:

Untuk jenis yang sudah ada didukung oleh sekelompok konstanta menambahkan satu lagi CONSTANT_Dynamic. Saat memuat kelas, tipe konstanta seperti itu diketahui, tetapi nilainya tidak diketahui. Pemuatan konstanta yang pertama mengarah ke panggilan ke metode bootstrap yang ditentukan dalam deklarasi.

Hasil dari metode ini menjadi nilai konstan. Tidak ada cara untuk mengubah nilai yang terkait dengan konstanta yang sudah diinisialisasi. Yang cukup logis untuk sebuah konstanta.

Jika Anda juga memikirkan Singleton, maka segera lupakan. Spesifikasi secara terpisah menekankan bahwa tidak ada jaminan keselamatan ulir dalam hal ini, dan metode inisialisasi dalam kode multi-ulir dapat disebut lebih dari sekali. Hanya dijamin bahwa dalam kasus beberapa panggilan ke metode bootstrap untuk konstanta yang sama, JVM akan melempar koin dan memilih salah satu dari nilai yang dihitung untuk peran nilai konstan, dan yang lainnya akan dikorbankan ke pengumpul sampah.

Secara perilaku, konstanta CONSTANT_Dynamic diselesaikan dengan menjalankan metode bootstrap pada parameter berikut:

  1. objek pencarian lokal,
  2. String yang mewakili komponen nama konstanta,
  3. Kelas mewakili tipe konstan yang diharapkan, dan
  4. argumen bootstrap yang tersisa.

As with invokedynamic, multiple threads can race to resolve, but a unique winner will be chosen and any other contending answers discarded.

Untuk memuat nilai dari kumpulan konstanta dalam bytecode, perintah disediakan ldc, ldc_wdan ldc2_w. Yang menarik bagi kami adalah yang pertama dari mereka - ldc.

Tidak seperti yang lain, ia hanya dapat memuat nilai dari 255 slot pertama dari konstanta, tetapi dibutuhkan 1 byte lebih sedikit dalam bytecode. Semua ini memberi kami penghematan hingga 255 byte dan 255 + ((65_524 - (255 * 5)) / 6) = 10_963elemen dalam enumerasi. Kali ini pertumbuhannya tidak begitu mengesankan, tetapi masih ada.

Berbekal pengetahuan ini, mari kita mulai.

Di blok inisialisasi statis, alih-alih pemanggilan metode, nextValue()kita sekarang akan memuat nilai konstanta dinamis. Nilai ordinalindeks ordinal elemen enumerasi akan diteruskan secara eksplisit, sehingga menyingkirkan lapangan valueIndex, metode pabriknextValue()dan keraguan tentang keamanan utas implementasi kami.

Sebagai metode bootstrap, kita akan menggunakan subtipe khusus MethodHandle yang meniru perilaku operator newdi Jawa. Pustaka standar untuk mendapatkan pegangan metode seperti itu menyediakan metode MethodHandles.Lookup :: findConstructor () , tetapi dalam kasus kami, JVM akan menangani pembangunan pegangan metode yang diinginkan.

Untuk menggunakan konstruktor enumerasi kami sebagai metode bootstrap, itu harus sedikit dimodifikasi dengan mengubah tanda tangan. Parameter yang diperlukan untuk metode bootstrap akan ditambahkan ke konstruktor tradisional elemen enumerasi nama dan nomor seri:

private FizzBuzz(MethodHandles.Lookup lookup, String name, Class<?> enumClass, int ordinal) {
    super(name, ordinal);
}

Dalam bentuk pseudo-code, inisialisasi akan terlihat seperti ini:

static  {
    Fizz = JVM_ldc(FizzBuzz::new, "Fizz", 0);
    Buzz = JVM_ldc(FizzBuzz::new, "Buzz", 1);
    FizzBuzz = JVM_ldc(FizzBuzz::new, "FizzBuzz", 2);

    $VALUES = createValues();
}

Dalam contoh di atas, instruksi yang ldcditetapkan sebagai pemanggilan metode JVM_ldc(), dalam bytecode di tempatnya akan menjadi instruksi JVM yang sesuai.

Karena sekarang kita memiliki konstanta terpisah untuk setiap elemen enumerasi, pembuatan dan pengisian array $VALUESjuga dapat diimplementasikan melalui konstanta dinamis. Metode bootstrap sangat sederhana:

private static FizzBuzz[] createValues(MethodHandles.Lookup lookup, String name, Class<?> clazz, FizzBuzz... elements) {
    return elements;
}

Semua trik dalam daftar parameter statis untuk konstanta dinamis ini, di sana kita akan mendaftar semua elemen yang ingin kita masukkan $VALUES:

BootstrapMethods:
  ...
  1: # 54 REF_invokeStatic FizzBuzz.createValues: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / Class; [LFizzBuzz;) [LFizzBuzz;)
    Argumen metode:
      # 1 # 0: Fizz: LFizzBuzz;
      # 2 # 0: Buzz: LFizzBuzz;
      # 3 # 0: FizzBuzz: LFizzBuzz;

JVM merusak array dari parameter statis ini dan meneruskannya ke metode bootstrap kami sebagai parameter vararg elements. Jumlah maksimum parameter statis adalah 65_535 tradisional, sehingga dijamin cukup untuk semua elemen enumerasi, berapa pun jumlahnya.

Untuk transfer dengan sejumlah besar elemen, perubahan ini akan mengurangi ukuran file kelas yang dihasilkan, dan dalam kasus ketika, karena sejumlah besar elemen, metode createValues()harus dipecah menjadi beberapa bagian, itu juga menyimpan slot di kumpulan konstan.
Dan pada akhirnya, itu sangat indah.


Kesulitan mendadak


Yang kami gagah mengatasi dengan menghasilkan kelas secara manual.

Perpustakaan tingkat tinggi menyediakan antarmuka yang nyaman dengan imbalan pembatasan kebebasan bertindak. Pustaka ASM yang kami gunakan untuk menghasilkan file kelas tidak terkecuali. Itu tidak menyediakan mekanisme untuk secara langsung mengendalikan isi kumpulan konstanta. Ini biasanya tidak terlalu penting, tetapi tidak dalam kasus kami.

Seperti yang Anda ingat, kita memerlukan 255 elemen pertama dari kumpulan konstan untuk menyimpan byte berharga di blok inisialisasi statis. Ketika konstanta dinamis ditambahkan dengan cara standar, mereka akan ditempatkan pada indeks acak dan dicampur dengan elemen lain yang tidak begitu penting bagi kita. Ini akan mencegah kita mencapai maksimum.

Fragmen kumpulan konstanta terbentuk dengan cara tradisional
Kolam konstan:
   # 1 = Utf8 FizzBuzz
   #2 = Class              #1             // FizzBuzz
   #3 = Utf8               java/lang/Enum
   #4 = Class              #3             // java/lang/Enum
   #5 = Utf8               $VALUES
   #6 = Utf8               [LFizzBuzz;
   #7 = Utf8               valueIndex
   #8 = Utf8               I
   #9 = Utf8               Fizz
  #10 = Utf8               LFizzBuzz;
  #11 = Utf8               Buzz
  #12 = Utf8               FizzBuzz
  #13 = Utf8               values
  #14 = Utf8               ()[LFizzBuzz;
  #15 = NameAndType        #5:#6          // $VALUES:[LFizzBuzz;
  #16 = Fieldref           #2.#15         // FizzBuzz.$VALUES:[LFizzBuzz;
  #17 = Class              #6             // "[LFizzBuzz;"
  #18 = Utf8               clone
  #19 = Utf8               ()Ljava/lang/Object;
  #20 = NameAndType        #18:#19        // clone:()Ljava/lang/Object;
  #21 = Methodref          #17.#20        // "[LFizzBuzz;".clone:()Ljava/lang/Object;
  ...
  #40 = NameAndType        #9:#10         // Fizz:LFizzBuzz;
  #41 = Dynamic            #0:#40         // #0:Fizz:LFizzBuzz;
  #42 = Fieldref           #2.#40         // FizzBuzz.Fizz:LFizzBuzz;
  #43 = NameAndType        #11:#10        // Buzz:LFizzBuzz;
  #44 = Dynamic            #0:#43         // #0:Buzz:LFizzBuzz;
  #45 = Fieldref           #2.#43         // FizzBuzz.Buzz:LFizzBuzz;
  #46 = NameAndType        #12:#10        // FizzBuzz:LFizzBuzz;
  #47 = Dynamic            #0:#46         // #0:FizzBuzz:LFizzBuzz;
  #48 = Fieldref           #2.#46         // FizzBuzz.FizzBuzz:LFizzBuzz;



Untungnya, ada solusi - saat membuat kelas, Anda dapat menentukan kelas sampel dari mana kumpulan konstanta dan atribut dengan deskripsi metode bootstrap akan disalin. Hanya sekarang kita harus membuatnya secara manual.

Bahkan, tidak sesulit kelihatannya pada pandangan pertama. Format file kelas cukup sederhana dan pembuatan manualnya agak membosankan, tetapi sama sekali tidak rumit.

Yang paling penting di sini adalah rencana yang jelas. Untuk menghitung dari COUNTelemen yang kita butuhkan:

  • COUNTketik catatan CONSTANT_Dynamic- konstanta dinamis kami
  • COUNTketik catatan CONSTANT_NameAndType- pasang tautan ke nama elemen enumerasi dan tipenya. Jenisnya akan sama untuk semua orang, ini adalah jenis kelas enumerasi kami.
  • COUNTketik record CONSTANT_Utf8- secara langsung nama-nama elemen enumerasi
  • COUNTcatatan tipe CONSTANT_Integer- nomor seri elemen enumerasi diteruskan ke konstruktor sebagai nilai parameterordinal
  • nama kelas saat ini dan basis, atribut, tanda tangan metode, dan detail implementasi membosankan lainnya. Mereka yang tertarik dapat melihat kode sumber generator.

Ada banyak elemen konstituen dalam kumpulan konstanta yang merujuk ke elemen-elemen lain dari pool berdasarkan indeks, jadi semua indeks yang kita butuhkan harus dihitung terlebih dahulu, elementNamesadalah daftar nama-nama elemen dari enumerasi kita:

int elementCount = elementNames.size();

int baseConDy = 1;
int baseNameAndType = baseConDy + elementCount;
int baseUtf8 = baseNameAndType + elementCount;
int baseInteger = baseUtf8 + elementCount;
int indexThisClass = baseInteger + elementCount;
int indexThisClassUtf8 = indexThisClass + 1;
int indexSuperClass = indexThisClassUtf8 + 1;
int indexSuperClassUtf8 = indexSuperClass + 1;
int indexBootstrapMethodsUtf8 = indexSuperClassUtf8 + 1;
int indexConDyDescriptorUtf8 = indexBootstrapMethodsUtf8 + 1;
int indexBootstrapMethodHandle = indexConDyDescriptorUtf8 + 1;
int indexBootstrapMethodRef = indexBootstrapMethodHandle + 1;
int indexBootstrapMethodNameAndType = indexBootstrapMethodRef + 1;
int indexBootstrapMethodName = indexBootstrapMethodNameAndType + 1;
int indexBootstrapMethodDescriptor = indexBootstrapMethodName + 1;

int constantPoolSize = indexBootstrapMethodDescriptor + 1;

Setelah itu, kami mulai menulis.

Di awal - tanda tangan file kelas, empat byte yang diketahui semua orang 0xCA 0xFE 0xBA 0xBEdan versi format file:

// Class file header
u4(CLASS_FILE_SIGNATURE);
u4(version);

Kemudian - kumpulan konstanta:

Kelompok konstanta
// Constant pool
u2(constantPoolSize);

// N * CONSTANT_Dynamic
for (int i = 0; i < elementCount; i++) {
    u1u2u2(CONSTANT_Dynamic, i, baseNameAndType + i);
}

// N * CONSTANT_NameAndType
for (int i = 0; i < elementCount; i++) {
    u1u2u2(CONSTANT_NameAndType, baseUtf8 + i, indexConDyDescriptorUtf8);
}

// N * CONSTANT_Utf8
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < elementCount; i++) {
    u1(CONSTANT_Utf8);
    utf8(elementNames.get(i));
}

// N * CONSTANT_Integer
for (int i = 0; i < elementCount; i++) {
    u1(CONSTANT_Integer);
    u4(i);
}

// ThisClass
u1(CONSTANT_Class);
u2(indexThisClassUtf8);

// ThisClassUtf8
u1(CONSTANT_Utf8);
utf8(enumClassName);

// SuperClass
u1(CONSTANT_Class);
u2(indexSuperClassUtf8);

// SuperClassUtf8
u1(CONSTANT_Utf8);
utf8(JAVA_LANG_ENUM);

// BootstrapMethodsUtf8
u1(CONSTANT_Utf8);
utf8(ATTRIBUTE_NAME_BOOTSTRAP_METHODS);

// ConDyDescriptorUtf8
u1(CONSTANT_Utf8);
utf8(binaryEnumClassName);

// BootstrapMethodHandle
u1(CONSTANT_MethodHandle);
u1(REF_newInvokeSpecial);
u2(indexBootstrapMethodRef);

// BootstrapMethodRef
u1u2u2(CONSTANT_Methodref, indexThisClass, indexBootstrapMethodNameAndType);

// BootstrapMethodNameAndType
u1u2u2(CONSTANT_NameAndType, indexBootstrapMethodName, indexBootstrapMethodDescriptor);

// BootstrapMethodName
u1(CONSTANT_Utf8);
utf8(BOOTSTRAP_METHOD_NAME);

// BootstrapMethodDescriptor
u1(CONSTANT_Utf8);
utf8(BOOTSTRAP_METHOD_DESCRIPTOR);


Setelah kolam konstan berbicara tentang pengubah akses dan bendera ( public, final, enun, dan sebagainya), nama kelas dan leluhurnya:

u2(access);
u2(indexThisClass);
u2(indexSuperClass);

Kelas dummy yang kami hasilkan tidak akan memiliki antarmuka, tidak ada bidang, tidak ada metode, tetapi akan ada satu atribut dengan deskripsi metode bootstrap:

// Interfaces count
u2(0);
// Fields count
u2(0);
// Methods count
u2(0);
// Attributes count
u2(1);

Dan di sini adalah tubuh atribut itu sendiri:

// BootstrapMethods attribute
u2(indexBootstrapMethodsUtf8);
// BootstrapMethods attribute size
u4(2 /* num_bootstrap_methods */ + 6 * elementCount);
// Bootstrap method count
u2(elementCount);

for (int i = 0; i < elementCount; i++) {
    // bootstrap_method_ref
    u2(indexBootstrapMethodHandle);
    // num_bootstrap_arguments
    u2(1);
    // bootstrap_arguments[1]
    u2(baseInteger + i);
}

Itu saja, kelas terbentuk. Kami mengambil byte ini dan membuat dari mereka ClassReader:

private ClassReader getBootstrapClassReader(int version, int access, String enumClassName, List<String> elementNames) {
    byte[] bootstrapClassBytes = new ConDyBootstrapClassGenerator(
        version,
        access,
        enumClassName,
        elementNames
    )
    .generate();

    if (bootstrapClassBytes == null) {
        return null;
    } else {
        return new ClassReader(bootstrapClassBytes);
    }
}

Itu tidak begitu sulit.

Kode Sumber Generator: ConDyBootstrapClassGenerator.java

Masa depan yang cerah


Kami menyimpang sebentar dari daftar kami:


public class DiscoverConstantValueAttribute {

    public static final String STRING = "Habrahabr, world!";

    public static final Object OBJECT = new Object();

}


Di blok inisialisasi statis kelas ini, tiba-tiba hanya akan ada satu operasi tulis, di bidang OBJECT:


static {
    OBJECT = new Object();
    //  0: new           #2                  // class java/lang/Object
    //  3: dup
    //  4: invokespecial #1                  // Method java/lang/Object."<init>":()V
    //  7: putstatic     #7                  // Field OBJECT:Ljava/lang/Object;
    // 10: return
}


Tapi bagaimana dengan itu STRING?
Tim akan membantu menjelaskan teka-teki ini javap -c -s -p -v DiscoverConstantValueAttribute.class, berikut adalah fragmen yang menarik minat kami:


public static final java.lang.String STRING;
  descriptor: Ljava/lang/String;
  flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: String Habrahabr, world!


Nilai bidang akhir statis telah dipindahkan dari blok inisialisasi ke atribut terpisah ConstantValue. Inilah yang mereka tulis tentang atribut ini di JVMS11 Β§4.7.2 :

Atribut ConstantValue mewakili nilai ekspresi konstan (JLS Β§15.28), dan digunakan sebagai berikut:
  • Jika flag ACC_STATIC dalam item access_flags dari struktur field_info diatur, maka bidang yang diwakili oleh struktur field_info diberi nilai yang diwakili oleh atribut ConstantValue sebagai bagian dari inisialisasi kelas atau antarmuka yang menyatakan bidang (Β§5.5). Ini terjadi sebelum permohonan metode inisialisasi kelas atau antarmuka kelas atau antarmuka tersebut (Β§2.9.2).
  • Jika tidak, Java Virtual Machine harus diam-diam mengabaikan atribut.


Jika atribut seperti itu terjadi pada waktu yang sama staticdan final(meskipun yang terakhir tidak diuraikan secara eksplisit di sini) dalam suatu bidang, maka bidang tersebut diinisialisasi dengan nilai dari atribut ini. Dan ini terjadi bahkan sebelum metode inisialisasi statis dipanggil.

Akan tergoda untuk menggunakan atribut ini untuk menginisialisasi elemen enumerasi, dalam bab kami sebelum terakhir hanya ada konstanta, meskipun dinamis.

Dan kami bukan yang pertama berpikir ke arah ini, ada disebutkan dalam JEP 309 ConstantValue. Sayangnya, penyebutan ini ada di bab kerja Masa Depan:

Pekerjaan di

masa depan Kemungkinan ekstensi di masa depan meliputi:

...
  • Melampirkan konstanta dinamis ke atribut ConstantValue dari bidang statis


Sementara itu, kita hanya dapat bermimpi tentang saat-saat ketika fitur ini akan beralih dari status "baik untuk dilakukan" ke "siap". Kemudian pembatasan ukuran kode dalam blok inisialisasi akan kehilangan pengaruhnya dan jumlah maksimum elemen dalam enumerasi akan menentukan batasan dari kumpulan konstan.

Menurut perkiraan kasar, dalam hal ini kita bisa berharap untuk 65 489 / 4 = 16_372elemen. Berikut 65_489adalah jumlah slot yang tidak digunakan dari pool konstan, 46 dari 65_535 kemungkinan secara teoritis pergi ke overhead. 4- jumlah slot yang diperlukan untuk deklarasi satu bidang dan konstanta dinamis yang sesuai.

Jumlah pastinya, tentu saja, dapat ditemukan hanya setelah rilis versi JDK dengan dukungan untuk fitur ini.


Tidak aman


Musuh kita adalah pertumbuhan linier dari blok inisialisasi dengan peningkatan jumlah elemen enumerasi. Jika kami telah menemukan cara untuk mengurangi inisialisasi dalam satu lingkaran, sehingga menghilangkan hubungan antara jumlah elemen dalam enumerasi dan ukuran blok inisialisasi, kami akan membuat terobosan lain.

Sayangnya, tidak ada API publik standar yang memungkinkan penulisan ke static finalbidang bahkan di dalam blok inisialisasi statis. Refleksi maupun VarHandles tidak akan membantu di sini. Satu-satunya harapan kami adalah besar dan mengerikan sun.misc.Unsafe.

Eksekusi FizzBuzz yang tidak aman mungkin terlihat seperti ini:

FizzBuzz Tidak Aman
import java.lang.reflect.Field;
import sun.misc.Unsafe;

public enum FizzBuzz {

    private static final FizzBuzz[] $VALUES;

    public static final FizzBuzz Fizz;
    public static final FizzBuzz Buzz;
    public static final FizzBuzz FizzBuzz;

    public static FizzBuzz[] values() {
        return (FizzBuzz[]) $VALUES.clone();
    }

    public static FizzBuzz valueOf(String name) {
        return (FizzBuzz) Enum.valueOf(FizzBuzz.class, name);
    }

    private FizzBuzz(String name, int ordinal) {
        super(name, ordinal);
    }

    private static FizzBuzz[] createValues() {
        return new FizzBuzz[] {
            Fizz,
            Buzz,
            FizzBuzz
        }
    }

    static  {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);

        String[] fieldNames = "Fizz,Buzz,FizzBuzz".split(",");
        for(int i = 0; i < fieldNames.length; i++) {
            String fieldName = fieldNames[i];
            Field field = FizzBuzz.class.getDeclaredField(fieldName);
            long fieldOffset = unsafe.staticFieldOffset(field);
            unsafe.putObject(FizzBuzz.class, fieldOffset, new FizzBuzz(fieldName, i));
        }

        $VALUES = createValues();
    }

}


Pendekatan ini memungkinkan kita untuk membuat enumerasi dengan sekitar 21 ribu elemen, karena lebih dari itu, kapasitas kumpulan konstanta tidak cukup.

Dokumentasi pada Enum :: ordinal () mengharuskan nilainya sesuai dengan nomor seri elemen yang sesuai dalam deklarasi enumerasi, jadi Anda harus secara eksplisit menyimpan daftar nama bidang dalam urutan yang benar, sehingga hampir dua kali lipat ukuran file kelas.

public final int ordinal ()

Mengembalikan ordinal konstanta enumerasi ini (posisinya dalam deklarasi enumnya, di mana konstanta awal diberikan ordinal nol).

Di sini API publik untuk isi kumpulan konstanta dapat membantu, kita sudah tahu bagaimana mengisinya dalam urutan yang kita butuhkan, tetapi tidak ada API semacam itu dan sepertinya tidak akan pernah ada. Metode Class :: getConstantPool () yang tersedia di OpenJDK dinyatakan sebagai paket-pribadi dan akan sangat terburu-buru untuk mengandalkannya dalam kode pengguna.

Blok inisialisasi sekarang cukup kompak dan hampir tidak bergantung pada jumlah elemen dalam enumerasi, sehingga Anda createValues()dapat menolaknya dengan menempelkan tubuhnya di loop:

static  {
    Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);

    String[] fieldNames = "Fizz,Buzz,FizzBuzz".split(",");
    FizzBuzz[] localValues = new FizzBuzz[fieldNames.length];
    for(int i = 0; i < fieldNames.length; i++) {
        String fieldName = fieldNames[i];
        Field field = FizzBuzz.class.getDeclaredField(fieldName);
        long fieldOffset = unsafe.staticFieldOffset(field);
        unsafe.putObject(
            FizzBuzz.class,
            fieldOffset,
            (localValues[i] = new FizzBuzz(fieldName, i))
        );
    }

    $VALUES = localValues;
}

Di sini proses seperti longsoran salju terjadi: bersama dengan metode createValues(), instruksi untuk membaca bidang elemen penghitungan menghilang, ketik catatan Fieldrefuntuk bidang ini menjadi tidak perlu , dan karenanya ketik NameAndTypecatatan untuk catatan jenis Fieldref. Di kumpulan konstan, 2 * < >slot dibebaskan yang dapat digunakan untuk mendeklarasikan elemen enumerasi tambahan.

Tapi tidak semuanya begitu cerah, tes menunjukkan penurunan kinerja yang signifikan: menginisialisasi kelas enumerasi dengan 65 ribu elemen membutuhkan waktu satu setengah menit yang tidak terpikirkan. Ternyata cukup cepat, "refleks melambat."

Implementasi Class :: getDeclaredField () di OpenJDK memiliki perilaku asimptotik linier dari sejumlah bidang dalam kelas, dan blok inisialisasi kami adalah kuadratik karena hal ini.

Menambahkan caching meningkatkan situasi, meskipun tidak sepenuhnya menyelesaikannya:

static  {
    Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);

    String[] fieldNames = "Fizz,Buzz,FizzBuzz".split(",");
    Field[] fields = FizzBuzz.class.getDeclaredFields();
    HashMap<String, Field> cache = new HashMap<>(fields.length);

    for(Field field : fields) {
        cache.put(field.getName(), field);
    }

    for (int i = 0; i < fieldNames.length; i++) {
        String fieldName = fieldNames[i];
        Field field = cache.get(fieldName);
        long fieldOffset = unsafe.staticFieldOffset(field);
        unsafe.putObject(
            FizzBuzz.class,
            fieldOffset,
            (localValues[i] = new FizzBuzz(fieldName, i))
        );
    }    

    $VALUES = localValues;
}

Pendekatan tidak aman yang dijelaskan dalam bab ini memungkinkan Anda untuk membuat transfer dengan jumlah elemen hingga 65_410, yang hampir 24 kali lebih banyak daripada hasil yang dapat dicapai dengan javac dan cukup dekat dengan batas teoritis 65_505 elemen yang dihitung oleh kami dalam publikasi siklus sebelumnya.


Periksa kinerja


Untuk pengujian, kami mengambil enumerasi terbesar, membuatnya menggunakan perintah java -jar HugeEnumGen.jar -a Unsafe UnsafeHugeEnum. Hasilnya, kami mendapatkan file kelas dengan ukuran 2 megabyte dan elemen 65_410.

Buat proyek Java baru di IDEA dan tambahkan kelas yang dihasilkan sebagai perpustakaan eksternal.

Hampir segera, menjadi jelas bahwa IDEA tidak siap untuk stress test seperti itu:



Pelengkapan otomatis elemen enumerasi memerlukan waktu puluhan detik baik pada ponsel i5 kuno dan pada i7 8700K yang lebih modern. Dan jika Anda mencoba menggunakan perbaikan cepat untuk menambahkan elemen yang hilang ke switch, maka IDEA bahkan berhenti menggambar ulang windows. Saya menduga itu sementara, tetapi gagal menunggu sampai selesai. Responsif selama debugging juga menyisakan banyak yang diinginkan.

Mari kita mulai dengan sejumlah kecil elemen di switch:

public class TestFew {

    public static void main(String... args) {
        for(String arg : args) {
            System.out.print(arg + " : ");

            try {
                UnsafeHugeEnum value = UnsafeHugeEnum.valueOf(arg);

                doSwitch(value);
            } catch(Throwable e) {
                e.printStackTrace(System.out);
            }
        }
    }

    private static void doSwitch(UnsafeHugeEnum value) {
        switch(value) {
            case VALUE_00001:
                System.out.println("First");
                break;
            case VALUE_31415:
                System.out.println("(int) (10_000 * Math.PI)");
                break;
            case VALUE_65410:
                System.out.println("Last");
                break;
            default:
                System.out.println("Unexpected value: " + value);
                break;
        }
    }

}

Tidak ada kejutan di sini, kompilasi dan peluncuran reguler:

$ java TestFew VALUE_00001 VALUE_00400 VALUE_31415 VALUE_65410
VALUE_00001 : First
VALUE_00400 : Unexpected value: VALUE_00400
VALUE_31415 : (int) (10_000 * Math.PI)
VALUE_65410 : Last

Bagaimana dengan item lainnya switch? Bisakah kita, misalnya, memproses switch65 ribu elemen dalam satu sekaligus?

switch(value) {
    case VALUE_00001:
    case VALUE_00002:
        ...
    case VALUE_65410:
        System.out.println("One of known values: " + value);
        break;
    default:
        System.out.println("Unexpected value: " + value);
        break;
}

Sayangnya, tidak. Ketika kami mencoba mengkompilasi, kami mendapatkan sejumlah pesan kesalahan:

$ javac -fullversion
javac full version "14.0.1+7"

$ javac TestAll.java
TestAll.java:18: error: code too large for try statement
        switch(value) {
        ^
TestAll.java:65433: error: too many constants
                break;
                ^
TestAll.java:17: error: code too large
    private static void doSwitch(UnsafeHugeEnum value) {
                        ^
TestAll.java:1: error: too many constants
public class TestAll {
       ^
4 errors



Javac dan beralih


Untuk memahami apa yang terjadi, kita harus mencari tahu bagaimana penerjemahan switchelemen-elemen enumerasi terjadi .

Spesifikasi JVM memiliki bab terpisah dalam JVMS11 Β§3.10 Kompilasi Switch , rekomendasi yang bermula untuk switchmenggunakan salah satu dari dua instruksi bytecode, tableswitchatau lookupswitch. switchKami tidak akan menemukan referensi untuk elemen string atau enumerasi dalam bab ini.

Dokumentasi terbaik adalah kode, jadi sekarang saatnya untuk menyelami sumbernya javac.

Pilihan antara tableswitchdan lookupswitchterjadi di Gen :: visitSwitch () dan tergantung pada jumlah opsi di switch. Dalam kebanyakan kasus, menang tableswitch:

// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
    nlabels > 0 &&
    table_space_cost + 3 * table_time_cost <=
    lookup_space_cost + 3 * lookup_time_cost
    ?
    tableswitch : lookupswitch;

Header tableswitchkira-kira 16 byte plus 4 byte per nilai. Jadi, switchdalam keadaan apa pun, tidak akan ada lebih banyak ( 65_535 - 16 ) / 4 = 16_379unsur.

Memang, setelah mengurangi jumlah cabang casedalam tubuh switchmenjadi 16 ribu, hanya ada satu kesalahan kompilasi, yang paling misterius:

TestAll.java:18: error: code too large for try statement
        switch(value) {
        ^

Dalam mencari sumber kesalahan, kami akan kembali sedikit lebih awal, ke tahap menghilangkan gula sintaksis. switchMetode bertanggung jawab untuk terjemahan visitEnumSwitch(), mapForEnum()dan kelas EnumMappingdi Lower.java .

Di sana kami juga menemukan komentar dokumenter kecil:

EnumMapping JavaDoc
/** This map gives a translation table to be used for enum
 *  switches.
 *
 *  <p>For each enum that appears as the type of a switch
 *  expression, we maintain an EnumMapping to assist in the
 *  translation, as exemplified by the following example:
 *
 *  <p>we translate
 *  <pre>
 *          switch(colorExpression) {
 *          case red: stmt1;
 *          case green: stmt2;
 *          }
 *  </pre>
 *  into
 *  <pre>
 *          switch(Outer$0.$EnumMap$Color[colorExpression.ordinal()]) {
 *          case 1: stmt1;
 *          case 2: stmt2
 *          }
 *  </pre>
 *  with the auxiliary table initialized as follows:
 *  <pre>
 *          class Outer$0 {
 *              synthetic final int[] $EnumMap$Color = new int[Color.values().length];
 *              static {
 *                  try { $EnumMap$Color[red.ordinal()] = 1; } catch (NoSuchFieldError ex) {}
 *                  try { $EnumMap$Color[green.ordinal()] = 2; } catch (NoSuchFieldError ex) {}
 *              }
 *          }
 *  </pre>
 *  class EnumMapping provides mapping data and support methods for this translation.
 */


Misterius tryternyata menjadi bagian dari kelas pembantu yang dibuat secara otomatis TestAll$0. Di dalam - deklarasi array statis dan kode untuk menginisialisasi.

Array memperbaiki korespondensi antara nama-nama elemen enumerasi dan switchnilai-nilai numerik yang diberikan padanya selama kompilasi , sehingga melindungi kode yang dikompilasi dari efek berbahaya refactoring.

Saat menyusun ulang, menambah yang baru, atau menghapus elemen enumerasi yang ada, beberapa dari mereka dapat mengubah nilainya ordinal()dan inilah yang melindungi tingkat ketidakwajaran tambahan.

try {
    $SwitchMap$UnsafeHugeEnum[UnsafeHugeEnum.VALUE_00001.ordinal()] = 1;
    //  9: getstatic     #2                  // Field $SwitchMap$UnsafeHugeEnum:[I
    // 12: getstatic     #3                  // Field UnsafeHugeEnum.VALUE_00001:LUnsafeHugeEnum;
    // 15: invokevirtual #4                  // Method UnsafeHugeEnum.ordinal:()I
    // 18: iconst_1
    // 19: iastore
}
// 20: goto          24
catch(NoSuchFieldError e) { }
// 23: astore_0

Kode inisialisasi sederhana dan mengkonsumsi 15 hingga 17 byte per elemen. Akibatnya, blok inisialisasi statis mengakomodasi inisialisasi tidak lebih dari 3_862 elemen. Angka ini ternyata adalah jumlah maksimum elemen enumerasi yang dapat kita gunakan switchbersama dengan implementasi saat ini javac.


Kesimpulan


Kami melihat bahwa menggunakan bahkan teknik sederhana seperti mengalokasikan pembuatan elemen enumerasi dan menginisialisasi array $VALUESke metode terpisah memungkinkan Anda untuk meningkatkan jumlah maksimum elemen dalam enumerasi dari 2_746 menjadi 10_920.

Hasil dinamis konstan dengan latar belakang pencapaian sebelumnya tidak terlihat sangat mengesankan dan memungkinkan Anda untuk mendapatkan hanya 43 elemen lebih banyak, tetapi dengan pendekatan ini jauh lebih elegan untuk menambahkan properti baru ke enumerasi - cukup modifikasi konstruktor dan berikan nilai yang diperlukan melalui parameter statis konstanta dinamis.

Jika suatu saat di masa depan atribut ConstantValueakan diajarkan untuk memahami konstanta dinamis, jumlah ini bisa naik menjadi 10 ribu menjadi 16.

Gunakansun.misc.Unsafememungkinkan Anda membuat lompatan raksasa dan meningkatkan jumlah elemen maksimum menjadi 65_410. Tetapi jangan lupa bahwa Unsafeini adalah API eksklusif yang dapat menghilang seiring waktu dan penggunaannya merupakan risiko yang cukup besar, seperti yang diperingatkan javac secara langsung:

Test.java:3: warning: Unsafe is internal proprietary API and may be removed in a future release
import sun.misc.Unsafe;
               ^

Tapi, ternyata, tidak cukup untuk menghasilkan enumerasi raksasa, Anda juga harus bisa menggunakannya.

Saat ini, ada masalah dengan dukungan enumerasi semacam itu baik dari IDE dan pada tingkat kompiler Java.

Sejumlah besar bidang di kelas dapat menurunkan daya tanggap IDE baik saat mengedit maupun selama debugging. Terkadang hingga hang lengkap.

Pembatasan yang diberlakukan oleh format file kelas dan detail implementasi javac menjadikannya mustahil untuk menggunakan switchlebih dari 3_862 elemen dalam kode pada saat yang bersamaan. Dari aspek positif, perlu disebutkan bahwa elemen-elemen ini bisa saja berubah-ubah.

Peningkatan lebih lanjut dari hasil hanya dimungkinkan melalui penyempurnaan kompiler Java, tetapi ini adalah cerita yang sama sekali berbeda.


Bahan tambahan


Kode sumber GitHub: https://github.com/Maccimo/HugeEnumGeneratorArticle

Mengumpulkan file JAR: https://github.com/Maccimo/HugeEnumGeneratorArticle/releases/tag/v1.0

Bantuan Startup yang Didukung

Huge enumeration generator

    https://github.com/Maccimo/HugeEnumGeneratorArticle

Additional information (in Russian):

    https://habr.com/ru/post/483392/
    https://habr.com/ru/post/501870/

Usage:
    java -jar HugeEnumGen.jar [ <options> ] <enum name>

    <enum name>
        An enumeration class name.
        Should be a valid Java identifier. May contain package name.

Options:

    -d <directory>
        Output directory path.
        Current working directory by default.

    -e <item list file>
        Path to UTF8-encoded text file with list of enumeration item names.
        Item names will be autogenerated if absent.
        Mutually exclusive with the -c option.

    -c <count>
        Count of autogenerated enumeration item names.
        Mutually exclusive with the -e option.
        Default value: Algorithm-depended

    -a <algorithm>
        Enumeration generation algorithm.
        Supported algorithms:
          ConDy          - Employ Constant Dynamic (JEP 309) for enum elements initialization
          ExtractMethod  - Extract enum elements initialization code to separate method
          Unsafe         - Employ sun.misc.Unsafe for enum elements initialization

        Default algorithm: ExtractMethod

    -h / -?
        Show this help page.

Example:

    java -jar HugeEnumGen.jar -d ./bin -c 2020 com.habr.maccimo.HugeEnum2020



All Articles