summaryrefslogtreecommitdiff
path: root/chapter25
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-30 19:25:04 -0700
committerCliff Click <cliffc@acm.org>2026-09-30 19:25:04 -0700
commitc4dcf6f751b35c7a2a3b46fa2d69b95d37d082a6 (patch)
treee5ad31706399578310cfbb3b0387a77cf83372ba /chapter25
parenta996996a1ad776089a4cb9cbf7744f5f4e8dde6d (diff)
downloadSimple-c4dcf6f751b35c7a2a3b46fa2d69b95d37d082a6.tar.gz
Simple-c4dcf6f751b35c7a2a3b46fa2d69b95d37d082a6.zip
Finish forward-port mem ops thru ch25
Diffstat (limited to 'chapter25')
-rw-r--r--chapter25/README.md18
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java49
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java2
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java238
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java34
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/Node.java1
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/PhiNode.java106
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/RegionNode.java2
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/StoreNode.java4
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java5
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter11Test.java2
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java2
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java4
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java7
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java104
15 files changed, 383 insertions, 195 deletions
diff --git a/chapter25/README.md b/chapter25/README.md
index 4feef80f..b7925a1f 100644
--- a/chapter25/README.md
+++ b/chapter25/README.md
@@ -9,6 +9,16 @@ These are the contents of the current snapshot. Independent correctness fixes
will move to earlier chapters first; moving the larger SSA redesign backward,
or splitting this chapter, is deferred. See the [backport queue](../docs/chapter-backports.md).
+The memory work from Chapters 21-24 carries forward here: Load search proves
+all arms of a merge before moving a read out of a loop, Phi factoring checks
+memory clobbers, and scheduling follows aliases through memory aggregates.
+The search follows `EscapeNode` to private or public memory and waits for
+unresolved Store aliases. Array-length reads have no element bounds check;
+array-element reads retain control. Integer value Phis use the existing typed
+conversion machinery to preserve the load's width across loop widening.
+New still produces private constructor memory, with publication handled by
+Escape; the earlier chapters' allocation-input design does not replace it.
+
You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter25)
in the [linear history](https://github.com/SeaOfNodes/Simple/tree/linear) and
[compare it with Chapter 24](https://github.com/SeaOfNodes/Simple/compare/linear-chapter24...linear-chapter25).
@@ -168,10 +178,12 @@ used only by the next instruction is removed when the operand accepts its
source register and is not tied to the instruction's result.
The table below is the **earlier audit**, before adjacent-copy forwarding.
-The current replay has ten String-constructor parse failures, also reproduced
+The replay has ten String-constructor parse failures, also reproduced
with the original allocator. On the identical 216 successful allocations,
-forwarding reduces 2,563 / 5,496 moves / weighted moves to 2,494 / 5,399.
-This is a partial comparison, not a replacement complete-suite total. The
+adjacent-copy forwarding reduced 2,563 / 5,496 moves / weighted moves to
+2,494 / 5,399. The subsequent memory corrections produce 2,488 / 5,379 on
+those same 216 successful allocations. These are partial comparisons, not
+replacement complete-suite totals. The
current cohort 25 has 14 allocations, including later C return ABI checks;
the historical table has ten. Native cohort-25 checks and fresh system-library
encoding pass in both runs. See the [backport record](../docs/chapter-backports.md)
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java
index ecd23c1e..484bcaa9 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java
@@ -172,17 +172,9 @@ public abstract class GlobalCodeMotion {
// Loads need their memory inputs' uses also done
if( n instanceof MemOpNode ld && ld._isLoad )
- for( Node memuse : ld.mem()._outputs )
- if( late[memuse._nid]==null &&
- // New makes new memory, never crushes load memory
- !(memuse instanceof NewNode) &&
- // Load-use directly defines memory
- (memuse._type instanceof TypeMem ||
- // Load-use directly defines memory
- memuse instanceof CallNode) ) {
- assert !( memuse._type instanceof TypeTuple tt && tt._types[ld._alias] instanceof TypeMem );
+ for( Node memuse : ld.antiDeps() )
+ if( late[memuse._nid]==null )
continue outer;
- }
// All uses done, schedule
_doSchedLate(n,ns,late,anti);
@@ -193,9 +185,7 @@ public abstract class GlobalCodeMotion {
for( Node def : n._inputs ) {
if( def==null ) continue;
if( late[def._nid]==null ) work.push(def);
- for( Node out : def._outputs )
- if( out instanceof MemOpNode ld && ld._isLoad && late[ld._nid]==null )
- work.push(ld);
+ wakeLoads(def,late,work,new BitSet());
}
if( n instanceof LoopNode loop )
for( Node phi : loop._outputs )
@@ -255,25 +245,32 @@ public abstract class GlobalCodeMotion {
best instanceof IfNode;
}
+ // A writer may consume a load's memory through an aggregate.
+ private static void wakeLoads(Node def, CFGNode[] late, WorkList<Node> work, BitSet visit) {
+ if( visit.get(def._nid) ) return;
+ visit.set(def._nid);
+ for( Node out : def._outputs )
+ if( out instanceof MemOpNode ld && ld._isLoad && late[ld._nid]==null ) work.push(ld);
+ if( def instanceof MemMergeNode )
+ for( int i=1; i<def.nIns(); i++ )
+ if( def.in(i)!=null ) wakeLoads(def.in(i),late,work,visit);
+ }
+
private static CFGNode find_anti_dep(CFGNode lca, MemOpNode load, CFGNode early, CFGNode[] late, int[] anti) {
// We could skip final-field loads here.
// Walk LCA->early, flagging Load's block location choices
for( CFGNode cfg=lca; cfg!=early.idom(); cfg = cfg.idom() )
anti[cfg._nid] = load._nid;
// Walk load->mem uses, looking for Stores causing an anti-dep
- for( Node mem : load.mem()._outputs ) {
+ for( Node mem : load.antiDeps() ) {
switch( mem ) {
case MemOpNode st:
- if( !st._isLoad && load._alias == st._alias ) {
- assert late[mem._nid] != null;
- lca = anti_dep(load,late[mem._nid],lca,st,anti);
- }
- break; // Loads do not cause anti-deps on other loads
- case EscapeNode esc:
- if( load._alias == esc.fld()._alias ) {
- assert late[mem._nid] != null;
- lca = anti_dep(load,late[mem._nid],lca,mem,anti);
- }
+ assert late[st._nid]!=null;
+ lca = anti_dep(load,late[st._nid],lca,st,anti);
+ break;
+ case EscapeNode st:
+ assert late[st._nid]!=null;
+ lca = anti_dep(load,late[st._nid],lca,st,anti);
break;
case CallNode call:
assert late[call._nid]!=null;
@@ -286,8 +283,6 @@ public abstract class GlobalCodeMotion {
if( phi.in(i)==load.mem() )
lca = anti_dep(load,phi.region().cfg(i),lca,null,anti);
break;
- case ReturnNode ret: break; // Load must already be ahead of Return
- case MemMergeNode ret: break; // Mem uses now on ScopeMin
default: throw Utils.TODO("Should not reach here");
}
}
@@ -299,7 +294,7 @@ public abstract class GlobalCodeMotion {
// Stores are already placed. Constrain the load at that block only.
if( anti[stblk._nid]==load._nid ) {
lca = stblk.domLCA(lca,null);
- if( lca==stblk && st!=null && st._inputs.find(load) == -1 )
+ if( lca==stblk && st!=null && !(st instanceof CallNode) && st._inputs.find(load) == -1 )
st.addDef(load); // Same-block load must precede the store.
}
return lca;
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
index cabf7d69..82c4af10 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
@@ -253,7 +253,7 @@ public class BulkMemPhiNode extends PhiNode {
mphi.setType(TypeMem.BOT.makeFrom(alias));
for( int i=1; i<nIns(); i++ )
mphi.addDef(CodeGen.CODE.add(preciseInput(in(i),alias)));
- return (MemPhiNode)mphi.peephole();
+ return CodeGen.CODE.add(mphi);
}
MemPhiNode _findPhi(int alias) {
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index 193fbe17..85ca42cd 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -31,6 +31,25 @@ public class LoadNode extends MemOpNode {
}
@Override public Tag serialTag() { return Tag.Load; }
+ @Override boolean canDrop(MemOpNode other, Node dep) {
+ return super.canDrop(other,dep) && !clobbered(dep);
+ }
+
+ // Check only immediate memory users. Writers clobber memory; Phis and
+ // aggregates might lead to a clobber, so stop rather than search further.
+ private boolean clobbered(Node dep) {
+ Node mem = mem();
+ dep.addDepForwards(mem);
+ for( Node use : mem._outputs ) {
+ if( use==null ) continue;
+ dep.addDepForwards(use);
+ if( use instanceof StoreNode || use instanceof PhiNode ||
+ use instanceof MemMergeNode ||
+ use instanceof NewNode || use instanceof EscapeNode || use instanceof CallNode ) return true;
+ }
+ return false;
+ }
+
// Debugger label
@Override public String label() { return "ld_"+mlabel(); }
@Override
@@ -89,10 +108,10 @@ public class LoadNode extends MemOpNode {
Node mem = mem();
Node ptr = ptr();
- // Loads into structs do not need a ctrl edge, as null-ptr checking is
- // baked into the type system. Loads into arrays DO need the ctrl
- // edge, at least until proper range-checking is in place.
- if( in(0)!=null && ptr._type instanceof TypeMemPtr tmp && !tmp._obj.isAry() ) {
+ // Null checks are handled by types. Array elements retain control for
+ // range checks, but reading an array's length has no index to check.
+ if( in(0)!=null && ptr._type instanceof TypeMemPtr tmp &&
+ (!tmp._obj.isAry() || _name.equals("#")) ) {
setDef(0,null);
return this;
}
@@ -112,19 +131,6 @@ public class LoadNode extends MemOpNode {
if( _alias == 1 )
return null;
- // External storage is shared even through different class namespaces.
- boolean external = ptr._type instanceof TypeMemPtr tmp &&
- (fld=tmp._obj.field(_name)) != null && fld._extern;
-
- // Simple Load-after-Store on same address.
- if( mem instanceof StoreNode st &&
- ptr == st.nnptr() &&
- off() == st.off() && // Must check same object
- st.val()._type.isa(_type) ) {
- assert _name.equals(st._name); // Equiv class aliasing is perfect
- return extend(st.val());
- }
-
if( mem instanceof MemMergeNode merge ) {
setDef(1,merge.alias(_alias));
// In support of uplifting control to a prior dominating load, if
@@ -144,67 +150,9 @@ public class LoadNode extends MemOpNode {
addDep(ld)._type.isa(_type) ) // and not rolling backwards
return ld;
- // Load-after-Store on same address, but bypassing provably unrelated
- // stores. This is a more complex superset of the above two peeps.
- // "Provably unrelated" is really weak.
- if( ptr instanceof ReadOnlyNode ro )
- ptr = ro.in(1);
- outer:
- while( true ) {
- switch( mem ) {
- case StoreNode st:
- if( ptr == addDep(st.ptr()) && off() == st.off() )
- return extend(castRO(st.val())); // Proved equal
- // Can we prove unequal? Offsets do not overlap?
- if( !off()._type.join(st.off()._type).isHigh() && // Offsets overlap
- (external || !neverAlias(ptr,st.ptr())) ) { // And might alias
- addDep( off()); // Offsets can fold, proving unequal
- addDep(st.off()); // Offsets can fold, proving unequal
- break outer; // Cannot tell, stop trying
- }
- // Pointers cannot overlap
- mem = st.mem(); // Proved never equal
- break;
- case PhiNode phi:
- // Assume related
- addDep(phi);
- break outer;
- case ConstantNode con:
- // Load from constant memory
- if( con._con instanceof TypeMem tmem )
- throw Utils.TODO("need to see a test case");
- // return ConstantNode.make(tmem._t);
- break outer; // Assume shortly dead
- case ProjNode mproj: // Memory projection
- switch( mproj.in(0) ) {
- case NewNode nnn1:
- if( external ) break outer; // Class allocation does not initialize C storage
- // Direct load from fresh zero/default-filled allocation.
- Type decl = declaredType();
- assert decl!=Type.BOTTOM;
- Type zero = decl.makeZero();
- assert zero.isa(_type); // Catch bug uninitialized null field
- return ConstantNode.make(zero);
- case CallEndNode cend: addDep(mproj); break outer; // TODO: Bypass no-alias call
- default: throw Utils.TODO("Should not reach here");
- }
- case MemMergeNode merge: mem = merge.alias(_alias); break;
- case EscapeNode esc:
- if( esc.self()==ptr ) // Proved equal
- { mem = esc.priv(); break; }
- // Mal-formed, dying
- if( !(esc.self().in(0) instanceof NewNode) )
- break outer;
- // Two NewNodes are always unequal
- if( ptr.in(0) instanceof NewNode )
- { mem = esc.pub(); break; }
- // TODO: Can we prove unequal?
- break outer;
-
- default:
- throw Utils.TODO();
- }
- }
+ Node win = find(mem,ptr,null,null);
+ if( win!=null ) return folded(win);
+ if( ptr instanceof ReadOnlyNode ro ) ptr=ro.in(1);
// Push a Load up through a Phi, as long as it collapses on at least
// one arm. If at a Loop, the backedge MUST collapse - else we risk
@@ -213,34 +161,119 @@ public class LoadNode extends MemOpNode {
// if( pred ) ptr.x = e0; val = pred ? e0
// else ptr.x = e1; : e1;
// val = ptr.x; ptr.x = val;
- if( mem instanceof PhiNode memphi && memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 &&
+ if( mem() instanceof MemPhiNode memphi && memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 &&
+ // Array access control must not be moved across the merge.
+ in(0)==null &&
// Offset can be hoisted
off() instanceof ConstantNode &&
- // Not control dependent
- in(0)==null &&
// Pointer can be hoisted
hoistPtr(ptr,memphi) ) {
- // Profit on RHS/Loop backedge
- if( profit(memphi,2) ||
- // Else must not be a loop to count profit on LHS.
- (!(memphi.region() instanceof LoopNode) && profit(memphi,1)) ) {
- Node ld1 = ld(1);
- Node ld2 = ld(2);
- PhiNode phi = new PhiNode(_name, memphi.region(),ld1,ld2);
- phi.setType(phi.compute().join(_type));
- return phi;
+ // Returning to this memory means no change only if the pointer
+ // is also loop invariant. A pointer Phi may select another object.
+ Node stop = memphi.region() instanceof LoopNode &&
+ !(ptr instanceof PhiNode p && p.region()==memphi.region()) ? memphi : null;
+ if( find(memphi.in(2),ptr,stop,new BitSet())!=null ||
+ (!(memphi.region() instanceof LoopNode) && find(memphi.in(1),ptr,null,new BitSet())!=null) ) {
+ // Integer loop Phis widen to i64. Keep the load's declared
+ // width at the result, as for ordinary typed loop variables.
+ if( _type instanceof TypeInteger && !declaredType().isa(_type) ) return null;
+ PhiNode phi = new PhiNode(_name,memphi.region()).keep();
+ phi.setType(_type instanceof TypeInteger ? TypeInteger.BOT : _type);
+ for( int i=1; i<memphi.nIns(); i++ ) {
+ Node p = ptr instanceof PhiNode pp && pp.region()==memphi.region() ? pp.in(i) : ptr;
+ phi.addDef(load(memphi.in(i),p,stop,phi));
+ }
+ CodeGen.CODE.add(phi.unkeep());
+ return _type instanceof TypeInteger ? new ConvertNode(declaredType(),phi) : phi;
}
}
return null;
}
- private Node ld( int idx ) {
- Node mem = mem(), ptr = ptr();
- assert in(0)==null;
- assert !(ptr instanceof PhiNode && ptr.in(0)==mem.in(0)); // If fails, need to use pre-merged ptr at same phi
- return new LoadNode(_loc,_name,_alias,null,mem.in(idx),ptr, off()).peephole();
+ // Load a flavored zero from a New
+ private Node zero(NewNode nnn) {
+ Type zero = declaredType().makeZero();
+ assert zero.isa(_type); // Catch an uninitialized non-null field
+ return castRO(new ConstantNode(zero).peephole());
+ }
+
+ // Search only; no new nodes or rewiring. Return a Store/New that folds,
+ // the unchanged loop memory, or a merge whose every arm folds. Null fails.
+ // Register dependencies so a later pointer/offset rewrite retries the query.
+ private Node find(Node mem, Node ptr, Node stop, BitSet visit) {
+ if( ptr instanceof ReadOnlyNode ro ) ptr=ro.in(1);
+ Field fld;
+ boolean external = ptr._type instanceof TypeMemPtr tmp &&
+ (fld=tmp._obj.field(_name))!=null && fld._extern;
+ while( mem!=null ) {
+ addDep(mem);
+ if( mem==stop ) return mem;
+ switch( mem ) {
+ case MemMergeNode merge: mem=merge.alias(_alias); break;
+ case StoreNode st:
+ // Alias 1 is still unresolved, not a proven different field.
+ if( st._alias==1 ) return null;
+ addDep(st.ptr());
+ if( _alias==st._alias && (ptr==st.ptr() || ptr==st.nnptr()) && off()==st.off() ) {
+ // A full-width load cannot narrow the forwarded value.
+ if( declaredType() instanceof TypeInteger ti && ti.log_size()==3 &&
+ !addDep(st.val())._type.isa(_type) ) return null;
+ return st;
+ }
+ if( (external || _alias==st._alias) && (external || !neverAlias(ptr,st.ptr())) &&
+ !addDep(off())._type.join(addDep(st.off())._type).isHigh() ) return null;
+ mem=st.mem();
+ break;
+ case ProjNode proj:
+ // New's memory is private; public memory is joined at Escape.
+ if( external || !(proj.in(0) instanceof NewNode nnn) ) return null;
+ if( declaredType()==Type.BOTTOM ) return null;
+ return nnn;
+ case EscapeNode esc:
+ if( external ) return null;
+ addDep(esc.self());
+ if( esc.self()==ptr ) { mem=esc.priv(); break; }
+ if( neverAlias(ptr,esc.self()) ) { mem=esc.pub(); break; }
+ return null;
+ case MemPhiNode phi:
+ addDep(phi.region());
+ // Only the original loop closes a successful cycle. Another
+ // loop or an unfinished merge needs a separate proof.
+ if( visit==null || !(phi.region() instanceof RegionNode r) || r instanceof LoopNode ||
+ r.inProgress() || phi.inProgress() || r._type!=Type.CONTROL ) return null;
+ if( visit.get(phi._nid) ) return phi; // Already proved every arm.
+ for( int i=1; i<phi.nIns(); i++ )
+ if( find(phi.in(i),ptr,stop,visit)==null ) return null;
+ visit.set(phi._nid);
+ return phi;
+ default: return null;
+ }
+ }
+ return null;
+ }
+
+ private Node folded(Node win) {
+ return win instanceof StoreNode st ? extend(castRO(st.val())) : zero((NewNode)win);
+ }
+
+ // Materialize only after the profitability search succeeds. The value Phi
+ // stands for a load which comes back around the loop with unchanged memory.
+ private Node load(Node mem, Node ptr, Node stop, PhiNode value) {
+ Node win=find(mem,ptr,stop,new BitSet());
+ if( win!=null ) {
+ if( win==stop ) return value;
+ if( win instanceof MemPhiNode mp ) {
+ PhiNode phi=new PhiNode(_name,mp.region()).keep();
+ for( int i=1; i<mp.nIns(); i++ ) phi.addDef(load(mp.in(i),ptr,stop,value));
+ return CodeGen.CODE.add(phi.unkeep().peephole());
+ }
+ Node fold=folded(win);
+ if( fold!=null ) return fold.peephole();
+ }
+ LoadNode ld=new LoadNode(_loc,_name,_alias,in(0),mem,ptr,off());
+ return ld.peephole();
}
private static boolean neverAlias( Node ptr1, Node ptr2 ) {
@@ -269,21 +302,6 @@ public class LoadNode extends MemOpNode {
return false;
}
- // Profitable if we find a matching Store on this Phi arm.
- private boolean profit(PhiNode memphi, int idx) {
- Node px = memphi.in(idx);
- if( px==null ) return false;
- // Memory is collapsing, no bother
- if( px._type instanceof TypeMem mem && mem._t.isHighOrConst() )
- return false;
- if( px instanceof StoreNode st1 && ptr()==addDep(st1.nnptr() ) && off()==st1.off() && addDep(st1.val())._type.isa(_type) )
- // To avoid cyclic pushing a Load up then down, getting here means
- // the load *must* match against the Store
- return true;
- addDep(px);
- return false;
- }
-
// Read-Only is a deep property, and cannot be cast-away
private Node castRO(Node rez) {
if( ptr()._type.isFinal() && !rez._type.isFinal() )
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
index 441edc3e..de7adede 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
@@ -8,6 +8,7 @@ import com.seaofnodes.simple.util.BAOS;
import com.seaofnodes.simple.util.Utils;
import java.lang.StringBuilder;
import java.util.BitSet;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
@@ -101,6 +102,13 @@ public abstract class MemOpNode extends TypeNode {
public StringBuilder _printMach( StringBuilder sb, BitSet visited ) { throw Utils.TODO(); }
+ // Extra conditions for factoring matching memory operations through a Phi.
+ // The caller has already checked the opcode, input shape and control.
+ boolean canDrop(MemOpNode other, Node dep) {
+ dep.addDep(ptr());
+ return _alias>1 && _alias==other._alias && declaredType()==other.declaredType() && err()==null;
+ }
+
@Override
public boolean eq(Node n) {
MemOpNode mem = (MemOpNode)n; // Invariant
@@ -128,4 +136,30 @@ public abstract class MemOpNode extends TypeNode {
return null;
}
+
+ // Memory writers which must follow this read. A packaging node is not a
+ // write; follow this alias through aggregates to its writers.
+ public ArrayList<Node> antiDeps() {
+ ArrayList<Node> deps = new ArrayList<>();
+ antiDeps(mem(),deps,new BitSet());
+ return deps;
+ }
+
+ private void antiDeps(Node mem, ArrayList<Node> deps, BitSet visit) {
+ if( visit.get(mem._nid) ) return;
+ visit.set(mem._nid);
+ for( Node use : mem._outputs ) {
+ if( use instanceof MemMergeNode merge ) {
+ Node slice = _alias<merge.nIns() ? merge.in(_alias) : null;
+ if( (slice==mem) || (slice==null && merge.in(1)==mem) )
+ antiDeps(merge,deps,visit);
+ } else if( use instanceof CallNode ||
+ (use instanceof MemOpNode st && !st._isLoad && st._alias==_alias) ||
+ (use instanceof EscapeNode esc && esc.fld()._alias==_alias) ||
+ (use instanceof MemPhiNode phi && phi._alias==_alias) ||
+ (use instanceof BulkMemPhiNode phi && !phi.isSplit(_alias)) ) {
+ if( !deps.contains(use) ) deps.add(use);
+ }
+ }
+ }
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java
index 59c9f8e3..8842db78 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/Node.java
@@ -306,6 +306,7 @@ public abstract class Node implements Cloneable {
// Error is 'use' does not exist; ok for 'use' to be null.
public boolean delUse( Node use ) {
_outputs.del(_outputs.find(use));
+ moveDepsToWorklist(); // User-count and anti-dependence queries can now change.
return _outputs.isEmpty();
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/PhiNode.java
index a4c73472..9d8adb15 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/PhiNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/PhiNode.java
@@ -154,67 +154,77 @@ public class PhiNode extends Node {
// Same op on all Phi paths; all ops have only the Phi as a use.
// None have a control input.
private boolean same_op() {
- Node busy=null;
- if( in(1) instanceof ConstantNode )
- return false;
- for( int i=1; i<nIns(); i++ ) {
- Node op = in(i);
- if( in(1).getClass() != op.getClass() || op.in(0)!=null || in(1).nIns() != op.nIns() )
- return false; // Wrong class or CFG bound or mismatched inputs
- if( in(1) instanceof MemOpNode mem ) {
- // Mismatched aliases
- if( mem._alias != ((MemOpNode)op)._alias ) return false;
- // Load is clobbered somewhere, and can not be pulled forward past the Phi?
- if( mem instanceof LoadNode )
- for( Node use : op.outs() )
- if( use instanceof StoreNode )
- return false;
- }
- if( in(1) instanceof EscapeNode )
- return false;
- if( in(1) instanceof MemMergeNode )
- return false; // Have to keep aliases straight
- if( op.nOuts() > 1 ) {
- if( busy==null ) busy = op;
- else { // Too many users, but addDep in case lose users
- for( Node out : op._outputs )
- if( out!=null && out!=this )
- addDep(out);
- for( Node out : busy._outputs )
- if( out!=null && out!=this )
- addDep(out);
+ Node op = in(1);
+ if( op instanceof EscapeNode || op instanceof CFGNode || op instanceof ConstantNode || op instanceof PhiNode ||
+ op instanceof ProjNode || op instanceof NewNode || op instanceof ScopeNode || op instanceof MemMergeNode ) return false;
+ // A bulk Phi must split its aliases before factoring precise Stores.
+ if( this instanceof BulkMemPhiNode ) return false;
+ // Bulk splitting currently identifies parallel slices by Region/alias.
+ // Factoring can introduce another slice at a different memory point;
+ // wait until that Region's bulk partitioning has finished.
+ if( op instanceof MemOpNode )
+ for( Node use : region()._outputs )
+ if( use instanceof BulkMemPhiNode ) {
+ addDepForwards(use);
return false;
}
+ Node busy = null;
+ for( int i=1; i<nIns(); i++ ) {
+ Node n = in(i);
+ if( addDep(region().in(i))._type==Type.XCONTROL ||
+ op.getClass()!=n.getClass() || n.nIns()!=op.nIns() || n.in(0)!=null || !op.eq(n) ) return false;
+ if( n instanceof MemOpNode mem && !mem.canDrop((MemOpNode)op,this) ) return false;
+ // Moving a Store must remove the old effect, not duplicate it.
+ // Allow one shared arm: the other arms disappear into the single
+ // factored operation, usually reducing the total operation count.
+ addDepForwards(n);
+ if( n.nOuts()>1 ) {
+ for( Node use : n._outputs )
+ if( use!=null && use!=this ) addDepForwards(use);
+ if( n instanceof StoreNode || busy!=null ) return false;
+ busy=n;
}
+ for( int j=1; j<n.nIns(); j++ )
+ if( n.in(j) instanceof ScopeNode || ((n.in(j)==null) != (op.in(j)==null)) ) return false;
}
return true;
}
private Node drop_same_op() {
- assert !(in(1) instanceof CFGNode);
- Node op = in(1);
- Node cp = op.copy();
- cp._type = null; // Fresh type
- cp.addDef(null); // No control
-
+ Node op = in(1), cp = op.copyEmpty();
+ cp._type = null;
+ cp.addDef(null);
for( int j=1; j<op.nIns(); j++ ) {
- boolean needsPhi = false;
- Node x = op.in(j); // Jth input from sample #1
+ Node x = op.in(j);
+ boolean different = false;
for( int i=2; i<nIns(); i++ )
- if( in(i).in(j) != x )
- { needsPhi=true; break; }
- if( needsPhi ) {
- x = make(_label,op.in(j)._type);
- x.addDef(region());
- for( int i=1; i<nIns(); i++ )
- x.addDef(in(i).in(j));
- x = x.peephole();
+ if( in(i).in(j)!=x ) different=true;
+ if( different ) {
+ Node[] ins = new Node[nIns()];
+ ins[0] = region();
+ Type t = Type.TOP;
+ for( int i=1; i<nIns(); i++ ) {
+ ins[i] = in(i).in(j);
+ t = t.meet(ins[i]._type);
+ }
+ // Do not introduce an operand Phi mixing incompatible type families.
+ if( t==Type.BOTTOM ) {
+ for( int i=1; i<ins.length; i++ ) addDep(ins[i]);
+ cp.kill();
+ return null;
+ }
+ PhiNode phi = j==1 && op instanceof MemOpNode mem
+ ? new MemPhiNode(_label,mem._alias,ins)
+ : PhiNode.make(_label,t,ins);
+ x = phi.peephole();
}
cp.addDef(x);
}
- // Test not running backwards, which can happen for e.g. And's
- if( cp.compute().isa(compute()) )
- return cp;
+ // Factoring must not widen the result (e.g. correlated And operands).
+ if( cp.compute().isa(compute()) ) return cp;
+ for( int i=1; i<nIns(); i++ )
+ for( int j=1; j<in(i).nIns(); j++ )
+ if( in(i).in(j)!=null ) addDep(in(i).in(j));
cp.kill();
return null;
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/RegionNode.java
index 32434850..a886183e 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/RegionNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/RegionNode.java
@@ -48,7 +48,7 @@ public class RegionNode extends CFGNode {
if( !hasPhi() && nIns()>3 && // No Phi users, just a control user
in(1) instanceof CProjNode p1 &&
in(2) instanceof CProjNode p2 &&
- addDep(p1.in(0))==addDep(p2.in(0)) &&
+ addDepForwards(p1).in(0)==addDepForwards(p2).in(0) &&
p1.in(0) instanceof IfNode iff ) {
// Replace with the iff.ctrl directly
if( nIns()==3 ) return iff.ctrl();
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/StoreNode.java
index 2683ddf5..fe379c80 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/StoreNode.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/StoreNode.java
@@ -49,6 +49,10 @@ public class StoreNode extends MemOpNode {
baos.write(_size);
}
+ @Override boolean canDrop(MemOpNode other, Node dep) {
+ return super.canDrop(other,dep) && _init==((StoreNode)other)._init;
+ }
+
// Debugger label
@Override public String label() { return "st_"+mlabel(); }
@Override public boolean isMem() { return true; }
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java
index c55002c1..aca0eea1 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java
@@ -24,6 +24,11 @@ public class LoadX86 extends MemOpX86 {
}
static void enc( Encoding enc, Type decl, short dst, short ptr, short idx, int off, int scale ) {
+ // Allocation may put a full-width integer in an XMM register (or a
+ // floating value in a GPR). Move its bits using the selected register bank.
+ if( dst>=x86_64_v2.XMM_OFFSET && decl.log_size()==3 ) decl=TypeFloat.F64;
+ else if( dst<x86_64_v2.XMM_OFFSET && decl instanceof TypeFloat )
+ decl = decl==TypeFloat.F32 ? TypeInteger.U32 : TypeInteger.BOT;
if( decl == TypeFloat.F32) enc.add1(0xF3);
if( decl == TypeFloat.F64) enc.add1(0xF2);
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter11Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter11Test.java
index 2e661901..d515df8d 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter11Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter11Test.java
@@ -152,6 +152,8 @@ return v.f;
""");
code.parse().opto();
assertEquals("return Phi(Loop,0,2);", code.print());
+ for( int arg : new int[]{-1,0,1,3,100} )
+ assertEquals("0",Eval2.eval(code,arg));
}
@Test
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java
index ce604cf1..e39c09e7 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter20Test.java
@@ -183,7 +183,7 @@ for( int i=0; i<ary#-1; i++ )
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"riscv" , "SystemV", 9,"return (add,.[],(mul,.[],1000));");
testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index b88e8af9..08da0654 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -279,7 +279,7 @@ for( int i=0; i<ary#-1; i++ )
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"riscv" , "SystemV", 9,"return (add,.[],(mul,.[],1000));");
testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
}
@@ -500,7 +500,7 @@ val sieve = { int N ->
// Evaluate on ARM5 emulator; expect return of an array of primes in
// the simulated heap.
- EvalArm64 A5 = TestArm64.build("sieve", src, 100, 105, false);
+ EvalArm64 A5 = TestArm64.build("sieve", src, 100, 88, false);
int trap_arm = A5.step(10000);
assertEquals(0, trap_arm);
int ary_arm = (int)A5.regs[arm.X0];
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java b/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java
index 86fcf96f..1503adb5 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/EvalArm64.java
@@ -544,10 +544,13 @@ public class EvalArm64 {
break;
}
- case 0xD2: {
+ case 0xD2: // MOVZ
+ case 0xF2: { // MOVK preserves the other halfwords
// movz
// get immediate
- rval = (ir >> 5) & 0x7FFF;
+ int shift = ((ir >> 21) & 3)*16;
+ rval = ((ir >> 5) & 0xFFFFL) << shift;
+ if( opcode1==0xF2 ) rval |= regs[rdid] & ~(0xFFFFL << shift);
break;
}
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java b/chapter25/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
new file mode 100644
index 00000000..2786b403
--- /dev/null
+++ b/chapter25/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java
@@ -0,0 +1,104 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.*;
+import java.util.BitSet;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// Execution regressions are separate from the fixed spill-measurement cohorts.
+public class MemoryEncodingTest {
+ @Test public void testLoopLoadSearch() {
+ String[] sources = {
+ """
+ u8[] !program=new u8[106]; u8[] !out=new u8[0];
+ int sum=0;
+ for(int i=0; i<arg; i++) {
+ sum+=program#;
+ if(i==1) out=new u8[i+1];
+ else if(i==3) out=new u8[2];
+ }
+ return sum+out#;
+ """,
+ // One possibly aliasing arm must reject the unchanged-memory proof.
+ """
+ struct S { int x; }; S !a=new S; S !b=new S;
+ a.x=10; b.x=20; S !p=a; if(arg&1) p=b;
+ int sum=0;
+ for(int i=0; i<6; i++) {
+ if(i==2) p.x=30;
+ else if(i==4) b.x=40;
+ sum+=a.x;
+ }
+ return sum;
+ """,
+ // Unchanged memory does not imply an unchanged loop-carried pointer.
+ """
+ struct S { int x; }; S !a=new S; S !b=new S;
+ a.x=10; b.x=20; S !p=a; int sum=0;
+ for(int i=0; i<6; i++) {
+ if(i==2) a.x=30;
+ sum+=p.x;
+ p=b; if(i&1) p=a;
+ }
+ return sum;
+ """,
+ // All arms fold, but their values differ: build a value Phi.
+ """
+ struct S { int x; }; S !a=new S; a.x=1; int sum=0;
+ for(int i=0; i<6; i++) {
+ sum+=a.x;
+ if(i&1) a.x=2;
+ else if(i==2) a.x=3;
+ else a.x=4;
+ }
+ return sum;
+ """
+ };
+ // Exercise both unresolved Store aliases and nested value-Phi cleanup.
+ for( long seed : new long[]{0,9,126} )
+ for( int p=0; p<sources.length; p++ ) {
+ CodeGen code=new CodeGen(sources[p],seed,true).parse().opto();
+ if( p==0 ) assertNull(code._stop.walk(n ->
+ n instanceof LoadNode ld && ld.ptr() instanceof ProjNode ptr &&
+ ptr.in(0) instanceof NewNode nn &&
+ nn.size()._type==com.seaofnodes.simple.type.TypeInteger.constant(110) ? ld : null));
+ for( int arg=0; arg<7; arg++ ) {
+ long expected=switch(p) {
+ case 0 -> 106*arg+(arg>=2 ? 2 : 0);
+ case 1 -> (arg&1)==0 ? 140 : 60;
+ case 2 -> 130;
+ default -> 16;
+ };
+ assertEquals(Long.toString(expected),Eval2.eval(code,arg));
+ }
+ }
+ }
+
+ @Test public void testKeepReadsBeforeWrites() {
+ var code = new CodeGen("""
+ struct S { int x; int y; };
+ S !a = new S; S !b = new S;
+ a.x=arg; b.x=arg+1;
+ S !p=a; S !q=b;
+ if (arg<0) { p=b; q=a; }
+ int v;
+ if (arg>1) { v=p.x; p.x=41; p.y=5; }
+ else { v=q.x; q.x=42; }
+ return v;
+ """).parse().opto();
+ StopNode stop = code._stop;
+ assertEquals(2,countMemoryNodes(stop,LoadNode.class,new BitSet()));
+ assertEquals("-1",Eval2.eval(code,-1));
+ assertEquals("1",Eval2.eval(code,0));
+ assertEquals("3",Eval2.eval(code,3));
+ }
+
+ private static int countMemoryNodes(Node n, Class<?> kind, BitSet seen) {
+ if (n==null || seen.get(n._nid)) return 0;
+ seen.set(n._nid);
+ int cnt=kind.isInstance(n) ? 1 : 0;
+ for (Node def : n._inputs) cnt+=countMemoryNodes(def,kind,seen);
+ return cnt;
+ }
+}