Paul King created GROOVY-12542:
----------------------------------
Summary: Branch directly on comparisons in conditions instead of
first producing a boolean value
Key: GROOVY-12542
URL: https://issues.apache.org/jira/browse/GROOVY-12542
Project: Groovy
Issue Type: Improvement
Reporter: Paul King
h3. Problem
A comparison used as an {{if}}/{{while}}/{{for}} condition is first turned into
a {{0}}/{{1}} value and then branched on, instead of jumping directly as javac
does:
{code:groovy}
@groovy.transform.CompileStatic
static void m(int n) {
for (int i = 0; i < n; i++) { }
}
{code}
{noformat}
Groovy (current) javac (equivalent Java)
ILOAD i ILOAD i
ILOAD n ILOAD n
IF_ICMPGE L1 IF_ICMPGE EXIT
ICONST_1
GOTO L2
L1: ICONST_0
L2: IFEQ EXIT
{noformat}
The comparison code ({{BinaryExpressionWriter.writeStdCompare}} and related
helpers) always produces a boolean, and the statement writers then emit
{{IFEQ}}/{{IFNE}} on it. This affects dynamic and statically compiled code. The
peephole optimizer cannot help, because the pattern spans labels.
Besides the extra instructions and branches, some bytecode consumers only
recognise javac's shape. For example, TornadoVM's TC compiler ({{CountedLoop}}
in tc-ir) requires a loop header to end in {{cond_br (icmp iv, bound)}}. With
the extra boolean step it finds no loop bound, so {{@Parallel}} loops written
in Groovy fail with:
{noformat}
TC-E3103 the bounds of the @Parallel loops are not compile-time constants after
specialisation, so no launch domain can be derived
{noformat}
h3. Proposed change
Add a small {{MethodVisitor}} stage, {{ConditionalJumpFoldingMethodVisitor}},
installed by {{PeepholeOptimizingClassVisitor}} directly under
{{PeepholeOptimizingMethodVisitor}}. It rewrites:
{noformat}
Jcc L1; ICONST_1; GOTO L2; L1: ICONST_0; L2: IFEQ L3 ==> Jcc L3
Jcc L1; ICONST_1; GOTO L2; L1: ICONST_0; L2: IFNE L3 ==> !Jcc L3
{noformat}
for any conditional jump ({{IFxx}}, {{IF_ICMPxx}}, {{IF_ACMPxx}},
{{IFNULL}}/{{IFNONNULL}}). Partial matches are replayed unchanged.
Safety:
* *Exact semantics:* the folded jump tests the same operands, or the same
{{LCMP}}/{{FCMPx}}/{{DCMPx}} result, as the original, so NaN behaviour is
unchanged.
* *Shared labels untouched:* the fold only fires when {{L1}} and {{L2}} are
each referenced solely by the pattern, so labels shared with other jumps (e.g.
from {{&&}}/{{||}}) are left as is.
* *Fails loudly:* if a removed label were ever referenced later, a
{{GroovyBugError}} is raised rather than emitting wrong bytecode.
* *Stored values unaffected:* a materialized boolean that is stored rather than
branched on (e.g. {{boolean r = a < b}}) is not touched.
* *Frames:* classes are written with {{COMPUTE_FRAMES}}, so no stack map frames
need adjusting.
* {{PeepholeOptimizingMethodVisitor.printTraceBytecode}} now unwraps the new
stage too.
h3. Tests
* New {{ConditionalJumpFoldingMethodVisitorTest}}:
** bytecode shape for {{for}} and {{if}} conditions
** stored comparisons are not folded
** runtime results in dynamic and {{@CompileStatic}} code for {{!}}, {{&&}},
{{||}}, ternary, int/long/float/double, null and identity comparisons
** for NaN, a folded branch always agrees with the same comparison stored as a
value
* Updated expected bytecode in {{StaticCompilationTest.testSingleAssignment3}},
{{StaticCompileNullCompareOptimizationTest.testOptimizeGroovyTruthForPrimitiveBoolean2}}
and {{testOptimizeGroovyTruthForPrimitiveNumberType}}, which asserted the old
shape.
* Full core test suite: 18,659 tests, 0 failures.
h3. Note
While testing, I noticed that {{Double.NaN >= 1d}} and {{1d >= Double.NaN}}
both evaluate to {{true}} in Groovy (dynamic and {{@CompileStatic}}, also on
6.0.0-beta-2), whereas IEEE 754 and Java give {{false}}. This is existing
behaviour and is preserved by this change; it may warrant a separate issue.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)