Brian asked me to talk a little about the String Templates use case for
Carrier. ( String Templates
JEP<https://bugs.openjdk.java.net/browse/JDK-8273943> for background.)
Going the example route, the following Java code constructs a TemplatedString
from a templated string literal that contains three embedded expressions, x, y,
and x + y;
int x = 10;
int y = 20;
TemplatedString ts = "Adding \{x} and \{y} equals \{x + y}."
Naively, we could capture the interesting bits of the templated string literal
at runtime using a generic GenericTS record;
public record GenericTS(String template, List<Object> values) implements
TemplatedString {}
...
TemplatedString ts = new GenericTS("Adding \uFFFC and \uFFFC equals
\uFFFC.", List.of(x, y, x + y));
// `\uFFFC` are placeholder
characters
There are several drawbacks using this generic record, but let's just focus on
the List.of(x, y, x + y). Clearly, using a list as a carrier would force all
the int expression values to be boxed. For an optimizing template policy, such
as STR (concatenation), boxing would be a performance killer. We need a way to
carry values without boxing.
Another approach is to use an anonymous class.
TemplatedString ts = new TemplatedString() {
private final int exp$1 = x;
private final int exp$2 = y;
private final int exp$3 = x + y;
public String template() { return "Adding \uFFFC and \uFFFC
equals \uFFFC."; }
public List<Object> values() { return List.of(exp$1, exp$2, exp$3); }
...
});
Using the fields of an anonymous class allows the TemplatedString to be
constructed without boxing and allow an optimizing TemplatePolicy to access
values without boxing (via MethodHandles.)
The anonymous class downside is that we end up with hundreds of these often
very similar classes. For example;
TemplatedString ts1 = "Adding \{x} and \{y} equals \{x + y}."
TemplatedString ts2 = "Subtracting \{x} from \{y} equals \{y - x}."
Even though the templates are different, the underlying carrier is still three
int values. The compiler could fold similarly shaped anonymous classes at
compile time, but that would only work for a single compilation unit. What is
needed is a runtime solution.
That's where java.lang.runtime.Carrier kicks in. Carrier provides an optimal
carrier aligning to types of values that are to be carried, spinning up
anonymous classes if needed and reusing anonymous classes when similar shape.
On Mar 3, 2022, at 2:20 PM, Remi Forax
<[email protected]<mailto:[email protected]>> wrote:
For the pattern matching,
we also need a 'with' method, that return a method handle that takes a carrier
and a value and return a new carrier with the component value updated.
static MethodHandle withComponent(MethodType methodType, int i)
// returns a mh (Carrier;T) -> Carrier with T the type of the component
It can be built on top of constructor() + component() but i think that i should
be part of the API instead of every user of the Carrier API trying to
re-implement it.
In term of spec, Jim, can you rename "component getter" to "component accessor"
which is the term used by records.
Rémi
________________________________
From: "Brian Goetz" <[email protected]<mailto:[email protected]>>
To: "Jim Laskey" <[email protected]<mailto:[email protected]>>,
"amber-spec-experts"
<[email protected]<mailto:[email protected]>>
Sent: Thursday, March 3, 2022 4:29:51 PM
Subject: Re: Proposal: java.lang.runtime.Carrier
Thanks Jim.
As background, (some form of) this code originated in a prototype for pattern
matching, where we needed a carrier for a tuple (T, U, V) to carry the results
of a match from a deconstruction pattern (or other declared pattern) on the
stack as a return value. We didn't want to spin a custom class per pattern,
and we didn't want to commit to the actual layout, because we wanted to
preserve the ability to switch later to a value class. So the idea is you
describe the carrier you want as a MethodType, and there's a condy that gives
you an MH that maps that shape of arguments to an opaque carrier (the
constructor), and other condys that give you MHs that map from the carrier to
the individual bindings. So pattern matching will stick those MHs in CP slots.
The carrier might be some bespoke thing (e.g., record anon(T t, U u, V v)), or
something that holds an Object[], or something with three int fields and two
ref fields, or whatever the runtime decides to serve up.
The template mechanism wants almost exactly the same thing for bundling the
parameters for uninterprted template strings.
Think of it as a macro-box; instead of boxing primitives to Object and Objects
to varargs, there's a single boxing operation from a tuple to an opaque type.
On 3/3/2022 8:57 AM, Jim Laskey wrote:
We propose to provide a runtime anonymous carrier class object generator;
java.lang.runtime.Carrier. This generator class is designed to share anonymous
classes when shapes are similar. For example, if several clients require
objects containing two integer fields, then Carrier will ensure that each
client generates carrier objects using the same underlying anonymous class.
Providing this mechanism decouples the strategy for carrier class generation
from the client facility. One could implement one class per shape; one class
for all shapes (with an Object[]), or something in the middle; having this
decision behind a bootstrap means that it can be evolved at runtime, and
optimized differently for different situations.
Motivation
The String Templates JEP
draft<https://bugs.openjdk.java.net/browse/JDK-8273943> proposes the
introduction of a TemplatedString object for the primary purpose of carrying
the template and associated values derived from a template literal. To avoid
value boxing, early prototypes described these carrierobjects using
per-callsite anonymous classes shaped by value types, The use of distinct
anonymous classes here is overkill, especially considering that many of these
classes are similar; containing one or two object fields and/or one or two
integral fields. Pattern matching has a similar issue when carrying the values
for the holes of a pattern. With potentially hundreds (thousands?) of template
literals or patterns per application, we need to find an alternate approach for
these value carriers.
Description
In general terms, the Carrier class simply caches anonymous classes keyed on
shape. To further increase similarity in shape, the ordering of value types is
handled by the API and not in the underlying anonymous class. If one client
requires an object with one object value and one integer value and a second
client requires an object with one integer value and one object value, then
both clients will use the same underlying anonymous class. Further, types are
folded as either integer (byte, short, int, boolean, char, float), long (long,
double) or object. [We've seen that performance hit by folding the long group
into the integer group is significant, hence the separate group.]
The Carrier API uses MethodType parameter types to describe the shape of a
carrier. This incorporates with the primary use case where bootstrap methods
need to capture indy non-static arguments. The API has three static methods;
// Return a constructor MethodHandle for a carrier with components
// aligning with the parameter types of the supplied methodType.
static MethodHandle constructor(MethodType methodType)
// Return a component getter MethodHandle for component i.
static MethodHandle component(MethodType methodType, int i)
// Return component getter MethodHandles for all the carrier's components.
static MethodHandle[] components(MethodType methodType)
Examples
import java.lang.runtime.Carrier;
...
// Define the carrier description.
MethodType methodType =
MethodType.methodType(Object.class, byte.class, short.class,
char.class, int.class, long.class,
float.class, double.class,
boolean.class, String.class);
// Fetch the carrier constructor.
MethodHandle constructor = Carrier.constructor(methodType);
// Create a carrier object.
Object object = (Object)constructor.invokeExact((byte)0xFF, (short)0xFFFF,
'C', 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFFL,
1.0f / 3.0f, 1.0 / 3.0,
true, "abcde");
// Get an array of accessors for the carrier object.
MethodHandle[] components = Carrier.components(methodType);
// Access fields.
byte b = (byte)components[0].invokeExact(object);
short s = (short)components[1].invokeExact(object);
char c =(char)components[2].invokeExact(object);
int i = (int)components[3].invokeExact(object);
long l = (long)components[4].invokeExact(object);
float f =(float)components[5].invokeExact(object);
double d = (double)components[6].invokeExact(object);
boolean tf (boolean)components[7].invokeExact(object);
String s = (String)components[8].invokeExact(object));
// Access a specific field.
MethodHandle component = Carrier.component(methodType, 3);
int ii = (int)component.invokeExact(object);