枚举第二部分中的最大值数

第一部分,理论  | 第二部分,实用



我们继续在枚举中搜索值的最大数量。
这次,我们将专注于问题的实际方面,并了解IDE,编译器和JVM将如何响应我们的成就。

内容


  
  Javac 工具
  提取方法
  动态类文件常量
    突如其来的困难
    光明的未来
  不安全的
  测试
    Javac和开关性能
  结论
  其他资源


工具类


Javac会照顾我们:它会从标识符中切出不喜欢的字符,并禁止从中继承字符java.lang.Enum,因此对于实验,我们需要其他工具。

我们将使用asmtools -JVM的汇编器和反汇编器测试假设,并使用ASM库以工业规模生成类文件

为了便于理解,将在类似Java的伪代码中复制正在发生的本质。


Java语言


首先,在没有技巧的情况下,仅靠一个就能获得最佳结果,这是合乎逻辑的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个字节来初始化$VALUESvalueIndex然后从静态初始化块返回还剩下65_524个字节来初始化字段。每个字段的初始化需要6个字节,这使我们能够创建10_920个元素的枚举。

一定要用代码生成来庆祝与javac相比增长近四倍!

生成器源代码:ExtractMethodHugeEnumGenerator.java生成的
类示例:ExtractMethodHugeEnum.class

动态类文件常量


现在是时候记住JEP 309及其神秘的动态常数了

简而言之,创新的实质是:

在由一组常量支持的现有类型中添加了另一个常量CONSTANT_Dynamic。加载类时,此类常量的类型已知,但其值未知。第一次加载常量会导致调用其声明中指定的bootstrap方法。

该方法的结果变为恒定值。无法更改与已初始化的常量关联的值。这对于一个常量来说是很合逻辑的。

如果您还想到了Singleton,请立即将其忘记。该规范单独强调了在这种情况下不能保证线程安全,并且多线程代码中的初始化方法可以被多次调用。仅保证在对同一常量多次调用bootstrap方法的情况下,JVM会抛出一个硬币并选择一个计算值作为常量值的作用,而其他值将被牺牲给垃圾收集器。

从行为上讲,可以通过对以下参数执行其bootstrap方法来解析CONSTANT_Dynamic常量:

  1. 本地查找对象,
  2. 代表常量名称部分的字符串,
  3. 代表期望的常量类型的Class,以及
  4. 任何剩余的引导程序参数。

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

到从字节码常量池负载值,提供的命令ldcldc_wldc2_w。我们首先感兴趣的是- ldc

与其他方法不同,它只能从常量池的前255个时隙中加载值,但字节码占用的字节数少1个字节。所有这些使我们节省了最多255个字节,并255 + ((65_524 - (255 * 5)) / 6) = 10_963在枚举中节省了一个元素。这次增长不是那么令人印象深刻,但是仍然存在。

有了这些知识,让我们开始吧。

在静态初始化块中,nextValue()我们现在将加载动态常量的值,而不是方法调用ordinal枚举元素序数索引的值将被显式传递,从而摆脱了field valueIndex,factory方法nextValue()并对我们实现的线程安全性表示怀疑。

作为引导方法,我们将使用MethodHandle的特殊子类型,子类型模仿newJava中运算符的行为用于获取此类方法句柄的标准库提供了MethodHandles.Lookup :: findConstructor()方法,但在我们的情况下,JVM将负责所需方法句柄的构造。

要将枚举的构造函数用作引导程序方法,必须通过更改签名对其进行略微修改。bootstrap方法所需的参数将添加到名称枚举元素和序列号的传统构造函数中:

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也可以通过动态常量来实现。bootstrap方法非常简单:

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

此动态常量的静态参数列表中的所有技巧,我们将列出要放入的所有元素$VALUES

BootstrapMethods:
  ...
  1:#54 REF_invokeStatic FizzBu​​zz.createValues:(Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / Class; [LFizzBu​​zz;)[LFizzBu​​zz;
    方法参数:
      #1#0:嘶嘶声:LFizzBu​​zz;
      #2#0:嗡嗡声:LFizzBu​​zz;
      #3#0:FizzBu​​zz:LFizzBu​​zz;

JVM从这些静态参数破坏数组,并将其作为vararg参数传递给我们的bootstrap方法elements静态参数的最大数目是传统的65_535,因此,保证枚举的所有元素都足够,无论有多少个元素。

对于具有大量元素的传输,此更改将减小生成的类文件的大小,并且在由于大量元素而createValues()不得不将方法拆分为几个部分的情况下,该方法还将时隙保存在常量池中。
最后,它只是美丽。


突如其来的困难


我们通过手动生成类来克服了这一难题。

高级库提供了方便的界面,以换取某些限制的行动自由。我们用于生成类文件的ASM库也不例外。它没有提供直接控制常量池内容的机制。这通常不是很重要,但就我们而言并非如此。

您还记得,我们需要常量池的前255个元素将宝贵的字节保存在静态初始化块中。当以标准方式添加动态常量时,它们将位于随机索引处,并与对我们不太重要的其他元素混合。这将阻止我们达到最大。

以传统方式形成的常数池的片段
恒定池:
   #1 = Utf8 FizzBu​​zz
   #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);


池常数谈论访问修饰符和标志后(publicfinalenun,等),类名和它的祖先:

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!


静态final字段的值已从初始化块移至单独的属性ConstantValue这是他们在JVMS11§4.7.2中关于此属性的内容

ConstantValue属性表示常量表达式的值(JLS§15.28),其用法如下:
  • 如果在field_info结构的access_flags项中设置了ACC_STATIC标志,则将field_info结构表示的字段分配为其ConstantValue属性表示的值,作为声明该字段的类或接口的初始化的一部分(第5.5节)。这发生在调用该类或接口的类或接口初始化方法之前(第2.9.2节)。
  • 否则,Java虚拟机必须静默忽略该属性。


如果发生同时这样的属性staticfinal(尽管后者没有明确地阐述了在这里)中的字段,则这样的字段被初始化为从该属性的值。而且甚至在调用静态初始化方法之前就发生了。

使用此属性来初始化枚举的元素很诱人,在上一章中,尽管有动态常量,但也只有常量。

我们并不是第一个朝这个方向思考的人,JEP 309中有提及ConstantValue不幸的是,此提及在“未来工作”一章中:

将来的工作

可能的将来扩展包括:

...
  • 将动态常量附加到静态字段的ConstantValue属性


同时,我们只能梦到该功能将从“做好”状态变为“准备就绪”状态的时间。然后,初始化块中对代码大小的限制将失去影响,枚举中元素的最大数量将确定常量池的限制。

根据粗略估计,在这种情况下,我们可以希望有一个65 489 / 4 = 16_372要素。65_489是常量池中未占用的时隙数,理论上可能的65_535中有46个进入了开销。4-声明一个字段所需的时隙数和相应的动态常数。

当然,确切的数字只有在支持此功能的JDK版本发布后才能找到。


不安全


我们的敌人是初始化块的线性增长以及枚举元素数量的增加。如果我们找到了减少循环初始化的方法,从而消除了枚举中的元素数量与初始化块的大小之间的关系,那么我们将取得另一个突破。

不幸的是,标准的公共API都不允许static final在静态初始化块内写入字段。Reflection和VarHandles都不会对这里有所帮助。我们唯一的希望是伟大而可怕的sun.misc.Unsafe

不安全地执行FizzBu​​zz可能看起来像这样:

不安全的FizzBu​​zz
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();
    }

}


这种方法使我们能够创建一个包含约21000个元素的枚举;更多的常量池的容量还不够。Enum :: ordinal()

上的文档要求其值与枚举声明中相应元素的序列号匹配,因此您必须以正确的顺序显式存储字段名称列表,从而几乎使类文件的大小增加一倍。

public final int ordinal()

返回此枚举常量的序数(其在枚举声明中的位置,其中初始常量的序数为零)。

在这里,用于常量池内容的公共API可能会有所帮助,我们已经知道如何按需要的顺序填充它,但是没有这样的API,而且不可能。 OpenJDK中提供的Class :: getConstantPool()方法被声明为package-private,在用户代码中依赖它很容易。

现在,初始化块非常紧凑,并且几乎与枚举中的元素数量无关,因此您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,000个元素的枚举类需要花费一分半钟的时间。结果很快,“反射速度变慢了”。OpenJDK

Class :: getDeclaredField()的实现具有中字段数的线性渐近行为,因此,我们的初始化块是平方的。

尽管无法完全解决,但添加缓存可以稍微改善这种情况:

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个元素的传输,这几乎是javac可获得的结果的24倍,并且非常接近我们在循环的上一版中计算出的65_505个元素的理论极限。


检查效果


对于测试,我们采用最大的枚举,并使用command生成它java -jar HugeEnumGen.jar -a Unsafe UnsafeHugeEnum。结果,我们得到了一个大小为2兆字节和65_410个元素的类文件。

在IDEA中创建一个新的Java项目,并将生成的类添加为外部库。

几乎可以立即看出,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,000个元素吗?

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

las,不。当我们尝试编译时,我们会收到一堆错误消息:

$ 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和开关


要了解正在发生的事情,我们必须弄清楚switch枚举元素的转换如何发生的

JVM规范在JVMS11§3.10编译开关中有单独的章节,其建议可以归结为switch使用两个字节码指令之一tableswitchlookupswitchswitch在本章中,我们将找不到对字符串或枚举元素的任何引用

最好的文档是代码,因此该深入了解源代码了javac

之间的选择出现在Gen :: visitSwitch()中,并取决于中的选项数量。在大多数情况下,获胜tableswitchlookupswitchswitchtableswitch

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

标头tableswitch约为16个字节,每个值4个字节。因此,switch在任何情况下都不能有更多( 65_535 - 16 ) / 4 = 16_379元素。

确实,case在将体内的分支数减少switch到1.6万后,仅存在一个编译错误,这是最神秘的:

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

为了寻找错误的根源,我们将更早返回到摆脱语法糖的阶段。switch方法分别负责翻译visitEnumSwitch()mapForEnum()和类EnumMappingLower.java

在这里,我们还发现了一个小的纪实评论:

枚举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这是一个专有的API,随着时间的流逝,它可能会消失,并且使用它会带来很大的风险,因为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