Add compact-strings module: Compact Strings (JEP 254) measured with JOL on JDK 25, with and without -XX:-CompactStrings, and compact object headers on 25 vs 27
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import java.util.Random;
|
||||
import org.openjdk.jol.info.GraphLayout;
|
||||
|
||||
/**
|
||||
* One million random lower-case strings of 30 to 55 characters (the same shape the old version of the post used),
|
||||
* measured with JOL's GraphLayout, then the same strings with one euro sign appended to each.
|
||||
*/
|
||||
public class MillionStrings {
|
||||
static final int COUNT = 1_000_000;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("java.version = " + System.getProperty("java.version"));
|
||||
String[] latin = new String[COUNT];
|
||||
String[] wide = new String[COUNT];
|
||||
Random r = new Random(1);
|
||||
long chars = 0;
|
||||
for (int i = 0; i < COUNT; i++) {
|
||||
int len = 30 + r.nextInt(26);
|
||||
StringBuilder sb = new StringBuilder(len);
|
||||
for (int j = 0; j < len; j++) sb.append((char) ('a' + r.nextInt(26)));
|
||||
latin[i] = sb.toString();
|
||||
wide[i] = latin[i] + "\u20AC"; // one non-Latin-1 character flips the whole string to UTF-16
|
||||
chars += len;
|
||||
}
|
||||
int latinCoder0 = 0, wideCoder1 = 0;
|
||||
for (int i = 0; i < COUNT; i++) {
|
||||
if (StringLayout.coder(latin[i]) == 0) latinCoder0++;
|
||||
if (StringLayout.coder(wide[i]) == 1) wideCoder1++;
|
||||
}
|
||||
System.out.printf("strings : %,d (average %.2f chars)%n", COUNT, chars / (double) COUNT);
|
||||
System.out.printf("coder == 0 (LATIN1) in set A : %,d%n", latinCoder0);
|
||||
System.out.printf("coder == 1 (UTF16) in set B : %,d%n", wideCoder1);
|
||||
GraphLayout a = GraphLayout.parseInstance((Object) latin);
|
||||
GraphLayout b = GraphLayout.parseInstance((Object) wide);
|
||||
System.out.printf("A: lower-case ASCII only : %,d bytes (%.1f MB, %.1f bytes per string)%n", a.totalSize(), a.totalSize() / 1e6, a.totalSize() / (double) COUNT);
|
||||
System.out.printf("B: same + one euro sign : %,d bytes (%.1f MB, %.1f bytes per string)%n", b.totalSize(), b.totalSize() / 1e6, b.totalSize() / (double) COUNT);
|
||||
System.out.printf("B / A : %.3f%n", b.totalSize() / (double) a.totalSize());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import java.lang.reflect.Field;
|
||||
import org.openjdk.jol.info.ClassLayout;
|
||||
import org.openjdk.jol.info.GraphLayout;
|
||||
import org.openjdk.jol.vm.VM;
|
||||
|
||||
/**
|
||||
* What one String really costs: the String object, the byte[] it points to, and the coder byte that says how to read it.
|
||||
* Measured with JOL (org.openjdk.jol:jol-core). Run by run.sh under several JVM flag sets.
|
||||
*/
|
||||
public class StringLayout {
|
||||
|
||||
static byte coder(String s) throws Exception {
|
||||
Field f = String.class.getDeclaredField("coder");
|
||||
f.setAccessible(true); // needs --add-opens java.base/java.lang=ALL-UNNAMED
|
||||
return f.getByte(s);
|
||||
}
|
||||
|
||||
static byte[] value(String s) throws Exception {
|
||||
Field f = String.class.getDeclaredField("value");
|
||||
f.setAccessible(true);
|
||||
return (byte[]) f.get(s);
|
||||
}
|
||||
|
||||
static String repeat(char c, int n) { return String.valueOf(c).repeat(n); }
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("java.version = " + System.getProperty("java.version"));
|
||||
System.out.println("vm.flags = " + String.join(" ", java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments()
|
||||
.stream().filter(a -> a.startsWith("-XX")).toList()));
|
||||
System.out.println(VM.current().details().lines().filter(l -> l.startsWith("# Objects are 8 bytes") || l.startsWith("# Field sizes")
|
||||
|| l.contains("Array element") || l.contains("compressed")).reduce("", (a, b) -> a + b + "\n"));
|
||||
|
||||
System.out.println("=== java.lang.String, the class");
|
||||
System.out.println(ClassLayout.parseClass(String.class).toPrintable());
|
||||
|
||||
System.out.println("=== one Latin-1 string and one UTF-16 string, field by field");
|
||||
System.out.println(ClassLayout.parseInstance("hello").toPrintable());
|
||||
System.out.println(ClassLayout.parseInstance("h\u20ACllo").toPrintable());
|
||||
|
||||
System.out.println("=== the byte[] behind each");
|
||||
System.out.println(ClassLayout.parseInstance(value("hello")).toPrintable());
|
||||
System.out.println(ClassLayout.parseInstance(value("h\u20ACllo")).toPrintable());
|
||||
|
||||
System.out.println("=== footprint by length (String object + its byte[], via GraphLayout)");
|
||||
System.out.println("chars content coder byte[].length String byte[] total");
|
||||
for (int n : new int[] {0, 1, 5, 10, 20, 40, 100, 1000}) {
|
||||
for (char c : new char[] {'a', '\u00E9', '\u20AC'}) { // a = ASCII, e-acute = Latin-1 supplement, euro = needs UTF-16
|
||||
String s = new String(repeat(c, n).toCharArray()); // a fresh object, never the interned literal
|
||||
long total = GraphLayout.parseInstance(s).totalSize();
|
||||
long str = ClassLayout.parseInstance(s).instanceSize();
|
||||
long arr = ClassLayout.parseInstance(value(s)).instanceSize();
|
||||
System.out.printf("%5d %-8s %5d %13d %6d %6d %5d%n", n,
|
||||
c == 'a' ? "ASCII" : c == '\u00E9' ? "U+00E9" : "U+20AC", coder(s), value(s).length, str, arr, total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* When does a string stay compact? Prints the coder of strings produced by common operations.
|
||||
* coder 0 = LATIN1 (1 byte per char), coder 1 = UTF16 (2 bytes per char).
|
||||
*/
|
||||
public class Upgrade {
|
||||
static void show(String label, String s) throws Exception {
|
||||
System.out.printf("%-46s length=%d coder=%d byte[].length=%d%n", label, s.length(),
|
||||
StringLayout.coder(s), StringLayout.value(s).length);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("java.version = " + System.getProperty("java.version"));
|
||||
show("\"hello\" (literal)", "hello");
|
||||
show("\"h\\u00E9llo\" (e-acute, Latin-1 supplement)", "h\u00E9llo");
|
||||
show("\"h\\u20ACllo\" (euro sign)", "h\u20ACllo");
|
||||
show("\"hello\" + \"\\u20AC\" (concat)", "hello".concat("\u20AC"));
|
||||
String wide = "\u20ACabcdef";
|
||||
show("wide = \"\\u20ACabcdef\"", wide);
|
||||
show("wide.substring(1) (all Latin-1 again)", wide.substring(1));
|
||||
show("wide.replace('\\u20AC', 'E')", wide.replace('\u20AC', 'E'));
|
||||
show("wide.toLowerCase()", wide.toLowerCase());
|
||||
show("new String(\"caf\\u00E9\".getBytes(UTF_8), UTF_8)", new String("caf\u00E9".getBytes(java.nio.charset.StandardCharsets.UTF_8), java.nio.charset.StandardCharsets.UTF_8));
|
||||
show("new String(utf8 bytes of ASCII, UTF_8)", new String("plain".getBytes(java.nio.charset.StandardCharsets.UTF_8), java.nio.charset.StandardCharsets.UTF_8));
|
||||
show("new String(char[] {'a','b'})", new String(new char[] {'a', 'b'}));
|
||||
show("new String(char[] {'a','\\u4E2D'})", new String(new char[] {'a', '\u4E2D'}));
|
||||
show("\"\\uD83D\\uDE00\" (emoji, a surrogate pair)", "\uD83D\uDE00");
|
||||
StringBuilder sb = new StringBuilder("ascii only");
|
||||
show("StringBuilder(\"ascii only\").toString()", sb.toString());
|
||||
sb.append('\u20AC');
|
||||
show("... after append('\\u20AC'), toString()", sb.toString());
|
||||
sb.setLength(0);
|
||||
sb.append("back to ascii");
|
||||
show("... after setLength(0) + ASCII, toString()", sb.toString());
|
||||
System.out.println("length() of the emoji: " + "\uD83D\uDE00".length() + " (UTF-16 code units, not code points)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user