[ 
https://issues.apache.org/jira/browse/GROOVY-12542?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12542:
-------------------------------
    Description: 
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

Relates to GROOVY-12538 (primitive double/float {{>}} and {{>=}} return true 
for NaN). That fix changes which compare instruction ({{DCMPL}}/{{DCMPG}}, 
{{FCMPL}}/{{FCMPG}}) precedes the conditional jump. This change only rewrites 
from the jump onwards, so it preserves whichever instruction is chosen. Tested 
together: with PR #3053 applied on top of this change, {{DoubleOperationsTest}} 
and {{ConditionalJumpFoldingMethodVisitorTest}} both pass.

  was:
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.


> 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
>            Priority: Major
>
> 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
> Relates to GROOVY-12538 (primitive double/float {{>}} and {{>=}} return true 
> for NaN). That fix changes which compare instruction ({{DCMPL}}/{{DCMPG}}, 
> {{FCMPL}}/{{FCMPG}}) precedes the conditional jump. This change only rewrites 
> from the jump onwards, so it preserves whichever instruction is chosen. 
> Tested together: with PR #3053 applied on top of this change, 
> {{DoubleOperationsTest}} and {{ConditionalJumpFoldingMethodVisitorTest}} both 
> pass.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to