diff options
| author | Cliff Click <cliffc@acm.org> | 2026-10-28 12:26:28 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-10-28 12:26:28 -0700 |
| commit | e481e3d215882faa82b19f29cfde5746d4e51863 (patch) | |
| tree | d6035c13e4ee471b67d7dd4732a62bc04020fbd1 /chapter28a/src | |
| parent | 07a87b4461015dd81f1d40cef1e2378882c86479 (diff) | |
| download | Simple-e481e3d215882faa82b19f29cfde5746d4e51863.tar.gz Simple-e481e3d215882faa82b19f29cfde5746d4e51863.zip | |
Backport Opto speedups and Minus fix
Diffstat (limited to 'chapter28a/src')
5 files changed, 137 insertions, 49 deletions
diff --git a/chapter28a/src/main/java/com/seaofnodes/simple/codegen/Opto.java b/chapter28a/src/main/java/com/seaofnodes/simple/codegen/Opto.java index 4db277df..6370bf5b 100644 --- a/chapter28a/src/main/java/com/seaofnodes/simple/codegen/Opto.java +++ b/chapter28a/src/main/java/com/seaofnodes/simple/codegen/Opto.java @@ -1,6 +1,7 @@ package com.seaofnodes.simple.codegen; import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.IterPeeps.WorkList; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Ary; @@ -25,30 +26,39 @@ abstract public class Opto { // FunNodes. public static void opto(CodeGen code) { // Reset Types before running the worklist algorithm - Ary<Type> oldTypes = new Ary<>(Type.class); // Used for asserts only - resetToTop(code,oldTypes); + Ary<Type> oldTypes = new Ary<>(Type.class); + WorkList<Node> deps = new WorkList<>(); + WorkList<Node> changes = new WorkList<>(); + Ary<Node> modes = new Ary<>(Node.class); + resetToTop(code,oldTypes,modes,deps); unlinkStart(code); // OPTIMISTIC INTERPROCEDURAL SCCP - sccp(code, oldTypes); + sccp(code, oldTypes, changes, deps); - // SCCP can leave a recursive numeric component at BOTTOM with no - // arithmetic mode selected. Resolve the whole component from all - // available int/float evidence before IterPeeps consumes the types. - resolveNumericModes(code); + // Settle modes, then resume SCCP on affected nodes without resetting types. + // Any further type changes must still fall; BOTTOM results stay BOTTOM. + resolveNumericModes(code,modes); if( !code._iter._work.isEmpty() ) - sccp(code, oldTypes); + sccp(code, oldTypes, changes, deps); // After SCCP add everything changed to the worklist and see if // peepholes can make more progress. - moveChangesToWorklist(code, oldTypes); + Node dep; + while( (dep=deps.pop())!=null ) code.add(dep); + moveChangesToWorklist(code, oldTypes, changes); } // Reset Types before running the worklist algorithm - private static void resetToTop(CodeGen code, Ary<Type> oldTypes ) { + private static void resetToTop(CodeGen code, Ary<Type> oldTypes, Ary<Node> modes, WorkList<Node> deps ) { code._start.walk( x -> { - oldTypes.setX(x._nid,x._type); // Record original types for asserts + // Opto ends Parm.inProgress(), enabling constant folding even + // when a parameter's type was already constant before SCCP. + if( x instanceof ParmNode && x._type.isHighOrConst() ) deps.push(x); + // SCCP adds call edges and default arguments, not numeric operations. + if( x instanceof ModeNode mode && mode.mode()==0 ) modes.push(x); + oldTypes.setX(x._nid,x._type); // Compare the final SCCP type with Iter's type x._type = Type.TOP; // Reset all Nodes to TOP code._iter.add(x); // Visit everybody at least once; most Nodes produce better than TOP return null; @@ -162,7 +172,7 @@ abstract public class Opto { // The core SCCP algorithm - private static void sccp( CodeGen code, Ary<Type> oldTypes ) { + private static void sccp( CodeGen code, Ary<Type> oldTypes, WorkList<Node> changes, WorkList<Node> deps ) { // Iter workList is empty at this point // OPTIMISTIC PASS Node n; @@ -171,6 +181,8 @@ abstract public class Opto { if( n.isDead() ) continue; count++; Type oval = n._type, nval = n.compute(); + // Filter candidates against the final SCCP type after convergence. + if( nval!=oldTypes.atX(n._nid) || n.isUnlocked() ) changes.push(n); if( oval == nval ) continue; assert oval.isa(nval) : "Non-monotonic SCCP: "+n.getClass().getSimpleName()+"#"+n._nid+" "+n+" old="+oval+" new="+nval; // Types start high and always fall Type pesiVal = oldTypes.atX(n._nid); @@ -210,6 +222,9 @@ abstract public class Opto { // Since n._type changed, visit all output neighbors code._iter.addAll(n._outputs); + // SCCP consumes these without running peepholes. Save them for + // the following Iter pass before moving clears the dependency list. + for( int i=0; i<n.nDeps(); i++ ) deps.push(n.dep(i)); n.moveDepsToWorklist(code._iter); // Quadratic (expensive) small-step assert. assert !CodeGen.expensiveAssert(count) || worklistCheck(code); @@ -246,22 +261,28 @@ abstract public class Opto { // After SCCP add everything changed to the worklist and see if peepholes // can make more progress. - private static void moveChangesToWorklist(CodeGen code, Ary<Type> oldTypes) { - code._start.walk( x -> { - assert x.compute() == x._type; // Hit the fixed point - assert x._nid >= oldTypes._len || x._type.isa(oldTypes.at(x._nid)); // Hit at least the bottom-up type - code.add(x); - return null; - }); + private static void moveChangesToWorklist(CodeGen code, Ary<Type> oldTypes, WorkList<Node> changes) { + Node n; + while( (n=changes.pop())!=null ) { + if( n.isDead() ) continue; + assert n.compute() == n._type; // Hit the fixed point. + Type old = oldTypes.atX(n._nid); + assert old==null || n._type.isa(old); + if( n._type!=old || n.isUnlocked() ) { + code.add(n); + code.addAll(n._inputs); + code.addAll(n._outputs); + n.moveDepsToWorklist(); + } + } } - // Resolve arithmetic modes which are ambiguous only because they belong to - // a recursive component. Float wins because integer operands can be - // converted to float. - private static void resolveNumericModes(CodeGen code) { + // Resolve modes left undecided by SCCP, following recursive inputs for + // evidence. Float wins because integer operands can be converted to float. + private static void resolveNumericModes(CodeGen code, Ary<Node> modes) { final BitSet visited = new BitSet(); - code._stop.walk(n -> { - if( n instanceof ModeNode modeNode && modeNode.mode()==0 ) { + for( Node n : modes ) { + if( !n.isDead() && n instanceof ModeNode modeNode && modeNode.mode()==0 ) { visited.clear(); visited.set(n._nid); // Inspect inputs, not Bool's integer result int mode = 0; @@ -270,17 +291,11 @@ abstract public class Opto { // A closed recursive numeric component can have no remaining // concrete evidence after SCCP, e.g. dead all-integer loops. // With no float evidence, settle to integer. - if( mode == 0 ) - mode = 1; - if( mode != 0 ) { - // If both evidence, FP wins - modeNode.setMode((byte)((mode&2)==2 ? 2 : 1)); - code.add(n); - code.addAll(n.outs()); - } + modeNode.setMode((byte)((mode&2)==2 ? 2 : 1)); + code.add(n); + code.addAll(n.outs()); } - return null; - }); + } } // Gather numeric evidence through Phi and interprocedural return cycles. diff --git a/chapter28a/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter28a/src/main/java/com/seaofnodes/simple/node/FunNode.java index ad79130e..15626a07 100644 --- a/chapter28a/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter28a/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -142,7 +142,7 @@ public class FunNode extends RegionNode { } // Upgrade inferred or user-written return type to actual - if( _ret!=null && _ret._type instanceof TypeTuple tt && + if( _ret!=null && addDep(_ret)._type instanceof TypeTuple tt && tt.ret() != _sig.ret() && tt.ret().isa(_sig.ret()) ) { setSig(_sig.makeFrom(tt.ret())); return this; diff --git a/chapter28a/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/chapter28a/src/main/java/com/seaofnodes/simple/node/MinusNode.java index 4d7e3ddb..6409ad21 100644 --- a/chapter28a/src/main/java/com/seaofnodes/simple/node/MinusNode.java +++ b/chapter28a/src/main/java/com/seaofnodes/simple/node/MinusNode.java @@ -2,7 +2,6 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Utils; import java.util.HashMap; @@ -29,28 +28,27 @@ public class MinusNode extends Node implements ModeNode { @Override public Type compute() { Type t = in(1)._type; + // Constant array payloads lie below their element ranges in the lattice. + if( t instanceof TypeConAry ary ) t = ary.elem(); if( t==null ) return Type.TOP; if( t.isHigh() ) return _mode==0 ? Type.TOP : _mode==1 ? TypeInteger.TOP : t instanceof TypeFloat ? t : TypeFloat.F64.dual(); - byte mode = _mode; - if( mode==0 ) { - if( t instanceof TypeFloat ) mode=2; - if( t instanceof TypeInteger ) mode=1; - } - if( mode==0 ) - return CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal() ? Type.BOTTOM : Type.TOP; - if( mode==1 ) { - if( t instanceof TypeInteger i0 && - i0._min != Long.MIN_VALUE && i0._max != Long.MIN_VALUE ) + if( t instanceof TypeInteger i0 ) { + if( _mode==2 ) return TypeFloat.F64; + if( i0._min != Long.MIN_VALUE && i0._max != Long.MIN_VALUE ) return TypeInteger.make(-i0._max,-i0._min,i0._widen); return TypeInteger.BOT; } - if( t instanceof TypeFloat f ) + if( t instanceof TypeFloat f ) { + if( _mode==1 ) return TypeInteger.BOT; return f.isConstant() ? TypeFloat.constant(-f.value()) : f; - return TypeFloat.F64; + } + // No numeric family is known. This must be below both integer and + // float results, in either analysis direction and with any mode. + return Type.BOTTOM; } @Override diff --git a/chapter28a/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter28a/src/main/java/com/seaofnodes/simple/node/Node.java index de39ca80..aeea5fbb 100644 --- a/chapter28a/src/main/java/com/seaofnodes/simple/node/Node.java +++ b/chapter28a/src/main/java/com/seaofnodes/simple/node/Node.java @@ -566,6 +566,8 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { // probed to see if it can be inserted. His edges are "locked", because // hacking his edges will change his hash. int _hash; + // Edge and mode edits remove the node from GVN, even if its type is unchanged. + public boolean isUnlocked() { return _hash==0; } // If the _hash is set, then the Node is in the GVN table; remove it. void unlock() { diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/MinusNodeTest.java b/chapter28a/src/test/java/com/seaofnodes/simple/MinusNodeTest.java new file mode 100644 index 00000000..497546df --- /dev/null +++ b/chapter28a/src/test/java/com/seaofnodes/simple/MinusNodeTest.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MinusNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.*; +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MinusNodeTest { + @Test public void testMonotoneCompute() { + CodeGen code = new CodeGen("return 0;"); + // Type.gather also loads cached fixtures which do not survive later compiler resets. + ArrayList<Type> inputs = new ArrayList<>(Arrays.asList( + Type.BOTTOM,Type.CONTROL,Type.NIL,TypeScalar.BOT,TypePtr.PTR, + TypeMemPtr.BOT,TypeFunPtr.BOT,TypeMem.BOT, + TypeConAryB.make(new byte[]{1,2,3}))); + TypeInteger.gather(inputs); + TypeFloat.gather(inputs); + int size = inputs.size(); + for( int i=0; i<size; i++ ) inputs.add(inputs.get(i).dual()); + for( long value : new long[]{Long.MIN_VALUE,Long.MIN_VALUE+1,-1,1,Long.MAX_VALUE} ) + inputs.add(TypeInteger.constant(value)); + for( double value : new double[]{-0.0,0.0,-1.0,1.0,Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY} ) + inputs.add(TypeFloat.constant(value)); + Type[][] expected = new Type[3][inputs.size()]; + for( CodeGen.Phase phase : new CodeGen.Phase[]{CodeGen.Phase.Parse,CodeGen.Phase.Iter,CodeGen.Phase.Opto,CodeGen.Phase.IterPostOpto} ) { + code.driver(phase); + for( byte mode=0; mode<3; mode++ ) { + // An isolated analysis input: vary its type without peepholes + // selecting a mode or folding the node under test. + Node input = new ConstantNode(Type.BOTTOM); + input.setDef(0,null); + MinusNode minus = new MinusNode(input,mode); + Type[] results = new Type[inputs.size()]; + for( int i=0; i<inputs.size(); i++ ) { + input._type = inputs.get(i); + results[i] = minus.compute(); + if( phase==CodeGen.Phase.Parse ) expected[mode][i] = results[i]; + else assertSame("Phase changed compute",expected[mode][i],results[i]); + } + // The same order relation covers both falling SCCP types + // and rising Iter types, including crossing numeric families. + for( int i=0; i<inputs.size(); i++ ) + for( int j=0; j<inputs.size(); j++ ) + if( inputs.get(i).isa(inputs.get(j)) ) + assertTrue("mode="+mode+": "+inputs.get(i)+" -> "+inputs.get(j)+ + " gave "+results[i]+" -> "+results[j],results[i].isa(results[j])); + } + } + } + + @Test public void testUnresolvedInput() { + new CodeGen("return 0;").driver(CodeGen.Phase.Opto); + Node input = new ConstantNode(TypeInteger.TRUE); + MinusNode minus = new MinusNode(input); + assertSame(TypeInteger.constant(-1),minus.compute()); + input._type = Type.BOTTOM; + assertSame(Type.BOTTOM,minus.compute()); + minus.setMode((byte)1); + assertSame(Type.BOTTOM,minus.compute()); + } + + @Test public void testRecursiveFloat() { + CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(x-1) : 1.0; }; return _neg(arg);") + .driver(CodeGen.Phase.TypeCheck); + for( int i=0; i<4; i++ ) + assertEquals((i&1)==0 ? "1.0" : "-1.0",Eval2.eval(code,i)); + } +} |
