العدد الأقصى للقيم في تعداد الجزء الثاني

الجزء الأول النظري  | الجزء الثاني عملي



نواصل البحث عن أكبر عدد ممكن من القيم في التعداد.
هذه المرة سوف نركز على الجانب العملي للقضية ونرى كيف ستستجيب IDE والمجمع و JVM لإنجازاتنا.

المحتوى


  أدوات
  Javac
  طريقة الاستخراج
  ديناميكي ملف الطبقة الثوابت
    صعوبات مفاجئة
    مستقبل مشرق
  غير آمن
  اختبار
    Javac والتبديل أداء
  الاستنتاج
  موارد إضافية


أدوات


تعتني بنا Javac: فهي تقطع الشخصيات التي لا تحبها من المعرفات وتحظر الموروث منها java.lang.Enum، لذلك بالنسبة للتجارب ، نحتاج إلى أدوات أخرى.

سنقوم باختبار الفرضيات باستخدام asmtools - المجمع والفك لـ JVM ، وننشئ ملفات فئة على نطاق صناعي - باستخدام مكتبة ASM .

من أجل بساطة الفهم ، سيتم تكرار جوهر ما يحدث في كود زائف يشبه جافا.


جافاك


كنقطة انطلاق ، من المنطقي أن تأخذ أفضل نتيجة ، يمكن تحقيقها بدون حيل ، بمساعدة واحدة فقط javac. كل شيء بسيط هنا - ونحن إنشاء ملف مصدر التعداد وإضافة عناصر إلى أنه حتى يرفض javac لتجميع ذلك مع لعنة "رمز كبير جدا".

منذ وقت طويل جدًا ، منذ Java 1.7 ، تم الاحتفاظ بهذا الرقم عند مستوى 2_746 عنصرًا. ولكن في مكان ما بعد Java 11 ، كانت هناك تغييرات في الخوارزمية لتخزين القيم في التجمع الثابت وانخفض العدد الأقصى إلى 2_743. نعم ، نعم ، فقط بسبب تغيير ترتيب العناصر في مجموعة الثوابت!

سنركز على أفضل القيم.


طريقة الاستخراج


نظرًا لأن أحد العوامل المقيدة يرتبط بحجم الرمز الثانوي في كتلة التهيئة الثابتة ، فسنحاول جعل الأخير أسهل ما يمكن.

تذكر كيف تبدو على مثال العد FizzBuzzمن الجزء الأول. توفر التعليقات تعليمات التجميع المناسبة.

ثابتة {}
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
}


أول ما يتبادر إلى الذهن هو وضع إنشاء المصفوفة وملئها $VALUESفي طريقة منفصلة.

$VALUES = createValues();

بتطوير هذه الفكرة ، يمكن نقل إنشاء مثيلات عناصر التعداد إلى نفس الطريقة:

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)
    };
}

أفضل بالفعل ، ولكن كل التقاط لعنصر مصفوفة وزيادة الفهرس اللاحقة يكلف 6 بايت ، وهو أمر مكلف للغاية بالنسبة لنا. ضعهم في طريقة منفصلة.


private static int valueIndex;

static  {
    $VALUES = createValues();

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

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

يستغرق تهيئة 11 بايت $VALUES، valueIndexوالعودة من كتلة التهيئة الثابتة ، وتبقى 65_524 بايت إضافية لتهيئة الحقول. تتطلب تهيئة كل حقل 6 بايت ، مما يمكننا من إنشاء تعداد لعناصر 10_920.

ما يقرب من أربعة أضعاف النمو مقارنة مع javac يجب بالتأكيد الاحتفال به من خلال إنشاء التعليمات البرمجية!

رمز مصدر المولد: ExtractMethodHugeEnumGenerator.java
مثال للفئة التي تم إنشاؤها : ExtractMethodHugeEnum.class

ثوابت ديناميكية ملف الطبقة


حان الوقت لتذكر JEP 309 وثوابته الديناميكية الغامضة .

جوهر الابتكار باختصار:

أضافت الأنواع الموجودة بالفعل المدعومة بمجموعة من الثوابت نوعًا آخر CONSTANT_Dynamic. عند تحميل فئة ، يُعرف نوع هذا الثابت ، ولكن قيمته غير معروفة. يؤدي التحميل الأول للثابت إلى استدعاء طريقة التمهيد المحددة في إعلانها.

تصبح نتيجة هذه الطريقة قيمة ثابتة. لا توجد طرق لتغيير القيمة المرتبطة بثابت تمت تهيئته بالفعل. وهو أمر منطقي تمامًا للثابت.

إذا فكرت أيضًا في Singleton ، فعليك نسيانها على الفور. تؤكد المواصفات بشكل منفصل أنه لا يوجد ضمان لسلامة الخيط في هذه الحالة ، ويمكن استدعاء طريقة التهيئة في التعليمات البرمجية متعددة الخيوط أكثر من مرة. من المضمون فقط أنه في حالة العديد من المكالمات إلى طريقة التمهيد لنفس الثابت ، فإن JVM سوف يرمي عملة معدنية ويحدد إحدى القيم المحسوبة لدور القيمة الثابتة ، وسيتم التضحية بالقيم الأخرى إلى جامع القمامة.

بشكل سلوكي ، يتم حل ثابت CONSTANT_Dinamic عن طريق تنفيذ أسلوب التشغيل الخاص به على المعلمات التالية:

  1. كائن بحث محلي ،
  2. السلسلة التي تمثل مكون اسم الثابت ،
  3. الفئة التي تمثل النوع الثابت المتوقع ، و
  4. أي وسيطات التمهيد المتبقية.

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

لتحميل القيم من مجموعة الثوابت في الرمز الثانوي ، يتم توفير الأوامر ldc، ldc_wو ldc2_w. من يهمنا هو أولهم - ldc.

إنه ، على عكس الآخرين ، قادر على تحميل القيم فقط من أول 255 فتحة من التجمع الثابت ، ولكنه يأخذ 1 بايت أقل في البايت كود. كل هذا يعطينا توفير يصل إلى 255 بايت 255 + ((65_524 - (255 * 5)) / 6) = 10_963وعنصر في التعداد. هذه المرة ليس النمو مثيرًا للإعجاب ، ولكنه لا يزال موجودًا.

مسلحين بهذه المعرفة ، لنبدأ.

في كتلة التهيئة الثابتة ، بدلاً من استدعاءات الطريقة ، nextValue()سنقوم الآن بتحميل قيمة الثابت الديناميكي. ordinalسيتم تمرير قيمة المؤشر الترتيبي لعنصر التعداد بشكل صريح ، وبالتالي التخلص من الحقل valueIndex، طريقة المصنعnextValue()والشكوك حول سلامة موضوع التنفيذ لدينا.

كطريقة تمهيد ، سنستخدم نوعًا فرعيًا خاصًا من MethodHandle يحاكي سلوك عامل newفي Java. توفر المكتبة القياسية طريقة MethodHandles.Lookup :: findConstructor () للحصول على مثل هذه الطريقة ، ولكن في حالتنا ، ستهتم JVM ببناء مقبض الطريقة الضروري.

لاستخدام مُنشئ تعدادنا كطريقة تمهيد ، يجب تعديله قليلاً عن طريق تغيير التوقيع. ستتم إضافة المعلمات المطلوبة لطريقة التمهيد إلى المُنشئ التقليدي لعنصر تعداد الاسم والرقم التسلسلي:

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

في شكل كود زائف ، ستبدو التهيئة على النحو التالي:

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

    $VALUES = createValues();
}

في المثال أعلاه ، تم ldcتعيين التعليمات كمكالمات طرق JVM_ldc()، في الرمز الثانوي في مكانها ستكون تعليمات JVM المقابلة.

نظرًا لأن لدينا الآن ثابتًا منفصلاً لكل عنصر من عناصر التعداد ، $VALUESيمكن أيضًا تنفيذ إنشاء الصفيف وملئه من خلال ثابت ديناميكي. طريقة التمهيد بسيطة للغاية:

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

كل الحيلة في قائمة المعلمات الثابتة لهذا الثابت الديناميكي ، سنقوم بإدراج جميع العناصر التي نريد وضعها في $VALUES:

طرق التمهيد:
  ...
  1: # 54 REF_invokeStatic FizzBuzz.createValues: (Ljava / lang / invoke / MethodHandles $ Lookup؛ Ljava / lang / String؛ Ljava / lang / Class؛ [LFizzBuzz؛) [LFizzBuzz؛
    الحجج الأسلوب:
      # 1 # 0: الفوران: LFizzBuzz ؛
      # 2 # 0: الطنين: LFizzBuzz ؛
      # 3 # 0: FizzBuzz: LFizzBuzz ؛

يفسد JVM المصفوفة من هذه المعلمات الثابتة ويمررها إلى طريقة التمهيد الخاصة بنا كمعلمة vararg elements. الحد الأقصى لعدد المعلمات الثابتة هو 65_535 التقليدي ، لذلك فمن المضمون أن يكون كافياً لجميع عناصر العد ، بغض النظر عن عددها.

بالنسبة للتحويلات التي تحتوي على عدد كبير من العناصر ، سيقلل هذا التغيير من حجم ملف الفئة الناتج ، وفي حالة أنه ، بسبب العدد الكبير من العناصر ، createValues()كان يجب تقسيم الطريقة إلى عدة أجزاء ، كما أنها تحفظ الفتحات في التجمع الثابت.
وفي النهاية ، إنها جميلة.


صعوبات مفاجئة


وهو ما نتغلب عليه بشكل بطولي من خلال إنشاء صفوف يدويًا.

توفر المكتبات عالية المستوى واجهة مناسبة مقابل بعض القيود على حرية العمل. مكتبة ASM التي نستخدمها لإنشاء ملفات الفئة ليست استثناءً. لا يوفر آليات للتحكم المباشر في محتويات مجموعة الثوابت. عادة ما لا يكون هذا مهمًا جدًا ، ولكن ليس في حالتنا.

كما تتذكر ، نحتاج إلى أول 255 عنصرًا من التجمع الثابت لحفظ وحدات البايت الثمينة في كتلة التهيئة الثابتة. عند إضافة ثوابت ديناميكية بطريقة قياسية ، سيتم وضعها في مؤشرات عشوائية ومختلطة مع عناصر أخرى ليست حاسمة بالنسبة لنا. هذا سيمنعنا من الوصول إلى الحد الأقصى.

جزء من مجموعة من الثوابت تتشكل بالطريقة التقليدية
Constant pool:
   #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;



لحسن الحظ ، يوجد حل بديل - عند إنشاء فئة ، يمكنك تحديد فئة فئة يتم من خلالها نسخ مجموعة من الثوابت وسمة مع وصف لطرق التمهيد. الآن فقط علينا إنشاؤها يدويًا.

في الواقع ، ليس الأمر صعبًا كما قد يبدو للوهلة الأولى. تنسيق ملف الفصل بسيط للغاية ، كما أن تكوينه اليدوي عملية شاقة إلى حد ما ، ولكنها ليست معقدة على الإطلاق.

أهم شيء هنا هو خطة واضحة. لتعداد من COUNTالعناصر التي نحتاجها:

  • COUNTاكتب السجلات CONSTANT_Dynamic- ثوابتنا الديناميكية
  • COUNTسجلات الكتابة CONSTANT_NameAndType- أزواج من الروابط إلى اسم عنصر التعداد ونوعه. سيكون النوع هو نفسه بالنسبة للجميع ، وهذا هو نوع الفصل في تعدادنا.
  • COUNTاكتب السجلات CONSTANT_Utf8- أسماء عناصر التعداد مباشرة
  • COUNTسجلات النوع CONSTANT_Integer- الأرقام التسلسلية لعناصر التعداد التي تم تمريرها إلى المُنشئ كقيمة معلمةordinal
  • أسماء الفئات الحالية والأساسية ، والسمات ، وتوقيعات الطريقة وتفاصيل التنفيذ المملة الأخرى. يمكن للمهتمين النظر في شفرة المصدر للمولد.

هناك الكثير من العناصر المكونة في مجموعة الثوابت التي تشير إلى عناصر أخرى من التجمع حسب الفهرس ، لذلك يجب حساب جميع المؤشرات التي نحتاجها مسبقًا ، elementNamesوهي قائمة بأسماء عناصر تعدادنا:

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;

بعد ذلك ، نبدأ في الكتابة.

في البداية - توقيع ملف الفئة ، البايتات الأربعة المعروفة للجميع 0xCA 0xFE 0xBA 0xBEوإصدار تنسيق الملف:

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

ثم - مجموعة من الثوابت:

تجمع الثوابت
// 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);


بعد ثابت بركة يتحدث عن معدلات الوصول والأعلام ( public، final، enun، وهلم جرا)، اسم الفئة وسلفها:

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

لن يكون للفئة الوهمية التي أنشأناها واجهات ، ولا حقول ، ولا طرق ، ولكن ستكون هناك سمة واحدة مع وصف طرق التمهيد:

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

وهنا جسم السمة نفسها:

// 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);
}

هذا كل شيء ، تتكون الطبقة. نأخذ هذه البايتات وننشئ منها 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);
    }
}

لم يكن الأمر صعبًا.

رمز مصدر المولد: ConDyBootstrapClassGenerator.java

مستقبل مشرق


نحن نتطرق لفترة وجيزة من قوائمنا:


public class DiscoverConstantValueAttribute {

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

    public static final Object OBJECT = new Object();

}


في كتلة التهيئة الثابتة لهذه الفئة ، ستحدث فجأة عملية كتابة واحدة فقط ، في الحقل 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
}


ولكن ماذا عن STRING؟
سيساعد الفريق في تسليط الضوء على هذا اللغز javap -c -s -p -v DiscoverConstantValueAttribute.class، وهنا الجزء الذي يهمنا:


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


تم نقل قيمة الحقل النهائي الثابت من كتلة التهيئة إلى سمة منفصلة ConstantValue. إليك ما يكتبون عن هذه السمة في JVMS11 §4.7.2 :

تمثل سمة ConstantValue قيمة تعبير ثابت (JLS §15.28) ، وتستخدم على النحو التالي:
  • إذا تم تعيين علامة ACC_STATIC في عنصر access_flags لبنية field_info ، فسيتم تعيين الحقل الذي يمثله هيكل field_info القيمة التي تمثلها سمة ConstantValue كجزء من تهيئة الفئة أو الواجهة التي تعلن عن الحقل (الفقرة 5.5). يحدث هذا قبل استدعاء طريقة تهيئة الطبقة أو الواجهة لتلك الفئة أو الواجهة (الفقرة 2.9.2).
  • خلاف ذلك ، يجب أن يتجاهل Java Virtual Machine السمة بصمت.


إذا تم العثور على هذه السمة في وقت واحد staticو final(على الرغم من أن هذا الأخير لم يتم ذكره صراحةً هنا) ، فسيتم تهيئة هذا الحقل بالقيمة من هذه السمة. ويحدث هذا حتى قبل استدعاء طريقة التهيئة الثابتة.

سيكون من المغري استخدام هذه السمة لتهيئة عناصر التعداد ، في فصلنا قبل الأخير كان هناك ثوابت فقط ، وإن كانت ديناميكية.

ولسنا أول من يفكر في هذا الاتجاه ، هناك ذكر في JEP 309 ConstantValue. لسوء الحظ ، هذا ذكر في فصل عمل المستقبل:

العمل

المستقبلي تشمل التمديدات المستقبلية المحتملة:

...
  • إرفاق الثوابت الديناميكية بسمة ConstantValue للحقول الثابتة


في هذه الأثناء ، يمكننا فقط أن نحلم بالأوقات التي ستنتقل فيها هذه الميزة من الحالة "من الجيد فعلها" إلى "جاهزة". ثم ستفقد القيود المفروضة على حجم التعليمات البرمجية في كتلة التهيئة تأثيرها وسيحدد الحد الأقصى لعدد العناصر في التعداد حدود التجمع الثابت.

وفقًا لتقديرات تقريبية ، في هذه الحالة يمكننا أن نأمل في 65 489 / 4 = 16_372عنصر. هذا 65_489هو عدد الفتحات غير المشغولة للمجمع الثابت ، 46 منها من 65_535 الممكنة نظريًا ذهبت إلى الأعلى. 4- عدد الفواصل الزمنية المطلوبة للإعلان عن حقل واحد والثابت الدينامي المقابل.

بالطبع ، لا يمكن العثور على الرقم الدقيق إلا بعد إصدار نسخة JDK مع دعم هذه الميزة.


غير آمن


عدونا هو النمو الخطي لكتلة التهيئة مع زيادة في عدد عناصر التعداد. إذا وجدنا طريقة للحد من التهيئة في حلقة ، وبالتالي إزالة العلاقة بين عدد العناصر في العد وحجم كتلة التهيئة ، فإننا سنحقق اختراقا آخر.

لسوء الحظ ، لا تسمح أي من واجهات برمجة التطبيقات العامة القياسية بالكتابة إلى static finalالحقول حتى داخل كتلة التهيئة الثابتة. لن يساعد التأمل ولا VarHandles هنا. أملنا الوحيد عظيم ورهيب sun.misc.Unsafe.

قد يبدو التنفيذ غير الآمن لـ FizzBuzz كالتالي:

FizzBuzz غير آمنة
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();
    }

}


يسمح لنا هذا النهج بإنشاء تعداد يحتوي على ما يقرب من 21 ألف عنصر ؛ ولأكثر من ذلك ، فإن سعة مجموعة الثوابت ليست كافية.

يتطلب التوثيق الموجود في Enum :: ordinal () أن تتطابق قيمته مع الرقم المتسلسل للعنصر المقابل في إعلان التعداد ، لذا يجب عليك تخزين قائمة أسماء الحقول بشكل صريح بالترتيب الصحيح ، وبالتالي مضاعفة حجم ملف الفئة تقريبًا.

الترقيم النهائي العام ()

يعيد ترتيبي ثابت التعداد هذا (موضعه في إعلان التعداد ، حيث يتم تعيين الثابت الأولي ترتيباً من الصفر).

هنا يمكن أن تساعد واجهة برمجة التطبيقات العامة لمحتويات مجموعة الثوابت ، فنحن نعرف بالفعل كيفية ملئها بالترتيب الذي نحتاج إليه ، ولكن لا توجد واجهة برمجة تطبيقات مثل هذه ومن غير المحتمل أن تكون موجودة على الإطلاق. تم التصريح عن أسلوب Class :: getConstantPool () المتوفر في OpenJDK كحزمة خاصة وسيكون من الصعب الاعتماد عليها في كود المستخدم.

أصبحت كتلة التهيئة الآن مضغوطة تمامًا وتقريباً مستقلة عن عدد العناصر في التعداد ، لذا يمكنك createValues()رفضها عن طريق تضمين جسمها في الحلقة:

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;
}

هنا تحدث عملية شبيهة بالانهيار الجليدي: إلى جانب الطريقة createValues()، تختفي تعليمات قراءة حقول عناصر التعداد ، وتصبح سجلات النوع Fieldrefلهذه الحقول غير ضرورية ، وبالتالي اكتب NameAndTypeالسجلات لنوع السجلات Fieldref. في التجمع الثابت ، 2 * < >يتم تحرير الفتحات التي يمكن استخدامها لإعلان عناصر تعداد إضافية.

لكن ليست كل شيء وردية للغاية ، تُظهر الاختبارات انخفاضًا كبيرًا في الأداء: إن تهيئة فئة التعداد التي تحتوي على 65 ألف عنصر تستغرق دقيقة واحدة ونصف لا يمكن تصوره. كما اتضح بسرعة كبيرة ، "الانعكاس يبطئ".

تطبيق Class :: getDeclaredField () في OpenJDK له سلوك مقارب لعدد من الحقول في الفصل ، وكتلة التهيئة لدينا تربيعية بسبب هذا.

تؤدي إضافة التخزين المؤقت إلى تحسين الحالة إلى حد ما ، على الرغم من أنها لا تحلها بالكامل:

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;
}

يتيح لك النهج غير الآمن الموضح في هذا الفصل إنشاء عمليات نقل باستخدام عدد من العناصر يصل إلى 65_410 ، وهو ما يقرب من 24 مرة أكثر من النتيجة التي يمكن تحقيقها باستخدام javac وهو قريب جدًا من الحد النظري البالغ 65_505 عناصر محسوبة من قبلنا في المنشور السابق للدورة.


تحقق من الأداء


بالنسبة للاختبارات ، نأخذ أكبر تعداد ، ننشئه باستخدام الأمر java -jar HugeEnumGen.jar -a Unsafe UnsafeHugeEnum. ونتيجة لذلك ، نحصل على ملف فئة بحجم 2 ميغابايت و 65_410 عنصر.

قم بإنشاء مشروع Java جديد في IDEA وأضف الفئة التي تم إنشاؤها كمكتبة خارجية.

على الفور تقريبًا ، أصبح من الملاحظ أن IDEA ليست جاهزة لمثل هذا الاختبار الإجهاد:



يستغرق الإكمال التلقائي لعنصر تعداد عشرات الثواني على كل من i5 المحمول القديم وعلى i7 8700K الأكثر حداثة. وإذا حاولت استخدام الإصلاح السريع لإضافة العناصر المفقودة إلى المحول ، فستتوقف IDEA عن إعادة رسم النوافذ. أشك في ذلك مؤقتًا ، لكنه فشل في انتظار اكتماله. كما أن الاستجابة أثناء التصحيح تترك الكثير مما هو مرغوب فيه.

لنبدأ بعدد صغير من العناصر في 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;
        }
    }

}

لا توجد مفاجآت هنا ، ويتم تجميعها وإطلاقها بشكل منتظم:

$ 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

ماذا عن المزيد من العناصر في switch؟ هل يمكننا ، على سبيل المثال ، معالجة switchجميع عناصرنا البالغ عددها 65 ألف عنصر في وقت واحد؟

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;
}

للأسف لا. عندما نحاول الترجمة ، نحصل على مجموعة كاملة من رسائل الخطأ:

$ 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



جافاك والتبديل


لفهم ما يحدث ، علينا أن نكتشف كيف تحدث ترجمة switchعناصر التعداد.

تحتوي مواصفات JVM على فصل منفصل في JVMS11 §3.10 تجميع المفاتيح ، والتي تتلخص توصياتها switchباستخدام واحد من اثنين من التعليمات البرمجية الثانوية ، tableswitchأو lookupswitch. switchلن نجد أي إشارات إلى السلاسل أو عناصر التعداد في هذا الفصل.

أفضل الوثائق هي الكود ، لذلك حان الوقت للتعمق في المصدر javac.

الاختيار بين tableswitchو lookupswitchيحدث في Gen :: visitSwitch () ويعتمد على عدد الخيارات في switch. في معظم الحالات ، يفوز 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;

يبلغ حجم الرأس tableswitch16 بايت تقريبًا بالإضافة إلى 4 بايت لكل قيمة. وبالتالي ، switchلا يمكن أن يكون هناك المزيد من ( 65_535 - 16 ) / 4 = 16_379العناصر تحت أي ظرف من الظروف .

في الواقع ، بعد تقليل عدد الفروع caseفي الجسم switchإلى 16 ألفًا ، يبقى خطأ تجميع واحد فقط ، الأكثر غموضاً:

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

بحثًا عن مصدر الخطأ ، سنعود مبكرًا قليلاً ، إلى مرحلة التخلص من السكر النحوي. و switchالأساليب هي المسؤولة عن الترجمة visitEnumSwitch()، mapForEnum()والطبقة EnumMappingفي Lower.java .

نجد أيضًا تعليقًا وثائقيًا صغيرًا:

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.
 */


tryيتحول الغموض إلى جزء من فئة المساعدة التي يتم إنشاؤها تلقائيًا TestAll$0. داخل - إعلان صفيف ثابت ورمز لتهيئته.

تعمل المصفوفة على إصلاح التوافق بين أسماء عناصر التعداد switchوالقيم العددية المخصصة لها أثناء التجميع ، وبالتالي حماية الشفرة المترجمة من التأثيرات الضارة لإعادة البناء.

عند إعادة الترتيب أو إضافة عناصر جديدة أو حذف عناصر التعداد الموجودة ، قد يغير بعضها القيمة ordinal()وهذا ما يحميه مستوى إضافي من التباعد.

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

رمز التهيئة بسيط ويستهلك من 15 إلى 17 بايت لكل عنصر. ونتيجة لذلك ، تستوعب كتلة التهيئة الثابتة تهيئة ما لا يزيد عن 3_862 من العناصر. يتضح أن هذا الرقم هو الحد الأقصى لعدد عناصر التعداد التي يمكننا استخدامها في عنصر واحد switchمع التنفيذ الحالي javac.


استنتاج


لقد رأينا أن استخدام مثل هذه التقنية البسيطة مثل تخصيص إنشاء عناصر التعداد وتهيئة صفيف $VALUESفي طريقة منفصلة يسمح لك بزيادة الحد الأقصى لعدد العناصر في التعداد من 2_746 إلى 10_920.

لا تبدو النتائج الديناميكية المستمرة على خلفية الإنجازات السابقة مثيرة للإعجاب للغاية وتسمح لك بالحصول على 43 عنصرًا إضافيًا فقط ، ولكن مع هذا النهج ، يكون من الأكثر أناقة إضافة خصائص جديدة إلى التعداد - فقط قم بتعديل المنشئ وتمريره القيم الضرورية من خلال المعلمات الثابتة للثابت الديناميكي.

إذا تم في وقت ما تعليم السمة المستقبلية ConstantValueفهم الثوابت الديناميكية ، يمكن أن يرتفع هذا الرقم إلى 10 آلاف إلى 16.

الاستخدامsun.misc.Unsafeيسمح لك بعمل قفزة عملاقة وزيادة الحد الأقصى لعدد العناصر إلى 65_410. ولكن لا تنس أن Unsafeهذه واجهة برمجة تطبيقات خاصة قد تختفي بمرور الوقت وأن استخدامها يمثل خطرًا كبيرًا ، كما يحذر javac مباشرة:

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

ولكن ، كما اتضح ، لا يكفي لإنشاء تعداد عملاق ، تحتاج أيضًا إلى أن تكون قادرًا على استخدامه.

توجد حاليًا مشكلات في دعم مثل هذه التعدادات من IDE وعلى مستوى مترجم Java.

يمكن أن يقلل عدد كبير من الحقول في الفصل من استجابة IDE أثناء التحرير وأثناء تصحيح الأخطاء. في بعض الأحيان تصل إلى تعليق كامل.

القيود التي يفرضها تنسيق ملف الفئة وتفاصيل تنفيذ javac تجعل من المستحيل استخدام switchأكثر من 3_862 عنصر في الكود في نفس الوقت. من الجوانب الإيجابية ، تجدر الإشارة إلى أن هذه يمكن أن تكون عناصر تعسفية 3_862.

لا يمكن زيادة تحسين النتائج إلا من خلال تحسين مترجم Java ، لكن هذه قصة مختلفة تمامًا.


مواد إضافية


كود مصدر GitHub: https://github.com/Maccimo/HugeEnumGeneratorArticle

ملف JAR الذي تم جمعه: https://github.com/Maccimo/HugeEnumGeneratorArticle/releases/tag/v1.0

تعليمات بدء التشغيل المعتمدة

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