methods with scalarized arguments (original) (raw)
John Rose john.r.rose at oracle.com
Thu May 17 18:29:12 UTC 2018
- Previous message (by thread): value type hygiene
- Next message (by thread): methods with scalarized arguments
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
On May 17, 2018, at 7:27 AM, Roland Westrelin <rwestrel at redhat.com> wrote:
[moving to valhalla-dev]
Thanks; I'm also changing the subject line.
Folks, there are many ideas, both practical and speculative, floating back and forth on the valhalla-spec-experts list; it's easy to read them in the archives. Roland is right to pull practical discussion of implementation (of something we know we want to do now!) to this list.
When it comes to the calling convention, there's one extra bit of complexity: method handles. For a method handle invoke to a method m() with value arguments, there will be a lambda form with a call to a method handle linker. If the LF is compiled as a standalone compiled method, the JIT has no way to know values are expected by the method that's behind the linker method so it passes buffered values. If method m() is JIT'ed so it's expecting scalarized values, the method handle call fails.
Sometimes I lump the interpreter and reflection with method handle linkers. The MH linkers are different in that they are supposed to run at almost full speed even when compiled, so they try hard to avoid full permutations of the argument list. Which leads to…
Idea: Avoid full permutations between the entry points by passing the arguments in an order in which the buffered calling sequence is an initial sequence of the scalarized calling sequence. Examples:
signature f(object, point, object) buffered: f(R1, R2, R3) scalarized: f(R1, {R2,R4}, R3)
signature f(object, complex, object) buffered: f(R1, R2, R3) scalarized: f(R1, {F1,F2}, R3)
signature f(object, vector, object) buffered: f(R1, R2, R3) scalarized: f(R1, V1, R3)
Where A[i], F[i], V[i] are the general, floating, and vector registers allocated internally to Java calling sequences.
Given these assignments, an adapter may need only limited data movement to convert between calling conventions.
This idea comes from the SPARC ABI, and also from HotSpot's handling of JNI arguments, which carefully arrange for coherence between sibling calling sequences. (In the case of SPARC, it allows a varargs function to spill register-based arguments into a pre-allocated stack argument area, which then becomes the va_list. Thus, varargs mode is memory only, while the normal mode is register based, with "holes" in the argument area in case a callee wants to play at varargs. Similar to our scalarized vs. buffered distinction.)
That problem is not specific to Lworld. It exists in MVT with _Value. We never solved it. But from previous discussions, it seems the way to solve that problem is for every method with value arguments to have 2 entry points: a scalarized values entry point and a buffered values entry point. In a first implementation, the buffered values entry point could fall back to the interpreter and the scalarized values entry point be eventually a JIT'ed method.
I like this. I think you are talking mainly about single-method calls, where there is no v-table in the way. (Invokes of static, private, or final methods, or invokespecial.) We can think about these, if it helps, as calls to degenerate v-table hierarchies of depth 1.
Now assuming we have to have 2 method entry points, why not use the buffered value entry point when one of the value arguments is null (or maybe null)?
Are you imagining a single nmethod with two entry points? Currently, nmethods do have two entry points for distinct calling sequences. This might add two more: <VEP, UEP> x <Buffered, Scalarized>.
I like this line of thinking very much.
What entry point to use at a call site could be decided at JIT compilation time: either all arguments are statically known to be non null and we can go with the scalarized values entry point or we fall back to the buffered values entry point. Whether the code being JIT'ed is legacy or not doesn't factor explicitly in the decision.
This is a very good property. We could let JIT dig around inside the implementation to see if it came from a legacy class, but that would be smelly code, I think. Better to have each method put out a bit-mask or some other thing right along side its descriptor, saying "this is where scalarization happens in my descriptor". BTW, the bit-map could have a fixed maximum length; 32 bits is not too small. Scalarizing can be restricted to normal arity methods, at least for a start. Speculation: Scalarization isn't as valuable for high-arity methods (arity >> 5).
Extra idea, use or toss: Arrange the buffered and scalarized entry points of an nmethod (or adapter) with a globally fixed offset between them. Then upgrading or downgrading a call is easy to do, even in assembly code. For extra points, put a bitmask word in the instruction stream, immediately before the scalarized entry point, so it is crystal clear when there is a match or mismatch between caller and callee.
— John
- Previous message (by thread): value type hygiene
- Next message (by thread): methods with scalarized arguments
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]