diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java b/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java index 5cc08d06ec3..8498e77cb36 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java @@ -86,14 +86,24 @@ public abstract sealed class AbstractPoolEntry { return stringHash | NON_ZERO; } - public static Utf8Entry rawUtf8EntryFromStandardAttributeName(String name) { - //assuming standard attribute names are all US_ASCII - var raw = name.getBytes(StandardCharsets.US_ASCII); - return new Utf8EntryImpl(null, 0, raw, 0, raw.length); + static int hashClassFromUtf8(boolean isArray, Utf8EntryImpl content) { + int hash = content.contentHash(); + return hashClassFromDescriptor(isArray ? hash : Util.descriptorStringHash(content.length(), hash)); + } + + static int hashClassFromDescriptor(int descriptorHash) { + return hash1(ClassFile.TAG_CLASS, descriptorHash); + } + + static boolean isArrayDescriptor(Utf8EntryImpl cs) { + // Do not throw out-of-bounds for empty strings + return !cs.isEmpty() && cs.charAt(0) == '['; } @SuppressWarnings("unchecked") public static T maybeClone(ConstantPoolBuilder cp, T entry) { + if (cp.canWriteDirect(entry.constantPool())) + return entry; return (T)((AbstractPoolEntry)entry).clone(cp); } @@ -146,7 +156,7 @@ public abstract sealed class AbstractPoolEntry { private final int offset; private final int rawLen; // Set in any state other than RAW - private @Stable int hash; + private @Stable int contentHash; private @Stable int charLen; // Set in CHAR state private @Stable char[] chars; @@ -165,10 +175,10 @@ public abstract sealed class AbstractPoolEntry { } Utf8EntryImpl(ConstantPool cpm, int index, String s) { - this(cpm, index, s, hashString(s.hashCode())); + this(cpm, index, s, s.hashCode()); } - Utf8EntryImpl(ConstantPool cpm, int index, String s, int hash) { + Utf8EntryImpl(ConstantPool cpm, int index, String s, int contentHash) { super(cpm, index, 0); this.rawBytes = null; this.offset = 0; @@ -176,7 +186,7 @@ public abstract sealed class AbstractPoolEntry { this.state = State.STRING; this.stringValue = s; this.charLen = s.length(); - this.hash = hash; + this.contentHash = contentHash; } Utf8EntryImpl(ConstantPool cpm, int index, Utf8EntryImpl u) { @@ -185,7 +195,7 @@ public abstract sealed class AbstractPoolEntry { this.offset = u.offset; this.rawLen = u.rawLen; this.state = u.state; - this.hash = u.hash; + this.contentHash = u.contentHash; this.charLen = u.charLen; this.chars = u.chars; this.stringValue = u.stringValue; @@ -236,7 +246,7 @@ public abstract sealed class AbstractPoolEntry { int singleBytes = JLA.countPositives(rawBytes, offset, rawLen); int hash = ArraysSupport.hashCodeOfUnsigned(rawBytes, offset, singleBytes, 0); if (singleBytes == rawLen) { - this.hash = hashString(hash); + this.contentHash = hash; charLen = rawLen; state = State.BYTE; } @@ -294,7 +304,7 @@ public abstract sealed class AbstractPoolEntry { throw malformedInput(px); } } - this.hash = hashString(hash); + this.contentHash = hash; charLen = chararr_count; this.chars = chararr; state = State.CHAR; @@ -307,8 +317,6 @@ public abstract sealed class AbstractPoolEntry { @Override public Utf8EntryImpl clone(ConstantPoolBuilder cp) { - if (cp.canWriteDirect(constantPool)) - return this; return (state == State.STRING && rawBytes == null) ? (Utf8EntryImpl) cp.utf8Entry(stringValue) : ((SplitConstantPool) cp).maybeCloneUtf8Entry(this); @@ -316,9 +324,13 @@ public abstract sealed class AbstractPoolEntry { @Override public int hashCode() { + return hashString(contentHash()); + } + + int contentHash() { if (state == State.RAW) inflate(); - return hash; + return contentHash; } @Override @@ -389,6 +401,38 @@ public abstract sealed class AbstractPoolEntry { return stringValue().equals(u.stringValue()); } + /** + * Returns if this utf8 entry's content equals a substring + * of {@code s} obtained as {@code s.substring(start, end - start)}. + * This check avoids a substring allocation. + */ + public boolean equalsRegion(String s, int start, int end) { + // start and end values trusted + if (state == State.RAW) + inflate(); + int len = charLen; + if (len != end - start) + return false; + + var sv = stringValue; + if (sv != null) { + return sv.regionMatches(0, s, start, len); + } + + var chars = this.chars; + if (chars != null) { + for (int i = 0; i < len; i++) + if (chars[i] != s.charAt(start + i)) + return false; + } else { + var bytes = this.rawBytes; + for (int i = 0; i < len; i++) + if (bytes[offset + i] != s.charAt(start + i)) + return false; + } + return true; + } + @Override public boolean equalsString(String s) { if (state == State.RAW) @@ -397,7 +441,7 @@ public abstract sealed class AbstractPoolEntry { case STRING: return stringValue.equals(s); case CHAR: - if (charLen != s.length() || hash != hashString(s.hashCode())) + if (charLen != s.length() || contentHash != s.hashCode()) return false; for (int i=0; i fieldRefEntry(reference.owner(), reference.nameAndType()); - case TAG_METHODREF -> methodRefEntry(reference.owner(), reference.nameAndType()); - case TAG_INTERFACEMETHODREF -> interfaceMethodRefEntry(reference.owner(), reference.nameAndType()); - default -> throw new IllegalArgumentException(String.format("Bad tag %d", reference.tag())); - }; - } - + reference = AbstractPoolEntry.maybeClone(this, reference); int hash = AbstractPoolEntry.hash2(TAG_METHODHANDLE, refKind, reference.index()); EntryMap map1 = map(); for (int token = map1.firstToken(hash); token != -1; token = map1.nextToken(hash, token)) { @@ -538,8 +633,7 @@ public final class SplitConstantPool implements ConstantPoolBuilder { if (!canWriteDirect(bootstrapMethodEntry.constantPool())) bootstrapMethodEntry = bsmEntry(bootstrapMethodEntry.bootstrapMethod(), bootstrapMethodEntry.arguments()); - if (!canWriteDirect(nameAndType.constantPool())) - nameAndType = nameAndTypeEntry(nameAndType.name(), nameAndType.type()); + nameAndType = AbstractPoolEntry.maybeClone(this, nameAndType); int hash = AbstractPoolEntry.hash2(TAG_INVOKEDYNAMIC, bootstrapMethodEntry.bsmIndex(), nameAndType.index()); EntryMap map1 = map(); @@ -569,8 +663,7 @@ public final class SplitConstantPool implements ConstantPoolBuilder { if (!canWriteDirect(bootstrapMethodEntry.constantPool())) bootstrapMethodEntry = bsmEntry(bootstrapMethodEntry.bootstrapMethod(), bootstrapMethodEntry.arguments()); - if (!canWriteDirect(nameAndType.constantPool())) - nameAndType = nameAndTypeEntry(nameAndType.name(), nameAndType.type()); + nameAndType = AbstractPoolEntry.maybeClone(this, nameAndType); int hash = AbstractPoolEntry.hash2(TAG_CONSTANTDYNAMIC, bootstrapMethodEntry.bsmIndex(), nameAndType.index()); EntryMap map1 = map(); @@ -628,8 +721,7 @@ public final class SplitConstantPool implements ConstantPoolBuilder { @Override public BootstrapMethodEntry bsmEntry(MethodHandleEntry methodReference, List arguments) { - if (!canWriteDirect(methodReference.constantPool())) - methodReference = methodHandleEntry(methodReference.kind(), methodReference.reference()); + methodReference = AbstractPoolEntry.maybeClone(this, methodReference); for (LoadableConstantEntry a : arguments) { if (!canWriteDirect(a.constantPool())) { // copy args list diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java b/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java index 7f3dd914e7f..80d908f6ce7 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java @@ -50,6 +50,7 @@ import java.lang.classfile.constantpool.NameAndTypeEntry; import java.lang.constant.ModuleDesc; import java.lang.reflect.AccessFlag; import jdk.internal.access.SharedSecrets; +import jdk.internal.vm.annotation.Stable; import static java.lang.classfile.ClassFile.ACC_STATIC; import java.lang.classfile.attribute.CodeAttribute; @@ -337,4 +338,82 @@ public class Util { interface WritableLocalVariable { boolean writeLocalTo(BufWriterImpl buf); } + + /** + * Returns the hash code of an internal name given the class or interface L descriptor. + */ + public static int internalNameHash(String desc) { + if (desc.length() > 0xffff) + throw new IllegalArgumentException("String too long: ".concat(Integer.toString(desc.length()))); + return (desc.hashCode() - pow31(desc.length() - 1) * 'L' - ';') * INVERSE_31; + } + + /** + * Returns the hash code of a class or interface L descriptor given the internal name. + */ + public static int descriptorStringHash(int length, int hash) { + if (length > 0xffff) + throw new IllegalArgumentException("String too long: ".concat(Integer.toString(length))); + return 'L' * pow31(length + 1) + hash * 31 + ';'; + } + + // k is at most 65536, length of Utf8 entry + 1 + public static int pow31(int k) { + int r = 1; + // calculate the power contribution from index-th octal digit + // from least to most significant (right to left) + // e.g. decimal 26=octal 32, power(26)=powerOctal(2,0)*powerOctal(3,1) + for (int i = 0; i < SIGNIFICANT_OCTAL_DIGITS; i++) { + r *= powerOctal(k & 7, i); + k >>= 3; + } + return r; + } + + // The inverse of 31 in Z/2^32Z* modulo group, a * INVERSE_31 * 31 = a + static final int INVERSE_31 = 0xbdef7bdf; + + // k is at most 65536 = octal 200000, only consider 6 octal digits + // Note: 31 powers repeat beyond 1 << 27, only 9 octal digits matter + static final int SIGNIFICANT_OCTAL_DIGITS = 6; + + // for base k, storage is k * log_k(N)=k/ln(k) * ln(N) + // k = 2 or 4 is better for space at the cost of more multiplications + /** + * The code below is as if: + * {@snippet lang=java : + * int[] powers = new int[7 * SIGNIFICANT_OCTAL_DIGITS]; + * + * for (int i = 1, k = 31; i <= 7; i++, k *= 31) { + * int t = powers[powersIndex(i, 0)] = k; + * for (int j = 1; j < SIGNIFICANT_OCTAL_DIGITS; j++) { + * t *= t; + * t *= t; + * t *= t; + * powers[powersIndex(i, j)] = t; + * } + * } + * } + * This is converted to explicit initialization to avoid bootstrap overhead. + * Validated in UtilTest. + */ + static final @Stable int[] powers = new int[] { + 0x0000001f, 0x000003c1, 0x0000745f, 0x000e1781, 0x01b4d89f, 0x34e63b41, 0x67e12cdf, + 0x94446f01, 0x50a9de01, 0x84304d01, 0x7dd7bc01, 0x8ca02b01, 0xff899a01, 0x25940901, + 0x4dbf7801, 0xe3bef001, 0xc1fe6801, 0xe87de001, 0x573d5801, 0x0e3cd001, 0x0d7c4801, + 0x54fbc001, 0xb9f78001, 0x2ef34001, 0xb3ef0001, 0x48eac001, 0xede68001, 0xa2e24001, + 0x67de0001, 0xcfbc0001, 0x379a0001, 0x9f780001, 0x07560001, 0x6f340001, 0xd7120001, + 0x3ef00001, 0x7de00001, 0xbcd00001, 0xfbc00001, 0x3ab00001, 0x79a00001, 0xb8900001, + }; + + static int powersIndex(int digit, int index) { + return (digit - 1) + index * 7; + } + + // (31 ^ digit) ^ (8 * index) = 31 ^ (digit * (8 ^ index)) + // digit: 0 - 7 + // index: 0 - SIGNIFICANT_OCTAL_DIGITS - 1 + private static int powerOctal(int digit, int index) { + return digit == 0 ? 1 : powers[powersIndex(digit, index)]; + } } diff --git a/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java b/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java index 7c97c9dd5a9..b5d3ba5d584 100644 --- a/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java +++ b/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java @@ -23,11 +23,14 @@ /* * @test - * @bug 8304031 8338406 + * @bug 8304031 8338406 8338546 * @summary Testing handling of various constant descriptors in ClassFile API. + * @modules java.base/jdk.internal.constant + * java.base/jdk.internal.classfile.impl * @run junit ConstantDescSymbolsTest */ +import java.lang.classfile.constantpool.ConstantPoolBuilder; import java.lang.constant.ClassDesc; import java.lang.constant.DynamicConstantDesc; import java.lang.constant.MethodHandleDesc; @@ -36,8 +39,14 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.function.Supplier; import java.lang.classfile.ClassFile; +import java.util.stream.Stream; + +import jdk.internal.classfile.impl.AbstractPoolEntry; +import jdk.internal.constant.ConstantUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static java.lang.classfile.ClassFile.ACC_PUBLIC; import static java.lang.constant.ConstantDescs.*; @@ -102,4 +111,58 @@ final class ConstantDescSymbolsTest { assertEquals(DEFAULT_NAME, cb.name); assertEquals(CondyBoot.class, cb.type); } + + static Stream classOrInterfaceEntries() { + return Stream.of( + CD_Object, CD_Float, CD_Long, CD_String, ClassDesc.of("Ape"), + CD_String.nested("Whatever"), CD_MethodHandles_Lookup, ClassDesc.ofInternalName("one/Two"), + ClassDesc.ofDescriptor("La/b/C;"), ConstantDescSymbolsTest.class.describeConstable().orElseThrow(), + CD_Boolean, CD_ConstantBootstraps, CD_MethodHandles + ); + } + + @ParameterizedTest + @MethodSource("classOrInterfaceEntries") + void testConstantPoolBuilderClassOrInterfaceEntry(ClassDesc cd) { + assertTrue(cd.isClassOrInterface()); + ConstantPoolBuilder cp = ConstantPoolBuilder.of(); + var internal = ConstantUtils.dropFirstAndLastChar(cd.descriptorString()); + + // 1. ClassDesc + var ce = cp.classEntry(cd); + assertSame(cd, ce.asSymbol(), "Symbol propagation on create"); + + // 1.1. Bare addition + assertTrue(ce.name().equalsString(internal), "Adding to bare pool"); + + // 1.2. Lookup existing + assertSame(ce, cp.classEntry(cd), "Finding by identical CD"); + + // 1.3. Lookup existing - equal but different ClassDesc + var cd1 = ClassDesc.ofDescriptor(cd.descriptorString()); + assertSame(ce, cp.classEntry(cd1), "Finding by another equal CD"); + + // 1.3.1. Lookup existing - equal but different ClassDesc, equal but different string + var cd2 = ClassDesc.ofDescriptor("" + cd.descriptorString()); + assertSame(ce, cp.classEntry(cd2), "Finding by another equal CD"); + + // 1.4. Lookup existing - with utf8 internal name + var utf8 = cp.utf8Entry(internal); + assertSame(ce, cp.classEntry(utf8), "Finding CD by UTF8"); + + // 2. ClassEntry exists, no ClassDesc + cp = ConstantPoolBuilder.of(); + utf8 = cp.utf8Entry(internal); + ce = cp.classEntry(utf8); + var found = cp.classEntry(cd); + assertSame(ce, found, "Finding non-CD CEs with CD"); + assertEquals(cd, ce.asSymbol(), "Symbol propagation on find"); + + // 3. Utf8Entry exists, no ClassEntry + cp = ConstantPoolBuilder.of(); + utf8 = cp.utf8Entry(internal); + ce = cp.classEntry(cd); + assertSame(utf8, ce.name(), "Reusing existing utf8 entry"); + assertEquals(cd, ce.asSymbol(), "Symbol propagation on create with utf8"); + } } diff --git a/test/jdk/jdk/classfile/UtilTest.java b/test/jdk/jdk/classfile/UtilTest.java index d9d8240ae91..be66d930580 100644 --- a/test/jdk/jdk/classfile/UtilTest.java +++ b/test/jdk/jdk/classfile/UtilTest.java @@ -23,18 +23,25 @@ /* * @test + * @bug 8338546 * @summary Testing ClassFile Util. + * @library java.base + * @modules java.base/jdk.internal.constant + * java.base/jdk.internal.classfile.impl + * @build java.base/jdk.internal.classfile.impl.* * @run junit UtilTest */ -import java.lang.classfile.ClassFile; import java.lang.classfile.Opcode; import java.lang.constant.MethodTypeDesc; -import java.lang.invoke.MethodHandles; import java.util.Arrays; -import java.util.BitSet; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + import jdk.internal.classfile.impl.RawBytecodeHelper; import jdk.internal.classfile.impl.Util; +import jdk.internal.classfile.impl.UtilAccess; +import jdk.internal.constant.ConstantUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -84,6 +91,50 @@ class UtilTest { assertEquals(Util.parameterSlots(MethodTypeDesc.ofDescriptor(methodDesc)), slots); } + @Test + void testPow31() { + int p = 1; + // Our calculation only prepares up to 65536, + // max length of CP Utf8 + 1 + for (int i = 0; i <= 65536; i++) { + final int t = i; + assertEquals(p, Util.pow31(i), () -> "31's power to " + t); + p *= 31; + } + } + + @ParameterizedTest + @ValueSource(classes = { + Long.class, + Object.class, + Util.class, + Test.class, + CopyOnWriteArrayList.class, + AtomicReferenceFieldUpdater.class + }) + void testInternalNameHash(Class type) { + var cd = type.describeConstable().orElseThrow(); + assertEquals(ConstantUtils.binaryToInternal(type.getName()).hashCode(), Util.internalNameHash(cd.descriptorString())); + } + + // Ensures the initialization statement of the powers array is filling in the right values + @Test + void testPowersArray() { + int[] powers = new int[7 * UtilAccess.significantOctalDigits()]; + for (int i = 1, k = 31; i <= 7; i++, k *= 31) { + int t = powers[UtilAccess.powersIndex(i, 0)] = k; + + for (int j = 1; j < UtilAccess.significantOctalDigits(); j++) { + t *= t; + t *= t; + t *= t; + powers[UtilAccess.powersIndex(i, j)] = t; + } + } + + assertArrayEquals(powers, UtilAccess.powersTable()); + } + @Test void testOpcodeLengthTable() { var lengths = new byte[0x100]; diff --git a/test/jdk/jdk/classfile/java.base/jdk/internal/classfile/impl/UtilAccess.java b/test/jdk/jdk/classfile/java.base/jdk/internal/classfile/impl/UtilAccess.java new file mode 100644 index 00000000000..27cefd6d944 --- /dev/null +++ b/test/jdk/jdk/classfile/java.base/jdk/internal/classfile/impl/UtilAccess.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.classfile.impl; + +public final class UtilAccess { + public static int significantOctalDigits() { + return Util.SIGNIFICANT_OCTAL_DIGITS; + } + + public static int powersIndex(int digit, int index) { + return Util.powersIndex(digit, index); + } + + public static int[] powersTable() { + return Util.powers; + } + + public static int reverse31() { + return Util.INVERSE_31; + } +} diff --git a/test/micro/org/openjdk/bench/jdk/classfile/ConstantPoolBuildingClassEntry.java b/test/micro/org/openjdk/bench/jdk/classfile/ConstantPoolBuildingClassEntry.java new file mode 100644 index 00000000000..0f8bf0449af --- /dev/null +++ b/test/micro/org/openjdk/bench/jdk/classfile/ConstantPoolBuildingClassEntry.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.jdk.classfile; + +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.constant.ClassDesc; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import static java.lang.constant.ConstantDescs.*; + +import static org.openjdk.bench.jdk.classfile.TestConstants.*; + +/** + * Tests constant pool builder lookup performance for ClassEntry. + * Note that ClassEntry is available only for reference types. + */ +@Warmup(iterations = 3) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@BenchmarkMode(Mode.Throughput) +@Fork(value = 1, jvmArgsAppend = {"--enable-preview"}) +@State(Scope.Benchmark) +public class ConstantPoolBuildingClassEntry { + // JDK-8338546 + ConstantPoolBuilder builder; + List classDescs; + List nonIdenticalClassDescs; + List internalNames; + List nonDuplicateClassDescs; + List nonDuplicateInternalNames; + int size; + + @Setup(Level.Iteration) + public void setup() { + builder = ConstantPoolBuilder.of(); + // Note these can only be reference types, no primitives + classDescs = List.of( + CD_Byte, CD_Object, CD_Long.arrayType(), CD_String, CD_String, CD_Object, CD_Short, + CD_MethodHandle, CD_MethodHandle, CD_Object, CD_Character, CD_List, CD_ArrayList, + CD_List, CD_Set, CD_Integer, CD_Object.arrayType(), CD_Enum, CD_Object, CD_MethodHandles_Lookup, + CD_Long, CD_Set, CD_Object, CD_Character, CD_Integer, CD_System, CD_String, CD_String, + CD_CallSite, CD_Collection, CD_List, CD_Collection, CD_String, CD_int.arrayType() + ); + size = classDescs.size(); + nonIdenticalClassDescs = classDescs.stream().map(cd -> { + var ret = ClassDesc.ofDescriptor(new String(cd.descriptorString())); + ret.hashCode(); // pre-compute hash code for cd + return ret; + }).toList(); + internalNames = classDescs.stream().map(cd -> { + // also sets up builder + cd.hashCode(); // pre-computes hash code for cd + var ce = builder.classEntry(cd); + var ret = ce.name().stringValue(); + ret.hashCode(); // pre-computes hash code for stringValue + return ret; + }).toList(); + nonDuplicateClassDescs = List.copyOf(new LinkedHashSet<>(classDescs)); + nonDuplicateInternalNames = nonDuplicateClassDescs.stream().map(cd -> + builder.classEntry(cd).asInternalName()).toList(); + } + + // Copied from jdk.internal.classfile.impl.Util::toInternalName + // to reduce internal dependencies + public static String toInternalName(ClassDesc cd) { + var desc = cd.descriptorString(); + if (desc.charAt(0) == 'L') + return desc.substring(1, desc.length() - 1); + throw new IllegalArgumentException(desc); + } + + /** + * Looking up with identical ClassDesc objects. Happens in bytecode generators reusing + * constant CD_Xxx. + */ + @Benchmark + public void identicalLookup(Blackhole bh) { + for (var cd : classDescs) { + bh.consume(builder.classEntry(cd)); + } + } + + /** + * Looking up with non-identical ClassDesc objects. Happens in bytecode generators + * using ad-hoc Class.describeConstable().orElseThrow() or other parsed ClassDesc. + * Cannot use identity fast path compared to {@link #identicalLookup}. + */ + @Benchmark + public void nonIdenticalLookup(Blackhole bh) { + for (var cd : nonIdenticalClassDescs) { + bh.consume(builder.classEntry(cd)); + } + } + + /** + * Looking up with internal names. Closest to ASM behavior. + * Baseline for {@link #identicalLookup}. + */ + @Benchmark + public void internalNameLookup(Blackhole bh) { + for (var name : internalNames) { + bh.consume(builder.classEntry(builder.utf8Entry(name))); + } + } + + /** + * The default implementation provided by {@link ConstantPoolBuilder#classEntry(ClassDesc)}. + * Does substring so needs to rehash and has no caching, should be very slow. + */ + @Benchmark + public void oldStyleLookup(Blackhole bh) { + for (var cd : classDescs) { + var s = cd.isClassOrInterface() ? toInternalName(cd) : cd.descriptorString(); + bh.consume(builder.classEntry(builder.utf8Entry(s))); + } + } + + /** + * Measures performance of creating new class entries in new constant pools with symbols. + */ + @Benchmark + public void freshCreationWithDescs(Blackhole bh) { + var cp = ConstantPoolBuilder.of(); + for (var cd : nonDuplicateClassDescs) { + bh.consume(cp.classEntry(cd)); + } + } + + /** + * Measures performance of creating new class entries in new constant pools with internal names. + */ + @Benchmark + public void freshCreationWithInternalNames(Blackhole bh) { + var cp = ConstantPoolBuilder.of(); + for (var name : nonDuplicateInternalNames) { + bh.consume(cp.classEntry(cp.utf8Entry(name))); + } + } +}