RFR: 8360389: Support printing from C2 compiled code
Benoît Maillard
bmaillard at openjdk.org
Mon Aug 18 07:07:00 UTC 2025
This PR adds support for printf-style debugging from C2 compiled code. This is implemented as a runtime call to a C++ method that prints the values of the nodes passed as arguments. The runtime C++ method is implemented with the help of variadic templates, as it is expected to support various combinations of argument types.
## Usage
Suppose we have this piece of Java code, that simply computes an arithmetic operation, and
that we compile `square` with C2.
class Square {
static int square(int a) {
return a * a;
}
public static void main(String[] args) {
square(9);
}
}
We would like to inspect the node that contains the value returned in this method.
We can add a call to `Compile::make_debug_print` and pass a message, the IGVN instance, as well as the node(s) that we would like to inspect.
```c++
void Compile::return_values(JVMState* jvms) {
GraphKit kit(jvms);
Node* ret = new ReturnNode(TypeFunc::Parms,
kit.control(),
kit.i_o(),
kit.reset_memory(),
kit.frameptr(),
kit.returnadr());
// Add zero or 1 return values
int ret_size = tf()->range()->cnt() - TypeFunc::Parms;
Node* return_value;
if (ret_size > 0) {
kit.inc_sp(-ret_size); // pop the return value(s)
kit.sync_jvms();
return_value = kit.argument(0);
ret->add_req(return_value);
// <-------------------- Simply insert this
C->make_debug_print_new<jint>("return:", initial_gvn(), return_value);
}
// bind it to root
root()->add_req(ret);
record_for_igvn(ret);
initial_gvn()->transform(ret);
}
We can then call run our code with `-XX:CompileCommand="compileonly,Square::square`
and we get the following output:
return:
int 81
This case is of course trivial, but more useful examples are shown later.
## Implementation
## Implementation
The debugging capability is implemented as a runtime call to a C++ printing method. For this, `Compile::make_debug_print` inserts a `CallLeafNode` into the graph and rewires control flow as needed.
The actual printing is handled by the `SharedRuntime::debug_print` method, written with a variadic template to support various argument type combinations. A pointer to this runtime method is obtained at compile time and is passed to the `CallLeafNode` constructor.
The first argument to the runtime printing method is the printing message, a static string encoded as a `ConP` node in the graph. The remaining arguments are the actual nodes being printed.
A key part is wiring the new call into the existing control-flow graph:
- A call node needs a control input and also at least one control usage (otherwise it will be removed).
- We wish the debug print call to be as "close as possible" in the CFG to the nodes being printed, by attaching it at the most appropriate control point.
- To do this, we:
- Follow the data dependencies of each argument node until reaching control-flow nodes, collecting these as candidates.
- Pick the candidate that is reachable from all other candidates in the CFG (i.e., all other candidates are predecessors of the one we pick).
- We then rewire the control users of the selected candidate node to use the new call node as their control input. This ensures the call node has a control usage and is not deleted as dead code.
<img width="424" height="257" alt="image" src="https://github.com/user-attachments/assets/228a46ee-7149-4a78-a310-89e3168eb1d1" />
## More examples
Here are other examples that showcase uses of this debug feature at various stages
in the compilation pipeline. Some of them are inspired by issues I worked on recently.
### Identity optimization
#### Modified C2 code
In `ConvD2LNode::Identity`:
```c++
Node* ConvD2LNode::Identity(PhaseGVN* phase) {
// Remove ConvD2L->ConvL2D->ConvD2L sequences.
if(in(1)->Opcode() == Op_ConvL2D && in(1)->in(1)->Opcode() == Op_ConvD2L) {
Node* identity = in(1)->in(1);
phase->C->make_debug_print_new<jlong>("value: ", phase, identity, phase->C->top());
return identity;
}
return this;
}
#### Java program
class TestSimpleD2L {
int instanceCount;
void test(long d) {
int i = 1;
int j = 1;
while (++i < 37) {
d = i;
for (; 8 > j; ++j) {
instanceCount = (int) d;
d = instanceCount;
}
}
}
void main(String[] strArr) {
for (int i = 0; i < 50_000; ++i) {
test(1);
}
}
}
java -XX:CompileCommand=quiet -XX:CompileCommand="compileonly,TestSimpleD2L::test" -XX:-TieredCompilation -Xbatch -Xcomp TestSimpleD2L.java
### Selecting nodes by ID between two optimization phases
#### Modified C2 code
In `Compile::Optimize`
```c++
// Set loop opts counter
if((_loop_opts_cnt > 0) && (has_loops() || has_split_ifs())) {
{
TracePhase tp(_t_idealLoop);
PhaseIdealLoop::optimize(igvn, LoopOptsDefault);
_loop_opts_cnt--;
if (major_progress()) print_method(PHASE_PHASEIDEALLOOP1, 2);
if (failing()) return;
}
// <----------------- START
Node* parm0 = C->root()->find(98);
Node* parm1 = C->root()->find(108);
C->make_debug_print_new<jint, jint>("hello there: ", &igvn, parm0, parm1);
// <----------------- END
#### Java program
public class Loop {
static int foo(int x) {
return x % 17;
}
static int test(int iterations) {
int x = 1;
int y = iterations + 20;
for (int i = 0; i < iterations; i++) {
x += foo(x);
// to print result of foo: id = 98
// to print x + foo(x): id = 108
}
return x + y;
}
public static void main(String[] args) {
int res = test(100);
System.out.println(res);
}
}
java -XX:CompileCommand=quiet -XX:CompileCommand="compileonly,Loop::test" -XX:-TieredCompilation -Xcomp -Xbatch -XX:-Inline Loop.java
### Ideal optimization
#### Modified C2 code
In `ModINode::Ideal`:
```c++
// Check for useless control input
// Check for excluding mod-zero case
if (in(0) && (ti->_hi < 0 || ti->_lo > 0)) {
set_req(0, nullptr); // Yank control input
phase->C->make_debug_print_new<jint>("ModI: ", phase, this);
return this;
}
#### Java program
public class TestModControlFoldedAfterCCP {
static void test() {
int i22, i24 = -1191, i28;
int iArr1[] = new int[1];
for (int i = 1;i < 100; i++) {
for (int j = 4; j > i; j--) {
i22 = i24;
// divisor is either -1191 or -13957
iArr1[0] = 5 % i22;
}
for (i28 = i; i28 < 2; ++i28) {
i24 = -13957;
}
}
}
public static void main(String[] args) {
test();
}
}
java -XX:CompileCommand=quiet -XX:CompileCommand="compileonly,TestModControlFoldedAfterCCP::test" -XX:-TieredCompilation -Xcomp -Xbatch -XX:-Inline TestModControlFoldedAfterCCP.java
## Testing
- [x] [GitHub Actions](https://github.com/benoitmaillard/jdk/actions?query=branch%3AJDK-8360389)
- [x] tier1-3, plus some internal testing
Thank you for reviewing!
-------------
Commit messages:
- Fix style and add comments
- Use format consistent with fieldDescriptor::print_on_for
- Fix bad format specifier
- Clean up includes
- Use platform-dependent macros for printing primitives types
- Remove old versions and rename new one
- Add null check when finding control candidates
- Remove unnecessary additions to worklist
- Fix bug caused by bad rewiring with nodes that have several control inputs
- Remove --i in DUIterator use
- ... and 31 more: https://git.openjdk.org/jdk/compare/c70258ca...d7641ea7
Changes: https://git.openjdk.org/jdk/pull/26475/files
Webrev: https://webrevs.openjdk.org/?repo=jdk&pr=26475&range=00
Issue: https://bugs.openjdk.org/browse/JDK-8360389
Stats: 290 lines in 6 files changed: 290 ins; 0 del; 0 mod
Patch: https://git.openjdk.org/jdk/pull/26475.diff
Fetch: git fetch https://git.openjdk.org/jdk.git pull/26475/head:pull/26475
PR: https://git.openjdk.org/jdk/pull/26475
More information about the hotspot-dev
mailing list