From c1d8d90f5246a41e42189ffbfcde017873bf5b72 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Sun, 20 Sep 2026 16:23:20 -0700 Subject: Forward-port allocator bug fixes and test support through ch25 Improved spilling --- chapter22/Makefile | 6 + chapter22/README.md | 105 +++++--- .../com/seaofnodes/simple/codegen/BuildLRG.java | 4 +- .../java/com/seaofnodes/simple/codegen/IFG.java | 29 +- .../com/seaofnodes/simple/codegen/RegAlloc.java | 133 +++------ .../com/seaofnodes/simple/codegen/RegMask.java | 18 +- .../simple/node/cpus/riscv/StoreRISC.java | 4 +- .../simple/node/cpus/x86_64_v2/MemOpX86.java | 2 +- .../java/com/seaofnodes/simple/BrainFuckTest.java | 2 +- .../java/com/seaofnodes/simple/Chapter20Test.java | 296 +++++++++++++++++---- .../com/seaofnodes/simple/Chapter21AllocTest.java | 55 ++++ .../java/com/seaofnodes/simple/Chapter21Test.java | 78 ++++-- .../java/com/seaofnodes/simple/Chapter22Test.java | 16 +- .../java/com/seaofnodes/simple/MergeSortTest.java | 2 +- .../java/com/seaofnodes/simple/SpillStats.java | 71 +++++ .../test/java/com/seaofnodes/simple/TestArm64.java | 9 +- .../src/test/java/com/seaofnodes/simple/TestC.java | 14 +- .../test/java/com/seaofnodes/simple/TestRisc5.java | 9 +- .../simple/codegen/RegAllocTestSupport.java | 208 +++++++++++++++ .../java/com/seaofnodes/simple/progs/person21.c | 34 +++ .../java/com/seaofnodes/simple/progs/person21.smp | 9 + 21 files changed, 848 insertions(+), 256 deletions(-) create mode 100644 chapter22/src/test/java/com/seaofnodes/simple/Chapter21AllocTest.java create mode 100644 chapter22/src/test/java/com/seaofnodes/simple/SpillStats.java create mode 100644 chapter22/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java create mode 100644 chapter22/src/test/java/com/seaofnodes/simple/progs/person21.c create mode 100644 chapter22/src/test/java/com/seaofnodes/simple/progs/person21.smp (limited to 'chapter22') diff --git a/chapter22/Makefile b/chapter22/Makefile index 55d5e70a..33855ddf 100644 --- a/chapter22/Makefile +++ b/chapter22/Makefile @@ -138,6 +138,12 @@ tests: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag @$(JVM) org.junit.runner.JUnitCore $(test_cp) @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap +spill-stats: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @[ -d build/objs ] || mkdir -p build/objs + @$(JVM) com.seaofnodes.simple.SpillStats + +.PHONY: spill-stats + fuzzer: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag @echo "fuzzing " $(test_cp) @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap diff --git a/chapter22/README.md b/chapter22/README.md index f31dcf19..bcb286d0 100644 --- a/chapter22/README.md +++ b/chapter22/README.md @@ -41,7 +41,7 @@ semantics - just the ability to nest type definitions. struct Outer { struct Inner { int in; - int pi = 3.14; + flt pi = 3.14; }; int out; }; @@ -53,6 +53,7 @@ need to be stored in each instances. Instead one copy is kept in a global space (not really a *class* object yet) and loaded from there. In this example: + - Instances of the `Outer` struct have a single `int` field `out`. - Instances of the `Inner` struct have a single `int` field `in`. - There exists a `Outer.Inner.pi` field in a global space with value `3.14`. @@ -68,7 +69,7 @@ We can now call external / FFI functions, and they are declared like any other variable except being assigned "C". Here is the binding for the libc `write` call: -`{ i32 fd, i32 buf, i32 len -> i32 } write = "C";` +`{ i32 fd, i64 buf, u32 len -> u32 } write = "C";` Any final variable assigned as "C" will be considered defined externally. The syntax of this might change slightly to avoid ambiguity with assign the same @@ -82,7 +83,8 @@ name `write` has to be unique when linking. The `sys` struct includes `libc` bindings, and any default library code including e.g. printing support and collections. As of this chapter it -includes minimal libc bindings and an easy print: +includes minimal libc bindings and an easy print. This excerpt shows the +parts used above: ```java // top-level default import @@ -91,7 +93,7 @@ struct sys { /** https://www.man7.org/linux/man-pages/man2/.2.html */ struct libc { // fd buf len -> len - { i32 i64 i32 -> i32 } write = "C"; + { i32 i64 u32 -> u32 } write = "C"; // addr len prot flags fd off -> void* { i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; // mmap flags @@ -105,8 +107,7 @@ struct sys { struct io { val p = { u8[~] str -> i64 ptr = str; // cast array base to i64 - i32 len = str#; // cast length to signed - return sys.libc.write(1,ptr,len); + return sys.libc.write(1,ptr,str#); }; }; }; @@ -166,29 +167,69 @@ The form ``` `0` ``` makes a `u8` value with the ASCII character `0`. ## Continuous Improvement -In the rush to get executable code from -[Chapters 19](https://github.com/SeaOfNodes/Simple/tree/linear-chapter19), -[Chapters 20](https://github.com/SeaOfNodes/Simple/tree/linear-chapter20), and -[Chapters 21](https://github.com/SeaOfNodes/Simple/tree/linear-chapter21), we -wrote a lot of code in a hurry. That also means we wrote a lot of bugs (sad -face), and will be paying the price for a few chapters. In this chapter we -also found and fixed a number of bugs, here's a sample: - -- x86 encoding bugs: -- - `imul` with immediate forms and `setXX` had incorrect `REX` forms. -- - x86 `div` was taking `RAX,RDX` as the second *input* register; they are already - hardwired as the *first* input register. -- - Many x86 ops that kill `flags` did not state so in the port. -- - Short forms for some x86 ops (e.g. using the `inc` vs `add+1`) -- Global Code Motion `ScheduleLate` missed visiting values only visible around - loop backedges; also was adding extra dependence edges between loads and - stores with different aliasing. -- Inlining would reset the **idepth** (as expected) but then use the existing - `CallNode` and `FunNode` that are *folding* but not yet peep'd away - leading - to incorrect **idepth** calculations. - -Many more bugs got fixed, but this sample should give an idea of the continuous -improvements going on. These happen because we have finally reached a point -where we can aggressive test again - we are writing Real (Simple) Code (tm) and -running it... and crashing on these bugs. Compilers are big, complex, beasties -and some amount of bugs and bug fixing is to be expected. +Executing larger programs exposes mistakes that graph-only tests can miss. +During the original development of this chapter, these included x86 instruction +encodings and flag clobbers, scheduling around loop backedges and memory stores, +and stale dominator depths during inlining. Corrections are being moved into the +earliest applicable chapters so readers can build on working compiler snapshots. +Native tests check both output and process exit status; emulator tests check the +returned values and memory as well as successful execution. + +## RegAlloc improvements: better color bias + +Chapter 21 introduced conservative copy coalescing. Here we improve the register +preferences used when related live ranges cannot coalesce. A copy chain can take +an already assigned register even at its terminal definition; a loop Phi prefers +its backedge's register, avoiding a move on each iteration when possible. + +When no live range is trivially colorable, cheap values make better spill +candidates. We favor a cloneable definition with distant or multiple uses over +one already adjacent to its only use, then callee-save values whose long spans +are cheap to split. Register order breaks ties between callee saves. These are +small preferences, not a general cost model. Grouping popular values by register +class is left for Chapter 23, cold loop splits for 24, and area/cost ranking for 25. + +Run `make spill-stats` in this directory. Each row below uses **this chapter's +compiler**, default worklist seed 123, and the same source/target combinations +as the earlier cohort. The Windows run combines x86 SystemV/Win64 and RISC-V/ARM +SystemV. Diagnostic graphs and mask regressions do not contribute to the totals. + +| Program cohort | Compilations | Split/move count | Loop-weighted count | +|---|---:|---:|---:| +| Chapter 20 | 39 | 324 | 443 | +| Chapter 21 | 52 | 447 | 986 | +| Chapter 22 | 24 | 67 | 67 | +| **Total** | **115** | **838** | **1,496** | + +`_spills` counts retained SplitNodes, including register moves; `_spillScaled` +weights each by `8^loopDepth`. These are compiler estimates, not measured runtime +memory traffic. The reporter also prints individual allocations and CPU/ABI sums. +Chapter 21's original `int age` person example is preserved as `person21`; this +chapter's `i32 age` version and revised infinite-loop example belong to cohort 22. + +For a controlled comparison, the same compiler and correctness fixes with +Chapter 21's color preferences and spill ordering produce 885 moves and 1,543 +weighted moves. This chapter saves **47 weighted moves (3.0%)**. RISC-V BrainFuck +improves from 42 to 28 and ARM from 34 to 28 in each of its two cohorts. Some +MergeSort cases and RISC-V Sieve each cost one more move. Summing the whole suite +shows whether those local tradeoffs pay off. + +The original Chapter 22 snapshot, which mixed several later heuristics, produced +854 moves and 1,736 weighted moves on these same 115 compilations. The corrected, +staged version saves 240 weighted moves (13.8%). Neither comparison should be +confused with comparing entire compiler chapters: lowering also changes. For +example, the frozen Chapter 20 String input has no explicit return, and Chapter +22's default-return handling eliminates that workload at the default seed. + +The full suite passes 416 tests plus the fuzzer; the statistics runner passes +55 selected tests. Routine backend comparisons use a fixed optimizer seed: +shuffling optimizer worklists should normalize to essentially the same graph, +so repeating allocation on those graphs adds little coverage. Use seed variation +when investigating optimizer normalization or worklist-order failures. + +Two pre-existing failures found during the review remain in the +[backport queue](../docs/chapter-backports.md): String fails during loop +analysis/scheduling at other optimizer seeds, and returning a function pointer +fails during relocation after Opto incorrectly deletes the referenced function. +Their reproductions and validation history are retained +there as separate correctness work. diff --git a/chapter22/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java b/chapter22/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java index e9b5eb2e..f4ce4366 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java @@ -40,8 +40,8 @@ abstract public class BuildLRG { int uidx = n.out(0)._inputs.find(n); RegMask mask1 = n.in (1) instanceof MachNode machx ? machx.outregmap() : alloc.lrg(n.in(1))._mask; RegMask mask2 = n.in (2) instanceof MachNode machx ? machx.outregmap() : alloc.lrg(n.in(2))._mask; - RegMask masko = n.out(0) instanceof MachNode machx ? machx.regmap(uidx): alloc.lrg(n )._mask; - if( !mask1.overlap(masko) && mask2.overlap(masko) ) + RegMask masko = n.out(0) instanceof MachNode machx ? machx.regmap(uidx): mach.outregmap(); + if( masko!=null && !mask1.overlap(masko) && mask2.overlap(masko) ) n.swap12(); } diff --git a/chapter22/src/main/java/com/seaofnodes/simple/codegen/IFG.java b/chapter22/src/main/java/com/seaofnodes/simple/codegen/IFG.java index fdd4e2f9..1268db6a 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/codegen/IFG.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/codegen/IFG.java @@ -207,20 +207,7 @@ abstract public class IFG { assert tlrg.leader(); // Always, tlrg cannot use kills if( tlrg._mask.overlap(killMask) ) { - // Disallow clone-ables from killing registers. Just fail - // them and re-clone closer to target... so no kill. - // Special case for Intel XOR used to zero. - Node n = (Node)m; - CFGNode effUseBlk = n.out(0) instanceof PhiNode phi ? phi.region().cfg(phi._inputs.find(n)) : n.out(0).cfg0(); - if( m.isClone() && // Must be clonable - (n.nOuts()>1 || // Has many users OR - // Only 1 user but effective use is remote block - effUseBlk != n.cfg0() )) - // Then fail the clonable; it should split or move - alloc.fail(alloc.lrg((Node)m)); - // Else clonable cannot move - else if( !tlrg.sub(killMask) ) - alloc.fail(tlrg); + if( !tlrg.sub(killMask) ) alloc.fail(tlrg); } } } @@ -438,13 +425,13 @@ abstract public class IFG { private static int pickRiskyScore( LRG lrg ) { // Pick single-def clonables that are not right next to their single-use. // Failing to color these will clone them closer to their uses. - if( !lrg._multiDef && lrg._machDef.isClone() ) { - Node def = ((Node)lrg._machDef); - Node use = ((Node)lrg._machUse); + if( !lrg._multiDef && lrg._machDef!=null && lrg._machUse!=null && lrg._machDef.isClone() ) { + Node def = (Node)lrg._machDef; + Node use = (Node)lrg._machUse; CFGNode cfg = def.cfg0(); - if( cfg != use.cfg0() || // Different blocks OR + if( lrg._multiUse || cfg != use.cfg0() || // Many uses or different blocks OR // Same block, but not close - cfg._outputs.find(def) < cfg._outputs.find(use)+1 ) + cfg._outputs.find(def)+1 < cfg._outputs.find(use) ) return 1000000; } @@ -452,7 +439,7 @@ abstract public class IFG { // and very cheap to spill. if( lrg._machDef instanceof CalleeSaveNode ) return 1000000-2-lrg._mask.firstReg(); - if( lrg._splitDef != null && lrg._splitDef. in(1) instanceof CalleeSaveNode && + if( lrg._splitDef != null && lrg._splitDef.in(1) instanceof CalleeSaveNode && lrg._splitUse != null && lrg._splitUse.out(0) instanceof ReturnNode ) return 1000000-1; @@ -506,7 +493,7 @@ abstract public class IFG { private static int biasable(Node split) { if( split instanceof SplitNode ) return 1; // Yes biasable, advance is slot 1 - if( split instanceof PhiNode phi ) return phi.region() instanceof LoopNode ? 2 : 1; // Yes biasable, advance is slot 1 + if( split instanceof PhiNode phi ) return phi.region() instanceof LoopNode ? 2 : 1; // Prefer the backedge in loops if( !(split instanceof MachNode mach) ) return 0; // Not biasable return mach.twoAddress(); // Only biasable if 2-addr } diff --git a/chapter22/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/chapter22/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java index 835ca2a5..c0be96f1 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java @@ -176,17 +176,35 @@ public class RegAlloc { if( n instanceof NewNode nnn ) nnn.cacheRegs(_code); } + // Optional diagnostic for values accidentally shared across functions. + //assert verifyFunctionLocalEdges(); + // Top driver: repeated rounds of coloring and splitting. byte round=0; while( !graphColor(round) ) { split(round); if( round >= 7 ) // Really expect to be done soon - throw Utils.TODO("Allocator taking too long"); + throw new IllegalStateException("Register allocation made no progress after eight rounds"); round++; } postColor(); // Remove no-op spills } + boolean verifyFunctionLocalEdges() { + for( CFGNode bb : _code._cfg ) { + if( bb instanceof StartNode ) continue; + FunNode fun = bb.fun(); + for( Node use : bb._outputs ) + if( use instanceof MachNode mach && !(use instanceof ParmNode) ) + for( int i=1; i "+use.uniqueName()+" input "+i; + } + } + return true; + } + private boolean graphColor(byte round) { _failed.clear(); _lrgs.clear(); @@ -233,13 +251,11 @@ public class RegAlloc { if( lrg._mask.isEmpty() && (!lrg._multiDef || lrg._1regUseCnt==1) ) { if( lrg._1regDefCnt <= 1 && lrg._1regUseCnt <= 1 && - (lrg._1regDefCnt + lrg._1regUseCnt) > 0 ) + (lrg._1regDefCnt + lrg._1regUseCnt) > 0 && + // A fixed-register clone with flexible uses must split at the uses. + !(lrg._1regDefCnt==1 && lrg._machDef.isClone() && lrg._1regUseCnt==0) ) return splitEmptyMaskSimple(round,lrg); - // Repeated single-reg uses from a single def. Special for archs - // with more fixed regs. - if( !lrg._multiDef && lrg._1regDefCnt <= 1 && lrg._1regUseCnt > 2 ) - if( splitEmptyMaskByUse(round,lrg) ) - return true; + // More complex conflicts use loop-boundary splitting. } // Generic split-by-loop depth. @@ -255,91 +271,20 @@ public class RegAlloc { // Split just after def if( lrg._1regDefCnt==1 && !lrg._machDef.isClone() ) - // Force must-split, even if a prior split same block because register - // conflicts. Example: + // An earlier copy is reusable only if no intervening clobber. + // Being in the same block alone is insufficient. Example: // alloc // V1/rax - forced by alloc // alloc // V2/rax - kills prior RAX // st4 [V1],len - No good, must split around - insertAfterAndReplace( makeSplit("def/empty1",round,lrg), (Node)lrg._machDef, false/*true*/); + insertAfterAndReplace(makeSplit("def/empty1",round,lrg),(Node)lrg._machDef,false); // Split just before use - if( lrg._1regUseCnt==1 || (lrg._1regDefCnt==1 && ((Node)lrg._machDef).nOuts()==1) ) + if( lrg._1regUseCnt==1 ) insertBefore((Node)lrg._machUse,lrg._uidx,"use/empty1",round,lrg); return true; } - // Single-def live range with an empty mask. There are many single-reg - // uses. Theory is there's many repeats of the same reg amongst the uses. - // In of splitting once per use, start by splitting into groups based on - // required input register. - boolean splitEmptyMaskByUse( byte round, LRG lrg ) { - Node def = (Node)lrg._machDef; - - // Look at each use, and break into non-overlapping register classes. - Ary rclass = new Ary<>(RegMask.class); - boolean done=false; - int ncalls=0; - while( !done ) { - done = true; - for( Node use : def._outputs ) - if( use instanceof MachNode mach ) { - if( mach instanceof CallNode ) ncalls++; - for( int i=1; i 1 ) return false; - - // Split by class - Ary ns = new Ary<>(Node.class); - for( RegMask rmask : rclass ) { - ns.addAll(def._outputs); - Node split = makeSplit(def,"popular",round,lrg,rmask); - split.insertAfter( def ); - if( split.nIns()>1 ) split.setDef(1,def); - // all uses by class to split - for( int j=0; j < def._outputs._len; j++ ) { - Node use = def._outputs.at(j); - if( use instanceof MachNode mach && use!=split ) { - // Check all use inputs for n, in case there's several - for( int i = 1; i < use.nIns(); i++ ) - // Find a def input, and check register class - if( use.in( i ) == def && mach.regmap( i ).overlap( rmask ) ) { - RegMask m = mach.regmap( i ); - if( m!=null && mach.regmap( i ).overlap( rmask ) ) - // Modify use to use the split version specialized to this rclass - { use.setDefOrdered( i, split ); j--; break; } - } - } - } - ns.clear(); - } - return true; - } - - - // Put use into a register class, perhaps adding a class or perhaps - // narrowing a class (and causing a repeat) - private static boolean putIntoRegClass( Ary rclass, RegMask rmask ) { - for( int i=0; i> (reg+1); - if( bits0!=0 ) - return (short)(reg+1+Long.numberOfTrailingZeros(bits0)); - reg=64; + if( reg < 63 ) { + long bits0 = _bits0 & (-1L << (reg+1)); + if( bits0!=0 ) return (short)Long.numberOfTrailingZeros(bits0); } - long bits1 = _bits1 >> (reg-64+1); - if( bits1!=0 ) - return (short)(reg+1+Long.numberOfTrailingZeros(bits1)); - return -1; + long bits1 = reg < 64 ? _bits1 : reg < 127 ? _bits1 & (-1L << (reg-63)) : 0; + return bits1==0 ? -1 : (short)(64+Long.numberOfTrailingZeros(bits1)); } boolean isEmpty() { return _bits0==0 && _bits1==0; } @@ -98,8 +94,8 @@ public class RegMask { // Has exactly 1 bit set public boolean size1() { - return ((_bits0 & -_bits0)==_bits0 && _bits1==0) || - ((_bits1 & -_bits1)==_bits1 && _bits0==0); + return !isEmpty() && (((_bits0 & -_bits0)==_bits0 && _bits1==0) || + ((_bits1 & -_bits1)==_bits1 && _bits0==0)); } // Cardinality diff --git a/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java b/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java index 7305c4d9..f07389eb 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java @@ -26,7 +26,9 @@ public class StoreRISC extends MemOpRISC { if( i==1 ) return null; // mem if( i==2 ) return riscv.RMASK; // ptr // 2 - index - if( i==4 ) return riscv.MEM_MASK; // Wide mask to store GPR and FPR + if( i==4 ) return _sz >= '4' + ? riscv.MEM_MASK // Word/dword value in GPR or FPR + : riscv.RMASK; // Byte/short stores require a GPR return null; // Anti-dependence } @Override public RegMask outregmap() { return null; } diff --git a/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java b/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java index 97586d87..9f80b1f5 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java @@ -66,7 +66,7 @@ public abstract class MemOpX86 extends MemOpNode implements MachNode { if( i==1 ) return null; // Memory if( i==2 ) return x86_64_v2.RMASK; // base in GPR if( i==3 ) return x86_64_v2.RMASK; // index in GPR - if( i==4 ) return _sz >= 2 + if( i==4 ) return _sz >= '4' ? x86_64_v2.MEM_MASK // value in GPR or XMM : x86_64_v2.RMASK; // Bytes and shorts in GPR only return null; // Anti-dependence diff --git a/chapter22/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/chapter22/src/test/java/com/seaofnodes/simple/BrainFuckTest.java index 1e1a1dc3..bf809d2a 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/BrainFuckTest.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/BrainFuckTest.java @@ -32,7 +32,7 @@ public class BrainFuckTest { assertEquals(brain_fuck.length(),A5.ld4s(ptr_arm)); for( int i=0; i { testCPU(src,cpu,os,spills,stop); return null; }); + } + + static void testCPU(String src, String cpu, String os, int spills, String stop) { + CodeGen code = new CheckedCodeGen(src); code.driver(CodeGen.Phase.RegAlloc,cpu,os); - int delta = spills>>3; - if( delta==0 ) delta = 1; - if( spills != -1 ) - assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); - if( stop != null ) - assertEquals(stop, code._stop.toString()); + SpillStats.record(code,"Chapter20",cpu,os); + SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + if( stop!=null ) assertEquals(stop,code._stop.toString()); } - private static void testAllCPUs( String src, int spills, String stop ) { - testCPU(src,"x86_64_v2", "SystemV",spills,stop); - testCPU(src,"riscv" , "SystemV",spills,stop); - testCPU(src,"arm" , "SystemV",spills,stop); + private void testAllCPUs( String src, int spills, String stop ) { + testTarget(src,"x86_64_v2", "SystemV",spills,stop); + testTarget(src,"riscv" , "SystemV",spills,stop); + testTarget(src,"arm" , "SystemV",spills,stop); } @Test public void testAlloc0() { - testCPU("return new u8[arg];","x86_64_v2", "SystemV",3,"return [u8];"); - testCPU("return new u8[arg];","riscv" , "SystemV",5,"return [u8];"); - testCPU("return new u8[arg];","arm" , "SystemV",5,"return [u8];"); + testTarget("return new u8[arg];","x86_64_v2","SystemV",4,"return [u8];"); + testTarget("return new u8[arg];","riscv","SystemV",5,"return [u8];"); + testTarget("return new u8[arg];","arm","SystemV",5,"return [u8];"); } @Test public void testBasic1() { String src = "return arg | 2;"; - testCPU(src,"x86_64_v2", "SystemV",1,"return (ori,mov(arg));"); - testCPU(src,"riscv" , "SystemV",0,"return ( arg | #2 );"); - testCPU(src,"arm" , "SystemV",0,"return (ori,arg);"); + testTarget(src,"x86_64_v2", "SystemV",1,"return (ori,mov(arg));"); + testTarget(src,"riscv" , "SystemV",0,"return ( arg | #2 );"); + testTarget(src,"arm" , "SystemV",0,"return (ori,arg);"); } @Test @@ -85,56 +96,109 @@ val sqrt = { int x -> guess = next; } }; -int cast_int = arg+2; -return sqrt(arg) + sqrt(cast_int); +return sqrt(arg) + sqrt(arg+2); """; - testCPU(src,"x86_64_v2", "Win64" ,55,null); - testCPU(src,"riscv" , "SystemV",19,null); - testCPU(src,"arm" , "SystemV",19,null); + testTarget(src,"x86_64_v2", "SystemV",48,null); + testTarget(src,"riscv" , "SystemV",17,null); + testTarget(src,"arm" , "SystemV",18,null); } @Test - public void testNewtonFloat() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/newtonFloat.smp")) - + "flt farg = arg; return test_sqrt(farg) + test_sqrt(farg+2.0);"; - testCPU(src,"x86_64_v2", "SystemV",39,null); - testCPU(src,"riscv" , "SystemV",17,null); - testCPU(src,"arm" , "SystemV",18,null); + public void testNewtonFloat() { + String src = +""" +// Newtons approximation to the square root +val sqrt = { flt x -> + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +flt farg = arg; +return sqrt(farg) + sqrt(farg+2.0); +"""; + testTarget(src,"x86_64_v2", "SystemV",23,null); + testTarget(src,"riscv" , "SystemV",18,null); + testTarget(src,"arm" , "SystemV",18,null); } @Test public void testAlloc2() { String src = "int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1];"; - testCPU(src,"x86_64_v2", "SystemV",-1,"return .[];"); - testCPU(src,"riscv" , "SystemV", 6,"return .[];"); - testCPU(src,"arm" , "SystemV", 6,"return .[];"); + testTarget(src,"x86_64_v2","SystemV",3,"return .[];"); + testTarget(src,"riscv","SystemV",6,"return .[];"); + testTarget(src,"arm","SystemV",6,"return .[];"); } - - @Test - public void testArray1() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/array1.smp")); - testCPU(src,"x86_64_v2", "SystemV",-1,"return .[];"); - testCPU(src,"riscv" , "SystemV", 8,"return (add,.[],(mul,.[],1000));"); - testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));"); + public void testArray1() { + String src = +""" +int[] !ary = new int[arg]; +// Fill [0,1,2,3,4,...] +for( int i=0; i + if( self == s ) return true; + if( self.cs# != s.cs# ) return false; + for( int i=0; i< self.cs#; i++ ) + if( self.cs[i] != s.cs[i] ) + return false; + return true; +}; + +val hashCode = { String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { String self -> + int hash=0; + if( self.cs ) { + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + } + if( !hash ) hash = 123456789; + return hash; +}; + +String !s = new String { cs = new u8[17]; }; +s.cs[0] = 67; // C +s.cs[1] = 108; // l +hashCode(s); +"""; + testTarget(src,"x86_64_v2", "SystemV",0,null); + testTarget(src,"riscv" , "SystemV",0,null); + testTarget(src,"arm" , "SystemV",0,null); } @Test public void testCast() { String src = "struct Bar { int x; }; var b = arg ? new Bar; return b ? b.x++ + b.x++ : -1;"; - testCPU(src,"x86_64_v2", "SystemV",2,null); - testCPU(src,"riscv" , "SystemV",2,null); - testCPU(src,"arm" , "SystemV",2,null); + testTarget(src,"x86_64_v2","SystemV",1,null); + testTarget(src,"riscv","SystemV",2,null); + testTarget(src,"arm","SystemV",2,null); } @Test @@ -152,9 +216,9 @@ if (b2) if (b1) return 1; if (b1) return 2; return 0; """; - testCPU(src,"x86_64_v2", "SystemV",0,"return Phi(Region,1,2,0);"); - testCPU(src,"riscv" , "SystemV",0,"return Phi(Region,1,2,0);"); - testCPU(src,"arm" , "SystemV",0,"return Phi(Region,1,2,0);"); + testTarget(src,"x86_64_v2", "SystemV",0,"return Phi(Region,1,2,0);"); + testTarget(src,"riscv" , "SystemV",0,"return Phi(Region,1,2,0);"); + testTarget(src,"arm" , "SystemV",0,"return Phi(Region,1,2,0);"); } @Test @@ -167,8 +231,126 @@ while (arg > 0) { } return arg; """; - testCPU(src,"x86_64_v2", "SystemV",3,null); - testCPU(src,"riscv" , "SystemV",2,null); - testCPU(src,"arm" , "SystemV",2,null); + testTarget(src,"x86_64_v2", "SystemV",3,null); + testTarget(src,"riscv" , "SystemV",2,null); + testTarget(src,"arm" , "SystemV",2,null); } + // Original Chapter 20 allocation workload, kept fixed for cohort comparisons. + @Test + public void testBrainfuck() { + var program = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.".getBytes(java.nio.charset.StandardCharsets.UTF_8); + var encoded = new StringBuilder("u8[] !program = new u8[").append(program.length).append("];"); + for (int i = 0; i < program.length; i++) { + int value = program[i] & 0xFF; + encoded.append("program[").append(i).append("] = ").append(value).append(";"); + } + + String src = encoded + """ + +int d = 0; +u8[] !output = new u8[0]; +u8[] !data = new u8[100]; + +for( int pc = 0; pc < program#; pc++ ) { + var command = program[pc]; + if (command == 62) { + d++; + } else if (command == 60) { + d--; + } else if (command == 43) { + data[d]++; + } else if (command == 45) { + data[d]--; + } else if (command == 46) { + // Output a byte; increase the output array size + var old = output; + output = new u8[output# + 1]; + for( int i = 0; i < old#; i++ ) + output[i] = old[i]; + output[old#] = data[d]; // Add the extra byte on the end + } else if (command == 44) { + data[d] = 42; + } else if (command == 91) { + if (data[d] == 0) { + for( int d = 1; d > 0; ) { + command = program[++pc]; + if (command == 91) d++; + if (command == 93) d--; + } + } + } else if (command == 93) { + if (data[d]) { + for( int d = 1; d > 0; ) { + command = program[--pc]; + if (command == 93) d++; + if (command == 91) d--; + } + } + } +} +return output; +"""; + testTarget(src,"x86_64_v2", "SystemV",40,null); + testTarget(src,"riscv" , "SystemV",28,null); + testTarget(src,"arm" , "SystemV",28,null); + //assertEquals("Hello World!\n", Eval2.eval(code, 0, 10000)); + } + // Original Chapter 20 allocation workload, kept fixed for cohort comparisons. + @Test + public void testMergeSort() { + String src = +""" +// based on the top-down version from https://en.wikipedia.org/wiki/Merge_sort + +val merge_sort = { int[] a, int[] b, int n -> + copy_array(a, 0, n, b); + split_merge(a, 0, n, b); +}; + +val split_merge = { int[] b, int begin, int end, int[] a -> + if (end - begin <= 1) + return 0; + int middle = (end + begin) / 2; + split_merge(a, begin, middle, b); + split_merge(a, middle, end, b); + merge(b, begin, middle, end, a); + return 0; +}; + +val merge = { int[] b, int begin, int middle, int end, int[] a -> + int i = begin, j = middle; + + for (int k = begin; k < end; k++) { + // && and || + bool cond = false; + if (i < middle) { + if (j >= end) cond = true; + else if (a[i] <= a[j]) cond = true; + } + if (cond) b[k] = a[i++]; + else b[k] = a[j++]; + } +}; + +val copy_array = { int[] a, int begin, int end, int[] b -> + for (int k = begin; k < end; k++) + b[k] = a[k]; +}; + +int[] !a = new int[arg]; +int[] !b = new int[a#]; + +for (int i = 0; i < a#; i++) + a[i] = a# - i; + +merge_sort(a, b, a#); + +return a; +"""; + testTarget(src,"x86_64_v2", "SystemV",52,null); + testTarget(src,"riscv" , "SystemV",44,null); + testTarget(src,"arm" , "SystemV",44,null); +//assertEquals("int[ 1,2,3,4,5,6,7,8,9,10,11]", Eval2.eval(code, 11)); + } + } diff --git a/chapter22/src/test/java/com/seaofnodes/simple/Chapter21AllocTest.java b/chapter22/src/test/java/com/seaofnodes/simple/Chapter21AllocTest.java new file mode 100644 index 00000000..4fddbb4b --- /dev/null +++ b/chapter22/src/test/java/com/seaofnodes/simple/Chapter21AllocTest.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple; + +import java.io.IOException; +import java.nio.file.*; +import org.junit.Test; + +// Revised Chapter 20 examples introduced with native encoding. +public class Chapter21AllocTest { + @Test + public void testNewtonInteger() { + String src = +""" +// Newtons approximation to the square root +val sqrt = { int x -> + int guess = x; + while( 1 ) { + int next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +return sqrt(arg) + sqrt(arg+2); +"""; + Chapter21Test.testCPU(src,"x86_64_v2", "Win64" ,48,null); + Chapter21Test.testCPU(src,"riscv" , "SystemV",19,null); + Chapter21Test.testCPU(src,"arm" , "SystemV",19,null); + } + + @Test + public void testNewtonFloat() throws IOException { + String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/newtonFloat.smp")) + + "flt farg = arg; return test_sqrt(farg) + test_sqrt(farg+2.0);"; + Chapter21Test.testCPU(src,"x86_64_v2", "SystemV",39,null); + Chapter21Test.testCPU(src,"riscv" , "SystemV",17,null); + Chapter21Test.testCPU(src,"arm" , "SystemV",18,null); + } + + @Test + public void testArray1() throws IOException { + String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/array1.smp")); + Chapter21Test.testCPU(src,"x86_64_v2", "SystemV",-1,"return .[];"); + Chapter21Test.testCPU(src,"riscv" , "SystemV", 8,"return (add,.[],(mul,.[],1000));"); + Chapter21Test.testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));"); + } + + + @Test + public void testString() throws IOException { + String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/stringHash.smp")); + Chapter21Test.testCPU(src,"x86_64_v2", "SystemV", 9,null); + Chapter21Test.testCPU(src,"riscv" , "SystemV", 3,null); + Chapter21Test.testCPU(src,"arm" , "SystemV", 3,null); + } + +} diff --git a/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 13dd410e..72f79d6c 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.cpus.arm.arm; import com.seaofnodes.simple.node.cpus.riscv.riscv; @@ -25,6 +26,53 @@ public class Chapter21Test { } + @Test public void testNarrowStores() throws IOException { + for( String type : new String[]{"i8","u8","i16","u16"} ) { + String src = "struct S { "+type+" x; }; S !s = new S; s.x = arg; return 0;"; + CodeGen code = new CodeGen(src).driver("riscv","SystemV",null); + int stores=0; + for( var bb : code._cfg ) + for( var node : bb.outs() ) + if( node instanceof com.seaofnodes.simple.node.cpus.riscv.StoreRISC st ) { + stores++; + assertEquals("Byte/short stores have no FP encoding",-1,st.regmap(4).nextReg((short)31)); + } + assertEquals(1,stores); + byte[] image = new byte[1<<20]; + byte[] bits = code._encoding.bits(); + System.arraycopy(bits,0,image,0,bits.length); + EvalRisc5 cpu = new EvalRisc5(image,1<<16); + cpu.regs[riscv.A0] = 0x8765; + assertEquals(0,cpu.step(100)); + assertEquals(type.endsWith("8") ? 0x65 : 0x8765, + type.endsWith("8") ? cpu.ld1z(1<<16) : cpu.ld2z(1<<16)); + code = new CodeGen(src).driver(CodeGen.Phase.Encoding,"x86_64_v2",TestC.CALL_CONVENTION); + stores=0; + for( var bb : code._cfg ) + for( var node : bb.outs() ) + if( node instanceof com.seaofnodes.simple.node.cpus.x86_64_v2.StoreX86 st ) { + stores++; + assertEquals("Byte/short stores have no XMM encoding",-1,st.regmap(4).nextReg((short)15)); + } + assertEquals(1,stores); + } + } + + @Test public void testNativeExitStatus() throws IOException { + Path source = Path.of("build/objs/nativeExit.c"); + Files.createDirectories(source.getParent()); + Files.writeString(source,"int main() { return 7; }\n"); + try { + TestC.gcc(source.toString(),null,null,false, + "build/objs/nativeExit"+(TestC.OS.startsWith("Windows") ? ".exe" : "")); + } catch( AssertionError error ) { + assertTrue(error.getMessage(),error.getMessage().contains("Program exit status")); + return; + } + fail("A failing native program must fail the test, even with empty stdout"); + } + + @Test public void testCoalescing() { com.seaofnodes.simple.codegen.RegAllocTestSupport.coalescing(); } @Test public void testRisc64BitStore() { byte[] mem = new byte[24]; EvalRisc5 cpu = new EvalRisc5(mem,mem.length); @@ -63,11 +111,9 @@ public class Chapter21Test { } static void testCPU( String src, String cpu, String os, int spills, String stop ) { - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); - int delta = spills>>3; - if( delta==0 ) delta = 1; - if( spills != -1 ) - assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); + SpillStats.record(code,"Chapter21",cpu,os); + SpillStats.checkSpills(spills,code._regAlloc._spillScaled); if( stop != null ) assertEquals(stop, code._stop.toString()); } @@ -81,7 +127,7 @@ public class Chapter21Test { } @Test public void testInfinite() { - String src = "struct S { int i; }; S !s = new S; while(1) s.i++; return s.i;"; + String src = "struct S { int i; }; S !s = new S; while(1) s.i++;"; testCPU(src,"x86_64_v2", "SystemV",0,"return Top;"); testCPU(src,"riscv" , "SystemV",2,"return Top;"); testCPU(src,"arm" , "SystemV",2,"return Top;"); @@ -107,7 +153,7 @@ public class Chapter21Test { public void testString() throws IOException { String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/stringHash.smp")); testCPU(src,"x86_64_v2", "SystemV", 9,null); - testCPU(src,"riscv" , "SystemV", 5,null); + testCPU(src,"riscv" , "SystemV", 3,null); testCPU(src,"arm" , "SystemV", 3,null); } @@ -162,11 +208,11 @@ public class Chapter21Test { String sprimes = sb.p("]").toString(); // Compile, link against native C; expect the above string of primes to be printed out by C - TestC.run("sieve",sprimes, 257); + TestC.run("sieve",sprimes, 178); // Evaluate on RISC5 emulator; expect return of an array of primes in // the simulated heap. - EvalRisc5 R5 = TestRisc5.build("sieve", 100, 160, false); + EvalRisc5 R5 = TestRisc5.build("sieve", 100, 89, false); int trap = R5.step(10000); assertEquals(0,trap); // Return register A0 holds sieve(100) @@ -178,7 +224,7 @@ public class Chapter21Test { // Evaluate on ARM5 emulator; expect return of an array of primes in // the simulated heap. - EvalArm64 A5 = TestArm64.build("sieve", 100, 160, false); + EvalArm64 A5 = TestArm64.build("sieve", 100, 93, false); int trap_arm = A5.step(10000); assertEquals(0, trap_arm); int ary_arm = (int)A5.regs[arm.X0]; @@ -192,14 +238,14 @@ public class Chapter21Test { String fib = "[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]"; TestC.run("fib", fib, 24); - EvalRisc5 R5 = TestRisc5.build("fib", 9, 17, false); + EvalRisc5 R5 = TestRisc5.build("fib", 9, 16, false); int trap = R5.step(100); assertEquals(0,trap); // Return register A0 holds fib(8)==55 assertEquals(55,R5.regs[riscv.A0]); // arm - EvalArm64 A5 = TestArm64.build("fib", 9, 17, false); + EvalArm64 A5 = TestArm64.build("fib", 9, 16, false); int trap_arm = A5.step(100); assertEquals(0,trap_arm); // Return register X0 holds fib(8)==55 @@ -208,7 +254,7 @@ public class Chapter21Test { @Test public void testPerson() throws IOException { String person = "6\n"; - TestC.run("person", person, 0); + TestC.run("person21", person, 0); // Memory layout starting at PS: int ps = 1<<16; // Person array pointer starts at heap start @@ -219,7 +265,7 @@ public class Chapter21Test { int p1 = ps+4*8+1*8; // P2 = { age } // sizeof=8 int p2 = ps+4*8+2*8; - EvalRisc5 R5 = TestRisc5.build("person", ps, 0, false); + EvalRisc5 R5 = TestRisc5.build("person21", ps, 0, false); R5.regs[riscv.A1] = 1; // Index 1 R5.st8(ps,3); // Length R5.st8(ps+1*8,p0); @@ -235,7 +281,7 @@ public class Chapter21Test { assertEquals(17+1,R5.ld8(p1)); assertEquals(60+0,R5.ld8(p2)); - EvalArm64 A5 = TestArm64.build("person", ps, 0, false); + EvalArm64 A5 = TestArm64.build("person21", ps, 0, false); A5.regs[arm.X1] = 1; // Index 1 A5.st8(ps, 3); A5.st8(ps+1*8,p0); @@ -257,7 +303,7 @@ public class Chapter21Test { // than what Win64 allows - so Win64 gets a lot more spills here. String arg_count = "191.000000\n"; TestC.run("arg_count", arg_count, - TestC.CALL_CONVENTION.equals("Win64") ? 42 : 15); + TestC.CALL_CONVENTION.equals("Win64") ? 32 : 15); EvalRisc5 R5 = TestRisc5.build("no_stack_arg_count", 0, 0, false); diff --git a/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java index 394ba336..369e365e 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.CodeGen; import java.io.IOException; import java.nio.file.Files; @@ -12,6 +13,13 @@ import org.junit.Test; import static org.junit.Assert.*; public class Chapter22Test { + @Test public void testInfiniteReturn() { + String src = "struct S { int i; }; S !s = new S; while(1) s.i++; return s.i;"; + testCPU(src,"x86_64_v2","SystemV",0,"return Top;"); + testCPU(src,"riscv","SystemV",2,"return Top;"); + testCPU(src,"arm","SystemV",2,"return Top;"); + } + @Test @Ignore public void testJig() throws IOException { @@ -23,11 +31,9 @@ public class Chapter22Test { } static CodeGen testCPU( String src, String cpu, String os, int spills, String stop ) { - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); - int delta = spills>>3; - if( delta==0 ) delta = 1; - if( spills != -1 ) - assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); + SpillStats.record(code,"Chapter22",cpu,os); + SpillStats.checkSpills(spills,code._regAlloc._spillScaled); if( stop != null ) assertEquals(stop, code._stop.toString()); return code; diff --git a/chapter22/src/test/java/com/seaofnodes/simple/MergeSortTest.java b/chapter22/src/test/java/com/seaofnodes/simple/MergeSortTest.java index a1004fb5..82b1e58e 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/MergeSortTest.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/MergeSortTest.java @@ -17,7 +17,7 @@ public class MergeSortTest { for( int prime : primes ) sb.p(prime).p(", "); String sprimes = sb.p("]").toString(); - TestC.run("sort", sprimes, 40); + TestC.run("sort", sprimes, 34); EvalRisc5 R5 = TestRisc5.build("src/test/java/com/seaofnodes/simple/progs", "sort", "merge_sort", 0, 36, false); diff --git a/chapter22/src/test/java/com/seaofnodes/simple/SpillStats.java b/chapter22/src/test/java/com/seaofnodes/simple/SpillStats.java new file mode 100644 index 00000000..82cffef8 --- /dev/null +++ b/chapter22/src/test/java/com/seaofnodes/simple/SpillStats.java @@ -0,0 +1,71 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import java.util.Map; +import java.util.TreeMap; +import org.junit.runner.*; +import org.junit.runner.notification.*; + +// Run existing tests and sum actual allocations, including ones without goldens. +// Helpers call record after allocation; ordinary JUnit runs collect nothing. +public class SpillStats extends RunListener { + private static SpillStats ACTIVE; + private Description _test; + private int _qualityFailures; + private final Map _totals = new TreeMap<>(); + + @Override public void testStarted(Description test) { _test = test; } + public static void record(CodeGen code, String cohort, String cpu, String abi) { + if( ACTIVE==null ) return; + String test = ACTIVE._test.getTestClass().getSimpleName()+"."+ACTIVE._test.getMethodName(); + for( String key : new String[]{cohort+","+cpu+","+abi,cohort+",ALL,ALL"} ) { + long[] sum = ACTIVE._totals.computeIfAbsent(key,k -> new long[3]); + sum[0]++; sum[1] += code._regAlloc._spills; sum[2] += code._regAlloc._spillScaled; + } + System.out.println("allocation,"+cohort+","+test+","+cpu+","+abi+","+ + code._regAlloc._spills+","+code._regAlloc._spillScaled); + } + + // The same native helpers serve old and new program cohorts. + public static void recordNative(CodeGen code, String cpu, String abi) { + String cohort = ACTIVE!=null && ACTIVE._test.getTestClass()==Chapter22Test.class + ? "Chapter22" : "Chapter21"; + record(code,cohort,cpu,abi); + } + + // Keep running every CPU and native result check when measuring quality. + // A changed golden still makes spill-stats fail at the end. + public static void checkSpills(int expected, int actual) { + if( expected==-1 ) return; + try { org.junit.Assert.assertEquals("Expect spills:",expected,actual,Math.max(1,expected>>3)); } + catch( AssertionError error ) { + if( ACTIVE==null ) throw error; + ACTIVE._qualityFailures++; + System.err.println(ACTIVE._test+": "+error.getMessage()); + } + } + + public static void main(String[] args) throws ClassNotFoundException { + if( args.length==0 ) args = new String[]{"Chapter20Test","Chapter21Test","Chapter21AllocTest","Chapter22Test","BrainFuckTest","MergeSortTest"}; + Class[] tests = new Class[args.length]; + for( int i=0; i>3; - if( delta == 0 ) delta =1; - if( spills != -1 ) assertEquals("Expect spills:", spills, code._regAlloc._spillScaled, delta); + SpillStats.checkSpills(spills,code._regAlloc._spillScaled); // Image byte[] image = new byte[1<<20]; // A megabyte (1024*1024 bytes) diff --git a/chapter22/src/test/java/com/seaofnodes/simple/TestC.java b/chapter22/src/test/java/com/seaofnodes/simple/TestC.java index 93fa9b66..44c06128 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/TestC.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/TestC.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.CodeGen; import java.io.IOException; import java.nio.file.Files; @@ -43,15 +44,13 @@ public abstract class TestC { String obj = bin+".o"; String exe = OS.startsWith("Windows") ? bin+".exe" : bin; // Compile simple, emit ELF - CodeGen code = new CodeGen(src).driver( CPU_PORT, simple_conv, obj); + CodeGen code = new CheckedCodeGen(src).driver( CPU_PORT, simple_conv, obj); + SpillStats.recordNative(code,CPU_PORT,simple_conv); String result = gcc(obj, c_conv, cfile, false, exe ); assertEquals(expected,result); // Allocation quality not degraded - int delta = spills>>3; - if( delta==0 ) delta = 1; - if( spills != -1 ) - assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + SpillStats.checkSpills(spills,code._regAlloc._spillScaled); } public static String gcc( String obj, String c_conv, String cfile, boolean stdin, String... args ) throws IOException { @@ -97,10 +96,9 @@ public abstract class TestC { ProcessBuilder smp = new ProcessBuilder(args); if( stdin ) smp.redirectInput(ProcessBuilder.Redirect.INHERIT); Process p = smp.start(); - try { exit = (byte)p.waitFor(); } catch( InterruptedException e ) { throw new IOException("interrupted"); } + try { exit = p.waitFor(); } catch( InterruptedException e ) { throw new IOException("interrupted"); } result = new String(p.getInputStream().readAllBytes()); - if( exit!=0 ) - System.err.println("exec exit code: "+exit); + assertEquals("Program exit status",0,exit); return result; } } diff --git a/chapter22/src/test/java/com/seaofnodes/simple/TestRisc5.java b/chapter22/src/test/java/com/seaofnodes/simple/TestRisc5.java index 041ed833..adb256eb 100644 --- a/chapter22/src/test/java/com/seaofnodes/simple/TestRisc5.java +++ b/chapter22/src/test/java/com/seaofnodes/simple/TestRisc5.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.FunNode; import com.seaofnodes.simple.node.Node; @@ -20,14 +21,12 @@ public abstract class TestRisc5 { public static EvalRisc5 build( String dir, String file, String main, int arg, int spills, boolean print ) throws IOException { // Compile and export Simple String src = Files.readString(Path.of(dir+"/"+file+".smp")); - CodeGen code = new CodeGen(src).driver("riscv", "SystemV",null); + CodeGen code = new CheckedCodeGen(src).driver("riscv", "SystemV",null); + SpillStats.recordNative(code,"riscv","SystemV"); if( print ) { code.print_as_hex(); System.out.print(code.asm()); } // Allocation quality not degraded - int delta = spills>>3; - if( delta==0 ) delta = 1; - if( spills != -1 ) - assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + SpillStats.checkSpills(spills,code._regAlloc._spillScaled); // Image byte[] image = new byte[1<<20]; // A megabyte (1024*1024 bytes) diff --git a/chapter22/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/chapter22/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java new file mode 100644 index 00000000..85de8b36 --- /dev/null +++ b/chapter22/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java @@ -0,0 +1,208 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.cpus.x86_64_v2.x86_64_v2; +import com.seaofnodes.simple.type.TypeInteger; +import static org.junit.Assert.*; + +// Small scheduled machine graphs isolate register constraints from peepholes. +public class RegAllocTestSupport { + private static final RegMask A = new RegMask(0), B = new RegMask(1); + private static class Op extends MachConcreteNode { + final RegMask _def, _use, _kill; + final boolean _clone; + Op(RegMask def, RegMask use, RegMask kill, boolean clone, Node... ins) { + super(ins); _def=def; _use=use; _kill=kill; _clone=clone; _type=TypeInteger.BOT; + } + @Override public String op() { return "testOp"; } + @Override public RegMask outregmap() { return _def; } + @Override public RegMask regmap(int i) { return _use; } + @Override public RegMask killmap() { return _kill; } + @Override public boolean isClone() { return _clone; } + @Override public Node copy() { return new Op(_def,_use,_kill,_clone,(Node)null); } + @Override public void encoding(Encoding enc) { throw new AssertionError(); } + } + private static CodeGen graph() { + CodeGen code = new CodeGen(""); + code._mach = new x86_64_v2(code); + code._cfg.add(code._start); + return code; + } + + public static void masks() { + for( int reg : new int[]{0,63,64,65,127} ) { + RegMask mask = new RegMask(reg); + assertEquals(reg,mask.firstReg()); + assertEquals(1,mask.size()); + } + assertEquals(-1,new RegMask(0L).firstReg()); + assertFalse(new RegMask(0L).size1()); + RegMask boundary = new RegMask(Long.MIN_VALUE,Long.MIN_VALUE|1); + assertEquals(64,boundary.nextReg((short)63)); + assertEquals(127,boundary.nextReg((short)64)); + assertEquals(-1,boundary.nextReg((short)127)); + } + + public static void union() { + CodeGen code = graph(); + Op use = new Op(null,B,null,false,code._start); + LRG a = new LRG((short)1), b = new LRG((short)2); + a._mask = new RegMask(3L); + b._mask = new RegMask(6L); + b.machUse(use,(short)2,true); + LRG both = a.union(b); + assertNotNull(both._mask); + assertEquals(1,both._mask.firstReg()); + assertEquals(1,both._mask.size()); + assertSame(use,both._machUse); + assertEquals(2,both._uidx); + } + + public static void killWithoutResult() { + CodeGen code = graph(); + Op def = new Op(A,null,null,false,code._start); + new Op(null,null,A,false,code._start); // Clobbers A, produces no value. + new Op(null,A,null,false,code._start,def); + RegAlloc alloc = new RegAlloc(code); + assertTrue(BuildLRG.run(0,alloc)); + assertFalse("A live value cannot survive a register kill",IFG.build(0,alloc)); + } + + public static void nullUseMask() { + CodeGen code = graph(); + Op def = new Op(A,null,null,false,code._start); + new Op(null,null,null,false,code._start,def); // Scheduling dependency only. + RegAlloc alloc = new RegAlloc(code); + assertTrue(BuildLRG.run(0,alloc)); + assertTrue(IFG.build(0,alloc)); + } + + public static void cloneRegisterClass() { + for( RegMask required : new RegMask[]{B,new RegMask(6L)} ) { + CodeGen code = graph(); + Op def = new Op(A,null,null,true,code._start); + Op use = new Op(null,required,null,false,code._start,def); + Op use2 = new Op(null,required,null,false,code._start,def); + RegAlloc alloc = new RegAlloc(code); + alloc.regAlloc(); // Re-cloning an A-only value cannot satisfy B/C uses. + assertEquals(0,alloc.regnum(def)); + assertTrue(required.test(alloc.regnum(use.in(1)))); + assertTrue(required.test(alloc.regnum(use2.in(1)))); + assertTrue(use.in(1) instanceof SplitNode); + } + } + + public static void commutativePhi() { + CodeGen code = graph(); + Op a = new Op(A,null,null,false,code._start); + Op b = new Op(A,null,null,false,code._start); + Op add = new Op(A,A,null,false,code._start,a,b) { + @Override public boolean commutes() { return true; } + }; + RegionNode region = new RegionNode(null,code._start,code._start); + new PhiNode("sum",TypeInteger.BOT,region,add,a); + code._cfg.add(region); + assertTrue(BuildLRG.run(0,new RegAlloc(code))); + } + + // Check the scheduled graph before encoding adds untyped branches or rewrites tail calls. + public static class CheckedCodeGen extends CodeGen { + public CheckedCodeGen(String src) { super(src); } + @Override public CodeGen regAlloc() { + super.regAlloc(); + checkRegisters(this); + return this; + } + } + + public static void checkRegisters(CodeGen code) { + assertTrue(code._regAlloc.verifyFunctionLocalEdges()); + for( CFGNode bb : code._cfg ) + for( Node n : bb._outputs ) { + if( n instanceof MachNode mach ) { + if( code._regAlloc.regnum(n)>=0 && mach.outregmap()!=null ) + assertTrue(n.getClass().getSimpleName()+"#"+n._nid,code._regAlloc.regnum(n)>=0 && mach.outregmap().test(code._regAlloc.regnum(n))); + for( int i=n instanceof ParmNode ? 2 : 1; i=0 && mach.regmap(i)!=null ) + assertTrue(n.getClass().getSimpleName()+"#"+n._nid+" input "+i,code._regAlloc.regnum(n.in(i))>=0 && mach.regmap(i).test(code._regAlloc.regnum(n.in(i)))); + if( mach.twoAddress()!=0 ) + assertEquals(code._regAlloc.regnum(n),code._regAlloc.regnum(n.in(mach.twoAddress()))); + } + if( n instanceof PhiNode && code._regAlloc.regnum(n)>=0 ) + for( int i=n instanceof ParmNode ? 2 : 1; i=3 ) { + from.addNeighbor(left); left.addNeighbor(from); + if( kind==3 ) { to.addNeighbor(right); right.addNeighbor(to); } + else { to.addNeighbor(left); left.addNeighbor(to); } + } + int fcnt=from.nadj(), tcnt=to.nadj(); + assertTrue(Coalesce.coalesce(0,alloc)); + if( kind==0 || kind==4 ) { + assertSame(def,use.in(1)); + assertSame(alloc.lrg(def),alloc.lrg(copy)); + assertSame(use,alloc.lrg(def)._machUse); + assertEquals(1,alloc.lrg(def)._uidx); + if( kind==4 ) { + assertEquals(1,left.nadj()); + assertSame(alloc.lrg(def),left._adj.at(0)); + assertSame(left,alloc.lrg(def)._adj.at(0)); + } + } else { + assertSame(copy,use.in(1)); + assertNotSame(alloc.lrg(def),alloc.lrg(copy)); + assertEquals(fcnt,from.nadj()); + assertEquals(tcnt,to.nadj()); + } + } + } + +} diff --git a/chapter22/src/test/java/com/seaofnodes/simple/progs/person21.c b/chapter22/src/test/java/com/seaofnodes/simple/progs/person21.c new file mode 100644 index 00000000..df95e1ca --- /dev/null +++ b/chapter22/src/test/java/com/seaofnodes/simple/progs/person21.c @@ -0,0 +1,34 @@ +#include +#include +#include + +typedef struct Person { + int age; +} Person; + +typedef struct PersonArray { + unsigned int length; + int pad; + Person* data[]; +} PersonArray; + +__attribute__ ((CALL_CONV)) +extern void fcn(PersonArray* people, int index); + +int main() { + Person base[5] = {{25},{1},{18},{45},{60}}; + unsigned char buf[8+8*5]; + PersonArray* people_array = (PersonArray*)buf; + people_array->length=5; + for (int i=0; ilength; i++) people_array->data[i] = &base[i]; + + fcn(people_array, 1); + fcn(people_array, 1); + fcn(people_array, 1); + fcn(people_array, 1); + fcn(people_array, 1); + + printf("%d\n", people_array->data[1]->age); // expected: 6 + + return 0; +} \ No newline at end of file diff --git a/chapter22/src/test/java/com/seaofnodes/simple/progs/person21.smp b/chapter22/src/test/java/com/seaofnodes/simple/progs/person21.smp new file mode 100644 index 00000000..9f0f5bea --- /dev/null +++ b/chapter22/src/test/java/com/seaofnodes/simple/progs/person21.smp @@ -0,0 +1,9 @@ +// -*-java-*- +struct Person { + int age; +}; + +val fcn = { Person?[] ps, int x -> + if( ps[x] ) + ps[x].age++; +}; \ No newline at end of file -- cgit v1.2.3