From cc099e088b2c1cffb58338d48756d61ad5219a75 Mon Sep 17 00:00:00 2001 From: Sundara Vishnu Satish Date: Wed, 10 Sep 2025 13:34:26 -0400 Subject: [PATCH 01/18] feat: skeleton --- tests/compile.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/compile.py diff --git a/tests/compile.py b/tests/compile.py new file mode 100644 index 000000000..e69de29bb From cefd38e6dce94de750347b9ab2b0ca1272977d00 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 13:54:28 -0400 Subject: [PATCH 02/18] add mpllib code --- tests/mpllib/AdjacencyGraph.sml | 390 +++++++++ tests/mpllib/AdjacencyInt.sml | 157 ++++ tests/mpllib/ArraySequence.sml | 281 ++++++ tests/mpllib/AugMap.sml | 509 +++++++++++ tests/mpllib/Benchmark.sml | 83 ++ tests/mpllib/BinarySearch.sml | 81 ++ tests/mpllib/CheckSort.sml | 69 ++ tests/mpllib/ChunkedTreap.sml | 186 ++++ tests/mpllib/Color.sml | 176 ++++ tests/mpllib/CommandLineArgs.sml | 104 +++ tests/mpllib/CountingSort.sml | 129 +++ tests/mpllib/DelayedSeq.sml | 414 +++++++++ tests/mpllib/DelayedStream.sml | 225 +++++ tests/mpllib/DoubleBinarySearch.sml | 123 +++ tests/mpllib/ExtraBinIO.sml | 73 ++ tests/mpllib/FindFirst.sml | 39 + tests/mpllib/FlattenMerge.sml | 38 + tests/mpllib/FuncSequence.sml | 46 + tests/mpllib/GIF.sml | 722 ++++++++++++++++ tests/mpllib/Geometry2D.sml | 88 ++ tests/mpllib/Geometry3D.sml | 23 + tests/mpllib/Hashset.sml | 111 +++ tests/mpllib/Hashtable.sml | 127 +++ tests/mpllib/MatCOO.sml | 681 +++++++++++++++ tests/mpllib/Merge.sml | 98 +++ tests/mpllib/Mergesort.sml | 69 ++ tests/mpllib/MeshToImage.sml | 278 ++++++ tests/mpllib/MkComplex.sml | 111 +++ tests/mpllib/MkGrep.sml | 105 +++ tests/mpllib/NearestNeighbors.sml | 298 +++++++ tests/mpllib/NewWaveIO.sml | 262 ++++++ tests/mpllib/OffsetSearch.sml | 26 + tests/mpllib/OldDelayedSeq.sml | 922 ++++++++++++++++++++ tests/mpllib/PPM.sml | 280 ++++++ tests/mpllib/ParFuncArray.sml | 223 +++++ tests/mpllib/Parse.sml | 179 ++++ tests/mpllib/ParseFile.sml | 190 +++++ tests/mpllib/PureSeq.sml | 222 +++++ tests/mpllib/Quicksort.sml | 133 +++ tests/mpllib/RadixSort.sml | 145 ++++ tests/mpllib/Rat.sml | 145 ++++ tests/mpllib/RecursiveStream.sml | 181 ++++ tests/mpllib/SEQUENCE.sml | 73 ++ tests/mpllib/STREAM.sml | 27 + tests/mpllib/SampleSort.sml | 195 +++++ tests/mpllib/SeqBasis.sml | 214 +++++ tests/mpllib/SeqifiedMerge.sml | 59 ++ tests/mpllib/Seqifier.sml | 206 +++++ tests/mpllib/Shuffle.sml | 64 ++ tests/mpllib/Signal.sml | 290 +++++++ tests/mpllib/StableMerge.sml | 100 +++ tests/mpllib/StableMergeLowSpan.sml | 69 ++ tests/mpllib/StableSort.sml | 81 ++ tests/mpllib/TFlatten.sml | 51 ++ tests/mpllib/TabFilterTree.sml | 76 ++ tests/mpllib/Tokenize.sml | 53 ++ tests/mpllib/Topology2D.sml | 1088 ++++++++++++++++++++++++ tests/mpllib/TreeMatrix.sml | 148 ++++ tests/mpllib/Util.sml | 354 ++++++++ tests/mpllib/compat/PosixReadFile.sml | 60 ++ tests/mpllib/compat/PosixWriteFile.sml | 13 + tests/mpllib/compat/mlton.mlb | 25 + tests/mpllib/compat/mlton.sml | 68 ++ tests/mpllib/compat/mpl-old.mlb | 25 + tests/mpllib/compat/mpl-old.sml | 49 ++ tests/mpllib/compat/mpl.mlb | 23 + tests/mpllib/compat/mpl.sml | 236 +++++ tests/mpllib/sources.mlton.mlb | 85 ++ tests/mpllib/sources.mpl.mlb | 85 ++ 69 files changed, 12559 insertions(+) create mode 100644 tests/mpllib/AdjacencyGraph.sml create mode 100644 tests/mpllib/AdjacencyInt.sml create mode 100644 tests/mpllib/ArraySequence.sml create mode 100644 tests/mpllib/AugMap.sml create mode 100644 tests/mpllib/Benchmark.sml create mode 100644 tests/mpllib/BinarySearch.sml create mode 100644 tests/mpllib/CheckSort.sml create mode 100644 tests/mpllib/ChunkedTreap.sml create mode 100644 tests/mpllib/Color.sml create mode 100644 tests/mpllib/CommandLineArgs.sml create mode 100644 tests/mpllib/CountingSort.sml create mode 100644 tests/mpllib/DelayedSeq.sml create mode 100644 tests/mpllib/DelayedStream.sml create mode 100644 tests/mpllib/DoubleBinarySearch.sml create mode 100644 tests/mpllib/ExtraBinIO.sml create mode 100644 tests/mpllib/FindFirst.sml create mode 100644 tests/mpllib/FlattenMerge.sml create mode 100644 tests/mpllib/FuncSequence.sml create mode 100644 tests/mpllib/GIF.sml create mode 100644 tests/mpllib/Geometry2D.sml create mode 100644 tests/mpllib/Geometry3D.sml create mode 100644 tests/mpllib/Hashset.sml create mode 100644 tests/mpllib/Hashtable.sml create mode 100644 tests/mpllib/MatCOO.sml create mode 100644 tests/mpllib/Merge.sml create mode 100644 tests/mpllib/Mergesort.sml create mode 100644 tests/mpllib/MeshToImage.sml create mode 100644 tests/mpllib/MkComplex.sml create mode 100644 tests/mpllib/MkGrep.sml create mode 100644 tests/mpllib/NearestNeighbors.sml create mode 100644 tests/mpllib/NewWaveIO.sml create mode 100644 tests/mpllib/OffsetSearch.sml create mode 100644 tests/mpllib/OldDelayedSeq.sml create mode 100644 tests/mpllib/PPM.sml create mode 100644 tests/mpllib/ParFuncArray.sml create mode 100644 tests/mpllib/Parse.sml create mode 100644 tests/mpllib/ParseFile.sml create mode 100644 tests/mpllib/PureSeq.sml create mode 100644 tests/mpllib/Quicksort.sml create mode 100644 tests/mpllib/RadixSort.sml create mode 100644 tests/mpllib/Rat.sml create mode 100644 tests/mpllib/RecursiveStream.sml create mode 100644 tests/mpllib/SEQUENCE.sml create mode 100644 tests/mpllib/STREAM.sml create mode 100644 tests/mpllib/SampleSort.sml create mode 100644 tests/mpllib/SeqBasis.sml create mode 100644 tests/mpllib/SeqifiedMerge.sml create mode 100644 tests/mpllib/Seqifier.sml create mode 100644 tests/mpllib/Shuffle.sml create mode 100644 tests/mpllib/Signal.sml create mode 100644 tests/mpllib/StableMerge.sml create mode 100644 tests/mpllib/StableMergeLowSpan.sml create mode 100644 tests/mpllib/StableSort.sml create mode 100644 tests/mpllib/TFlatten.sml create mode 100644 tests/mpllib/TabFilterTree.sml create mode 100644 tests/mpllib/Tokenize.sml create mode 100644 tests/mpllib/Topology2D.sml create mode 100644 tests/mpllib/TreeMatrix.sml create mode 100644 tests/mpllib/Util.sml create mode 100644 tests/mpllib/compat/PosixReadFile.sml create mode 100644 tests/mpllib/compat/PosixWriteFile.sml create mode 100644 tests/mpllib/compat/mlton.mlb create mode 100644 tests/mpllib/compat/mlton.sml create mode 100644 tests/mpllib/compat/mpl-old.mlb create mode 100644 tests/mpllib/compat/mpl-old.sml create mode 100644 tests/mpllib/compat/mpl.mlb create mode 100644 tests/mpllib/compat/mpl.sml create mode 100644 tests/mpllib/sources.mlton.mlb create mode 100644 tests/mpllib/sources.mpl.mlb diff --git a/tests/mpllib/AdjacencyGraph.sml b/tests/mpllib/AdjacencyGraph.sml new file mode 100644 index 000000000..2e77447ab --- /dev/null +++ b/tests/mpllib/AdjacencyGraph.sml @@ -0,0 +1,390 @@ +functor AdjacencyGraph (Vertex: INTEGER) = +struct + + structure A = Array + structure AS = ArraySlice + + structure Vertex = + struct + type t = Vertex.int + open Vertex + val maxVal = toInt (valOf maxInt) + end + + structure VertexSubset = + struct + datatype h = SPARSE of Vertex.t Seq.t | DENSE of int Seq.t + type t = h * int + exception BadRep + + fun empty thresh = (SPARSE (Seq.empty()), thresh) + + fun size (vs, thresh) = + case vs of + SPARSE s => Seq.length s + | DENSE s => Seq.reduce op+ 0 s + + fun plugOnes s positions = + (Seq.foreach positions (fn (i, v) => AS.update (s, Vertex.toInt v, 1))) + + fun append (vs, threshold) s n = + case vs of + SPARSE ss => + if (Seq.length ss) + (Seq.length s) > threshold then + let + val dense_rep = Seq.tabulate (fn x => 0) n + val _ = plugOnes dense_rep ss + val _ = plugOnes dense_rep s + in + (DENSE (dense_rep), threshold) + end + else (SPARSE(Seq.append (ss, s)), threshold) + | DENSE ss => (plugOnes ss s; (DENSE ss, threshold)) + + fun sparse_to_dense vs n = + case vs of + SPARSE s => + let + val dense_rep = Seq.tabulate (fn x => 0) n + val _ = Seq.foreach s (fn (i, v) => AS.update (dense_rep, Vertex.toInt v, 1)) + in + DENSE (dense_rep) + end + | DENSE _ => raise BadRep + + fun dense_to_sparse vs = + case vs of + SPARSE _ => raise BadRep + | DENSE s => + let + val (offsets, total) = Seq.scan op+ 0 s + val sparse = ForkJoin.alloc total + val _ = Seq.foreach s (fn (i, v) => + if (v=1) then A.update (sparse, Seq.nth offsets i, Vertex.fromInt i) + else if (v = 0) then () + else raise BadRep + ) + in + SPARSE (AS.full sparse) + end + + fun from_sparse_rep s threshold n = + if (Seq.length s) < threshold then (SPARSE (s), threshold) + else (sparse_to_dense (SPARSE (s)) n, threshold) + + fun from_dense_rep s countopt threshold = + let + val count = + case countopt of + SOME x => x + | NONE => Seq.reduce op+ 0 s + val d = DENSE(s) + in + if count < threshold then (dense_to_sparse(d), threshold) + else (d, threshold) + end + end + + type vertex = Vertex.t + fun vertexNth s v = Seq.nth s (Vertex.toInt v) + fun vToWord v = Word64.fromInt (Vertex.toInt v) + + (* offsets, degrees, compact neighbors *) + type graph = (int Seq.t) * (int Seq.t) * (vertex Seq.t) + + fun degree G v = + let val (offsets, degrees, _) = G + in (vertexNth degrees v) + end + + fun neighbors G v = + let + val (offsets, _, nbrs) = G + in + Seq.subseq nbrs (vertexNth offsets v, degree G v) + end + + fun numVertices G = + let val (_, degrees, _) = G + in Seq.length degrees + end + + fun numEdges G = + let val (_, _, nbrs) = G + in Seq.length nbrs + end + + fun computeDegrees (N, M, offsets) = + AS.full (SeqBasis.tabulate 10000 (0, N) (fn i => + let + val off = Seq.nth offsets i + val nextOff = if i+1 < N then Seq.nth offsets (i+1) else M + val deg = nextOff - off + in + if deg < 0 then + raise Fail ("AdjacencyGraph.computeDegrees: vertex " ^ Int.toString i + ^ " has negative degree") + else + deg + end)) + + fun parse chars = + let + fun isNewline i = (Seq.nth chars i = #"\n") + + (* Computing newline positions takes up about half the time of parsing... + * Can we do this faster? *) + val nlPos = + AS.full (SeqBasis.filter 10000 (0, Seq.length chars) (fn i => i) isNewline) + val numLines = Seq.length nlPos + 1 + fun lineStart i = + if i = 0 then 0 else 1 + Seq.nth nlPos (i-1) + fun lineEnd i = + if i = Seq.length nlPos then Seq.length chars else Seq.nth nlPos i + fun line i = Seq.subseq chars (lineStart i, lineEnd i - lineStart i) + + val _ = + if numLines >= 3 then () + else raise Fail ("AdjacencyGraph: missing or incomplete header") + + val _ = + if Parse.parseString (line 0) = "AdjacencyGraph" then () + else raise Fail ("expected AdjacencyGraph header") + + fun tryParse thing lineNum = + let + fun whoops () = + raise Fail ("AdjacencyGraph: line " + ^ Int.toString (lineNum+1) + ^ ": error while parsing " ^ thing) + in + case (Parse.parseInt (line lineNum) handle _ => whoops ()) of + SOME x => if x >= 0 then x else whoops () + | NONE => whoops () + end + + val numVertices = tryParse "num vertices" 1 + val numEdges = tryParse "num edges" 2 + + val _ = + if numLines >= numVertices + numEdges + 3 then () + else raise Fail ("AdjacencyGraph: not enough offsets and/or edges to parse") + + val offsets = AS.full (SeqBasis.tabulate 1000 (0, numVertices) + (fn i => tryParse "edge offset" (3+i))) + + val neighbors = AS.full (SeqBasis.tabulate 1000 (0, numEdges) + (fn i => Vertex.fromInt (tryParse "neighbor" (3+numVertices+i)))) + in + (offsets, computeDegrees (numVertices, numEdges, offsets), neighbors) + end + + fun writeAsBinaryFormat g filename = + let + val (offsets, _, nbrs) = g + + val file = TextIO.openOut filename + val _ = TextIO.output (file, "AdjacencyGraphBin\n") + val _ = TextIO.closeOut file + + val file = BinIO.openAppend filename + fun w8 (w: Word8.word) = BinIO.output1 (file, w) + fun w64 (w: Word64.word) = + let + open Word64 + infix 2 >> andb + in + (* this will only work if Word64 = LargeWord, which is good. *) + w8 (Word8.fromLarge (w >> 0w56)); + w8 (Word8.fromLarge (w >> 0w48)); + w8 (Word8.fromLarge (w >> 0w40)); + w8 (Word8.fromLarge (w >> 0w32)); + w8 (Word8.fromLarge (w >> 0w24)); + w8 (Word8.fromLarge (w >> 0w16)); + w8 (Word8.fromLarge (w >> 0w8)); + w8 (Word8.fromLarge w) + end + fun wi (x: int) = w64 (Word64.fromInt x) + fun wv (v: vertex) = w64 (vToWord v) + in + wi (numVertices g); + wi (numEdges g); + Util.for (0, numVertices g) (fn i => wi (Seq.nth offsets i)); + Util.for (0, numEdges g) (fn i => wv (Seq.nth nbrs i)); + BinIO.closeOut file + end + + fun parseBin bytes = + let + val header = "AdjacencyGraphBin\n" + val header' = + if Seq.length bytes < String.size header then + raise Fail ("AdjacencyGraphBin: missing or incomplete header") + else + CharVector.tabulate (String.size header, fn i => + Char.chr (Word8.toInt (Seq.nth bytes i))) + val _ = + if header = header' then () + else raise Fail ("expected AdjacencyGraphBin header") + + val bytes = Seq.drop bytes (String.size header) + + (* this will only work if Word64 = LargeWord, which is good. *) + fun r64 i = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val off = i*8 + val w = Word8.toLarge (Seq.nth bytes off) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off+1))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off+2))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off+3))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off+4))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off+5))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off+6))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off+7))) + in + w + end + + fun ri i = Word64.toInt (r64 i) + fun rv i = Vertex.fromInt (ri i) + + val numVertices = ri 0 + val numEdges = ri 1 + + val offsets = + AS.full (SeqBasis.tabulate 10000 (0, numVertices) (fn i => ri (i+2))) + val nbrs = + AS.full (SeqBasis.tabulate 10000 (0, numEdges) (fn i => rv (i+2+numVertices))) + in + (offsets, computeDegrees (numVertices, numEdges, offsets), nbrs) + end + + fun parseFile path = + let + val file = TextIO.openIn path + + val h1 = "AdjacencyGraph\n" + val h2 = "AdjacencyGraphBin\n" + + val actualHeader = + TextIO.inputN (file, Int.max (String.size h1, String.size h2)) + in + TextIO.closeIn file; + + if String.isPrefix h1 actualHeader then + let + val (c, tm) = Util.getTime (fn _ => ReadFile.contentsSeq path) + val _ = print ("read file in " ^ Time.fmt 4 tm ^ "s\n") + val (graph, tm) = Util.getTime (fn _ => parse c) + val _ = print ("parsed graph in " ^ Time.fmt 4 tm ^ "s\n") + in + graph + end + else if String.isPrefix h2 actualHeader then + let + val (c, tm) = Util.getTime (fn _ => ReadFile.contentsBinSeq path) + val _ = print ("read file in " ^ Time.fmt 4 tm ^ "s\n") + val (graph, tm) = Util.getTime (fn _ => parseBin c) + val _ = print ("parsed graph in " ^ Time.fmt 4 tm ^ "s\n") + in + graph + end + else + raise Fail ("unknown header " ^ actualHeader) + end + + (* Useful as a sanity check for symmetrized graphs -- + * (every symmetrized graph has edge parity 0, but not all graphs with + * edge parity 0 are symmetrized!) *) + fun parityCheck g = + let + val (offsets, _, _) = g + val n = numVertices g + + fun canonical (u, v) = + if Vertex.< (u, v) then (u, v) else (v, u) + fun xorEdges ((u1, v1), (u2, v2)) = + (Word64.xorb (u1, u2), Word64.xorb (v1, v2)) + fun packEdge (u, v) = (vToWord u, vToWord v) + + val (p1, p2) = SeqBasis.reduce 100 xorEdges (0w0, 0w0) (0, n) (fn i => + let + val u = Vertex.fromInt i + val offset = Seq.nth offsets i + in + SeqBasis.reduce 1000 xorEdges (0w0, 0w0) (0, degree g u) (fn j => + packEdge (canonical (u, Seq.nth (neighbors g u) j))) + end) + + in + p1 = 0w0 andalso p2 = 0w0 + end + + fun fromSortedEdges sorted = + let + fun edgeInts (u, v) = (Vertex.toInt u, Vertex.toInt v) + val m = Seq.length sorted + val n = + 1 + SeqBasis.reduce 10000 Int.max ~1 (0, m) + (Int.max o edgeInts o Seq.nth sorted) + + fun k i = Vertex.toInt (#1 (Seq.nth sorted i)) + + val ends = Seq.tabulate (fn i => if i = n then m else 0) (n+1) + val _ = ForkJoin.parfor 10000 (0, m) (fn i => + if i = m-1 then + AS.update (ends, k i, m) + else if k i <> k (i+1) then + AS.update (ends, k i, i+1) + else ()) + val (offsets, _) = Seq.scan Int.max 0 ends + + fun off i = Seq.nth offsets (i+1) - Seq.nth offsets i + val degrees = Seq.tabulate off n + + val nbrs = Seq.map #2 sorted + in + (offsets, degrees, nbrs) + end + + fun dedupEdges edges = + let + val sorted = + Mergesort.sort (fn ((u1,v1), (u2,v2)) => + case Vertex.compare (u1, u2) of + EQUAL => Vertex.compare (v1, v2) + | other => other) edges + in + AS.full (SeqBasis.filter 5000 (0, Seq.length sorted) (Seq.nth sorted) + (fn i => i = 0 orelse Seq.nth sorted (i-1) <> Seq.nth sorted i)) + end + + fun randSymmGraph n d = + let + val m = Real.ceil (Real.fromInt n * Real.fromInt d / 2.0) + + fun makeEdge i = + let + val u = (2 * i) div d + val v = Util.hash i mod (n-1) + in + (Vertex.fromInt u, Vertex.fromInt (if v < u then v else v+1)) + end + + val bothWays = ForkJoin.alloc (2*m) + val _ = ForkJoin.parfor 1000 (0, m) (fn i => + let + val (u, v) = makeEdge i + in + A.update (bothWays, 2*i, (u,v)); + A.update (bothWays, 2*i+1, (v,u)) + end) + in + fromSortedEdges (dedupEdges (AS.full bothWays)) + end + +end diff --git a/tests/mpllib/AdjacencyInt.sml b/tests/mpllib/AdjacencyInt.sml new file mode 100644 index 000000000..936410d77 --- /dev/null +++ b/tests/mpllib/AdjacencyInt.sml @@ -0,0 +1,157 @@ +structure AdjInt = +struct + type 'a seq = 'a Seq.t + + structure G = AdjacencyGraph(Int) + structure AS = ArraySlice + open G.VertexSubset + + fun to_seq g (vs, threshold) = + case vs of + SPARSE s => s + | DENSE s => to_seq g (G.VertexSubset.dense_to_sparse vs, threshold) + + fun should_process_sparse g n = + let + val denseThreshold = G.numEdges g div 20 + val deg = Int.div (G.numEdges g, G.numVertices g) + val count = (1 + deg) * n + in + count <= denseThreshold + end + + fun edge_map_dense g vertices f h = + let + val inFrontier = vertices + val n = Seq.length vertices + val res = Seq.tabulate (fn _ => 0) n + + fun processVertex v = + if not (h v) then 0 + else + let + val neighbors = G.neighbors g v + fun loop i = + if i >= Seq.length neighbors then 0 else + let val u = Seq.nth neighbors i + in + if not (Seq.nth inFrontier u = 1) then + loop (i+1) + else + case f (u, v) of + NONE => loop (i+1) + | SOME x => (AS.update (res, x, 1); 1) + end + in + loop 0 + end + val count = SeqBasis.reduce 1000 op+ 0 (0, n) processVertex + in + (res, count) + end + + fun edge_map_sparse g vertices f h = + let + val n = Seq.length vertices + fun ui uidx = Seq.nth vertices uidx + val r = + SeqBasis.scan 1000 op+ 0 (0, n) (G.degree g o ui) + val (offsets, totalOutDegree) = (AS.full r, Array.sub (r, n)) + val store = ForkJoin.alloc totalOutDegree + val k = 100 + val numBlocks = 1 + (totalOutDegree-1) div k + fun map_block i = + let + val lo = i*k + val hi = Int.min((i+1)*k, totalOutDegree) + val ulo = + let + val a = BinarySearch.search (Int.compare) offsets lo + in + if (Seq.nth offsets a) > lo then a - 1 + else a + end + fun map_seq idx (u, uidx) count = + if idx >= hi then count + else if idx >= (Seq.nth offsets (uidx + 1)) then map_seq idx (ui (uidx + 1), uidx + 1) count + else + let + val v = Seq.nth (G.neighbors g u) (idx - (Seq.nth offsets uidx)) + in + if (h v) then + case f (u, v) of + SOME x => (Array.update (store, lo + count, x); map_seq (idx + 1) (u, uidx) (count + 1)) + | NONE => (map_seq (idx + 1) (u, uidx) count) + else + (map_seq (idx + 1) (u, uidx) count) + end + in + map_seq lo (ui ulo, ulo) 0 + end + val counts = SeqBasis.tabulate 1 (0, numBlocks) map_block + val outOff = SeqBasis.scan 10000 op+ 0 (0, numBlocks) (fn i => Array.sub (counts, i)) + val outSize = Array.sub (outOff, numBlocks) + val result = ForkJoin.alloc outSize + in + ForkJoin.parfor (totalOutDegree div (Int.max (outSize, 1))) (0, numBlocks) (fn i => + let + val soff = i * k + val doff = Array.sub (outOff, i) + val size = Array.sub (outOff, i+1) - doff + in + Util.for (0, size) (fn j => + Array.update (result, doff+j, Array.sub (store, soff+j))) + end); + (AS.full result) + end + + fun edge_map g (vs, threshold) (fpar, f) h = + case vs of + SPARSE s => + from_sparse_rep (edge_map_sparse g s fpar h) threshold (G.numVertices g) + | DENSE s => + let + val (res, count) = edge_map_dense g s f h + in + from_dense_rep res (SOME count) threshold + end + + fun vertex_foreach g (vs, threshold) f = + case vs of + SPARSE s => + Seq.foreach s (fn (i, u) => f u) + | DENSE s => + Seq.foreach s (fn (i, b) => if (b = 1) then (f i) else ()) + + fun vertex_map_ g (vs, threshold) f = + case vs of + SPARSE s => + let + val s' = + AS.full (SeqBasis.tabFilter 1000 (0, Seq.length s) + (fn i => + let + val u = Seq.nth s i + val b = f u + in + if b then SOME u + else NONE + end + )) + in + (from_sparse_rep s' threshold (G.numVertices g)) + end + | DENSE s => + let + val res = + Seq.map (fn i => if (Seq.nth s i = 1) andalso f i then 1 else 0) s + in + from_dense_rep res NONE threshold + end + + + fun vertex_map g vs f needOut = + if needOut then vertex_map_ g vs f + else (vertex_foreach g vs; vs) + +end diff --git a/tests/mpllib/ArraySequence.sml b/tests/mpllib/ArraySequence.sml new file mode 100644 index 000000000..952e5b55a --- /dev/null +++ b/tests/mpllib/ArraySequence.sml @@ -0,0 +1,281 @@ +structure ArraySequence = +struct + + val for = Util.for + val par = ForkJoin.par + val parfor = ForkJoin.parfor + val alloc = ForkJoin.alloc + + + val GRAN = 5000 + + + structure A = + struct + open Array + type 'a t = 'a array + fun nth a i = sub (a, i) + end + + + structure AS = + struct + open ArraySlice + type 'a t = 'a slice + fun nth a i = sub (a, i) + end + + + type 'a t = 'a AS.t + type 'a seq = 'a t + + (* for compatibility across all sequence implementations *) + fun fromArraySeq s = s + fun toArraySeq s = s + fun force s = s + + + val nth = AS.nth + val length = AS.length + + fun empty () = AS.full (A.fromList []) + fun singleton x = AS.full (A.array (1, x)) + val $ = singleton + fun toString f s = + "<" ^ String.concatWith "," (List.tabulate (length s, f o nth s)) ^ ">" + + + fun fromArray a = AS.full a + + + fun fromList l = AS.full (A.fromList l) + val % = fromList + fun toList s = + SeqBasis.foldl (fn (list, x) => x :: list) [] + (0, length s) (fn i => nth s (length s - i - 1)) + + + fun subseq s (i, k) = + AS.subslice (s, i, SOME k) + fun take s n = subseq s (0, n) + fun drop s n = subseq s (n, length s - n) + fun first s = nth s 0 + fun last s = nth s (length s - 1) + + fun tabulate f n = + AS.full (SeqBasis.tabulate GRAN (0, n) f) + + + fun map f s = + tabulate (f o nth s) (length s) + + + fun mapIdx f s = + tabulate (fn i => f (i, nth s i)) (length s) + + + fun enum s = + mapIdx (fn xx => xx) s + + + fun zipWith f (s, t) = + tabulate + (fn i => f (nth s i, nth t i)) + (Int.min (length s, length t)) + + + fun zipWith3 f (s1, s2, s3) = + tabulate + (fn i => f (nth s1 i, nth s2 i, nth s3 i)) + (Int.min (length s1, Int.min (length s2, length s3))) + + + fun zip (s, t) = + zipWith (fn xx => xx) (s, t) + + + fun rev s = + tabulate (fn i => nth s (length s - 1 - i)) (length s) + + + (** TODO: make faster *) + fun fromRevList list = rev (fromList list) + + + fun append (s, t) = + let + val (ns, nt) = (length s, length t) + fun ith i = if i < ns then nth s i else nth t (i-ns) + in + tabulate ith (ns + nt) + end + + + fun append3 (a, b, c) = + let + val (na, nb, nc) = (length a, length b, length c) + fun ith i = + if i < na then + nth a i + else if i < na + nb then + nth b (i - na) + else + nth c (i - na - nb) + in + tabulate ith (na + nb + nc) + end + + + fun foldl f b s = + SeqBasis.foldl f b (0, length s) (nth s) + + fun foldr f b s = + SeqBasis.foldr f b (0, length s) (nth s) + + fun iterate f b s = + SeqBasis.foldl f b (0, length s) (nth s) + + + fun iteratePrefixes f b s = + let + val prefixes = alloc (length s) + fun g ((i, b), a) = + let + val _ = A.update (prefixes, i, b) + in + (i+1, f (b, a)) + end + val (_, r) = iterate g (0, b) s + in + (AS.full prefixes, r) + end + + + fun reduce f b s = + SeqBasis.reduce GRAN f b (0, length s) (nth s) + + + fun scan f b s = + let + val p = AS.full (SeqBasis.scan GRAN f b (0, length s) (nth s)) + in + (take p (length s), nth p (length s)) + end + + fun scanWithTotal f b s = + AS.full (SeqBasis.scan GRAN f b (0, length s) (nth s)) + + fun scanIncl f b s = + let + val p = AS.full (SeqBasis.scan GRAN f b (0, length s) (nth s)) + in + drop p 1 + end + + + fun filter p s = + (* Assumes that the predicate p is pure *) + AS.full (SeqBasis.filter GRAN (0, length s) (nth s) (p o nth s)) + + fun filterSafe p s = + (* Does not assume that the predicate p is pure *) + AS.full (SeqBasis.tabFilter GRAN (0, length s) (fn i => if p (nth s i) then SOME (nth s i) else NONE)) + + fun filterIdx p s = + AS.full (SeqBasis.filter GRAN (0, length s) (nth s) (fn i => p (i, nth s i))) + + fun filtermap (p: 'a -> bool) (f:'a -> 'b) (s: 'a t): 'b t = + AS.full (SeqBasis.filter GRAN (0, length s) (fn i => f (nth s i)) (p o nth s)) + + + fun mapOption f s = + AS.full (SeqBasis.tabFilter GRAN (0, length s) (f o nth s)) + + + fun equal eq (s, t) = + length s = length t andalso + SeqBasis.reduce GRAN (fn (a, b) => a andalso b) true (0, length s) + (fn i => eq (nth s i, nth t i)) + + + fun inject (s, updates) = + let + val result = map (fn x => x) s + in + parfor GRAN (0, length updates) (fn i => + let + val (idx, r) = nth updates i + in + AS.update (result, idx, r) + end); + + result + end + + + fun applyIdx s f = + parfor GRAN (0, length s) (fn i => f (i, nth s i)) + + + fun foreach s f = applyIdx s f + + + fun indexSearch (start, stop, offset: int -> int) k = + case stop-start of + 0 => + raise Fail "ArraySequence.indexSearch: should not have hit 0" + | 1 => + start + | n => + let + val mid = start + (n div 2) + in + if k < offset mid then + indexSearch (start, mid, offset) k + else + indexSearch (mid, stop, offset) k + end + + + fun flatten s = + let + val offsets = SeqBasis.scan GRAN op+ 0 (0, length s) (length o nth s) + fun offset i = A.nth offsets i + val total = offset (length s) + val result = alloc total + + val blockSize = GRAN + val numBlocks = Util.ceilDiv total blockSize + in + parfor 1 (0, numBlocks) (fn blockIdx => + let + val lo = blockIdx * blockSize + val hi = Int.min (lo + blockSize, total) + + val firstOuterIdx = indexSearch (0, length s, offset) lo + val firstInnerIdx = lo - offset firstOuterIdx + + (** i = outer index + * j = inner index + * k = output index, ranges from [lo] to [hi] + *) + fun loop i j k = + if k >= hi then () else + let + val inner = nth s i + val numAvailableHere = length inner - j + val numRemainingInBlock = hi - k + val numHere = Int.min (numAvailableHere, numRemainingInBlock) + in + for (0, numHere) (fn z => A.update (result, k+z, nth inner (j+z))); + loop (i+1) 0 (k+numHere) + end + in + loop firstOuterIdx firstInnerIdx lo + end); + + AS.full result + end + + +end diff --git a/tests/mpllib/AugMap.sml b/tests/mpllib/AugMap.sml new file mode 100644 index 000000000..78b907c1f --- /dev/null +++ b/tests/mpllib/AugMap.sml @@ -0,0 +1,509 @@ +datatype scheme = WB of real + +signature Aug = +sig + type key + type value (* can make this polymorhpic *) + type aug + val compare : key * key -> order + val g : key * value -> aug + val f : aug * aug -> aug + val id : aug + val balance : scheme + val debug : key * value * aug -> string +end + +signature AugMap = +sig + exception Assert + structure T : Aug + datatype am = Leaf | Node of {l : am, k : T.key, v : T.value, a : T.aug, r : am, size : int} + val empty : unit -> am + val size : am -> int + val find : am -> T.key -> T.value option + val union : am -> am -> (T.value * T.value -> T.value) -> am + val insert : am -> T.key -> T.value -> (T.value * T.value -> T.value) -> am + val multi_insert : am -> ((T.key * T.value) Seq.t) -> (T.value * T.value -> T.value) -> am + val mapReduce : am -> (T.key * T.value -> 'b) -> ('b * 'b -> 'b)-> 'b -> 'b + val join : am -> T.key -> T.value -> am -> am + val filter : (T.key * T.value -> bool) -> am -> am + val build : ((T.key * T.value) Seq.t) -> int -> int -> am + val aug_left : am -> T.key -> T.aug + val aug_filter : am -> (T.aug -> bool) -> am + val aug_range : am -> T.key -> T.key -> T.aug + val aug_project : (T.aug -> 'a) -> ('a * 'a -> 'a) -> am -> T.key -> T.key -> 'a + val up_to : am -> T.key -> am + val print_tree : am -> string -> unit + val singleton : T.key -> T.value -> am +end + +functor PAM (T: Aug) : AugMap = +struct + type key = T.key + type value = T.value + type aug = T.aug + + (* how to add the metric for balancing? *) + datatype am = Leaf | Node of {l : am, k : key, v : value, a : aug, r : am, size : int} + exception Assert + structure T = T + (* | FatLeaf (array ) in order traversal gran + leaves*) + (* joinG that takes the grain and join which does something itself *) + (* maybe not store augmented values for thin leaves*) + (* fat leaves trick -- use leaves of arrays *) + val gran = 100 + + fun weight m = + case m of + Leaf => 0 + | Node n => #size n + + fun size m = + case m of + Leaf => 0 + | Node n => #size n + + fun aug_val m = + case m of + Leaf => T.id + | Node {a, ...} => a + + structure WBST = + struct + + fun leaf_weight () = 0 + + fun singleton_weight () = 1 + + val ratio = + case T.balance of + WB (x) => x / (1.0 - x) + + fun size_heavy s1 s2 = + ratio * (Real.fromInt s1) > (Real.fromInt s2) + + fun heavy (m1 : am, m2 : am) = size_heavy (weight m1) (weight m2) + + fun like s1 s2 = not (size_heavy s1 s2) andalso not (size_heavy s2 s1) + + fun compose m1 k v m2 = + let + val new_size = (size m1) + (size m2) + 1 + val a = T.f ((aug_val m1), T.f (T.g(k, v), (aug_val m2))) + in + Node {l = m1, k = k, v = v, a = a, r = m2, size = new_size} + end + + fun rotateLeft m = + case m of + Leaf => m + | Node {l, k, v, a, r, size} => + case r of + Leaf => m + | Node {l = rl, k = rk, v = rv, r = rr, ...} => + let + val left = compose l k v rl + in + compose left rk rv rr + end + + fun rotateRight m = + case m of + Leaf => m + | Node {l, k, v, a, r, size} => + case l of + Leaf => m + | Node {l = ll, k = lk, v = lv, r = lr, ...} => + let + val right = compose lr k v r + in + compose ll lk lv right + end + + fun joinLeft (m1 : am) k v (m2 : am) = + let + val w1 = weight m1 + val w2 = weight m2 + in + if (like w1 w2) then compose m1 k v m2 + else + case m2 of + Leaf => compose m1 k v m2 + | Node {l, k = kr, v = vr, r, size, ...} => + let + val t' = joinLeft m1 k v l + val (wlt', wrt') = case t' of + Leaf => raise Assert + | Node {l, r, ...} => ((weight l), (weight r)) + val wr = weight r + in + if like (weight t') wr then compose t' kr vr r + else if like wrt' wr andalso like wlt' (wrt' + wr) then + rotateRight (compose t' kr vr r) + else rotateRight (compose (rotateLeft t') kr vr r) + end + end + + fun joinRight m1 k v m2 = + let + val w1 = weight m1 + val w2 = weight m2 + in + if like w1 w2 then compose m1 k v m2 + else + case m1 of + Leaf => compose m1 k v m2 + | Node {l, k = kl, v = vl, r, size, ...} => + let + val t' = joinRight r k v m2 + val (wlt', wrt') = case t' of + Leaf => raise Assert + | Node n => ((weight (#l n)), (weight (#r n))) + val wl = weight l + in + if like wl (weight t') then compose l kl vl t' + else if like wl wlt' andalso like (wl + wlt') wrt' then + rotateLeft (compose l kl vl t') + else rotateLeft (compose l kl vl (rotateRight t')) + end + end + + fun join m1 k v m2 = + if heavy(m1, m2) then joinRight m1 k v m2 + else if heavy(m2, m1) then joinLeft m1 k v m2 + else compose m1 k v m2 + + end + + fun par (f1, f2) = ForkJoin.par (f1, f2) + + fun eval (inpar, f1, f2) = + if inpar then ForkJoin.par (f1, f2) + else (f1(), f2()) + + fun join m1 k v m2 = + case T.balance of + WB _ => WBST.join m1 k v m2 + + fun join2 m1 m2 = + let + fun splitLast {l, k, v, a, r, size, ...} = + case r of + Leaf => (l, k, v) + | Node n => + let + val (m', k', v') = splitLast n + in + (join l k v m', k', v') + end + in + case m1 of + Leaf => m2 + | Node n => + let + val (m', k', v') = splitLast n (*get the greatest element in m1*) + in + join m' k' v' m2 + end + end + + fun empty () = Leaf + + fun singleton k v = + Node {l = Leaf, k = k, v = v, a = T.g (k, v), r = Leaf, size = 1} + + fun build_sorted s i j = + if i = j then empty() + else if i + 1 = j then singleton (#1 (Seq.nth s i)) (#2 (Seq.nth s i)) + else + let + val m = i + Int.div ((j - i), 2) + val (l, r) = if (j - i) > 1000 then eval (true, fn _ => build_sorted s i m, fn _ => build_sorted s (m + 1) j) + else (build_sorted s i m, build_sorted s (m + 1) j) + val (x, y) = Seq.nth s m + in + join l x y r + end + + fun find m k = + case m of + Leaf => NONE + | Node ({l, k = kr, v, r, ...}) => + case T.compare(k, kr) of + LESS => find l k + | EQUAL => SOME v + | GREATER => find r k + + fun insert m k v h = + case m of + Leaf => singleton k v + | Node {l, k = kr, v = vr, r, ...} => + case T.compare(k, kr) of + EQUAL => join l k (h (v, vr)) r + | LESS => join (insert l k v h) kr vr r + | GREATER => join l kr vr (insert r k v h) + + fun key_equal k1 k2 = T.compare (k1, k2) = EQUAL + + fun multi_insert m s h = + let + val ss = Mergesort.sort (fn (i, j) => T.compare(#1 i, #1 j)) s + fun insert_helper m' i j = + if i >= j then + m' + else if (j - i) < gran then + (* this v/s the else branch with recursive calls done sequentially *) + SeqBasis.foldl (fn (m'', (k, v)) => insert m'' k v h) m' (i, j) (Seq.nth ss) + else + case m' of + Leaf => build_sorted s i j + | Node {l, k = kr, v = vr, r, ...} => + let + fun bin_search k i j = + (* inv i < j, all k inside [i, j) *) + (* returns [lk, rk) every element in the range has key = k *) + if (i + 1 = j) then + if key_equal k (#1 (Seq.nth ss i)) then (i, j) + else (i + 1, j) + else + let + val mid = i + Int.div ((j - i), 2) (* i < mid < j*) + val mid_val = Seq.nth ss mid + fun until_boundary b1 b2 e i f = + if i < b1 then b1 + else if i >= b2 then b2 - 1 + else if (e (Seq.nth ss i)) then + until_boundary b1 b2 e (f i) f + else i + in + case T.compare (k, #1 mid_val) of + LESS => bin_search k i mid + | EQUAL => + let + val bound_func = until_boundary i j (fn t => key_equal k (#1 t)) mid + in + (bound_func (fn i => i - 1), 1 + bound_func (fn i => i + 1)) + end + | GREATER => bin_search k mid j + end + val (lk, rk) = bin_search kr i j + val (l, r) = par (fn _ => insert_helper l i lk, fn _ => insert_helper r rk j) + val nvr = SeqBasis.foldl (fn (v', (k, v)) => h (v, v')) vr (lk, rk) (Seq.nth ss) + in + join l kr nvr r + end + in + insert_helper m 0 (Seq.length ss) + end + + fun split m k = + case m of + Leaf => (Leaf, NONE, Leaf) + | Node {l, k = kr, v = vr, r, ...} => + case T.compare(k, kr) of + EQUAL => (l, SOME vr, r) + | LESS => + let + val (ll, so, lr) = split l k + in + (ll, so, join lr kr vr r) + end + | GREATER => + let + val (rl, so, rr) = split r k + in + (join l kr vr rl, so, rr) + end + + (* this is not efficient because each join is more expensive *) + (* fun split_tail_rec m k = + let + fun split_helper m accl accr = + case m of + Leaf => (accl, NONE, accr) + | Node {l, k = kr, v = vr, r, ...} => + case T.compare(k, kr) of + EQUAL => (join2 l accl, SOME vr, join2 r accr) + | LESS => split_helper l accl (join2 (singleton kr vr) (join2 r accr)) + | GREATER => split_helper r (join2 (singleton kr vr) (join2 accl l)) accr + in + split_helper m (empty()) (empty()) + end *) + + fun union m1 m2 h = + case (m1, m2) of + (Leaf, _) => m2 + | (_, Leaf) => m1 + | (Node {l = l1, k = k1, v = v1, r = r1, size, ...}, m2) => + let + val (l2, so, r2) = split m2 k1 + val new_val = case so of + NONE => v1 + | SOME v' => h(v1, v') + + val (ul, ur) = eval(size > gran, fn _ => union l1 l2 h, fn _ => union r1 r2 h) + in + join ul k1 new_val ur + end + + fun filter h m = + case m of + Leaf => m + | Node {l, k, v, r, ...} => + let + val (l', r') = par(fn _ => filter h l, fn _ => filter h r) + in + if h(k, v) then join l' k v r' + else join2 l' r' + end + + fun mapReduce m g f id = + case m of + Leaf => id + | Node {l, k, v, r, size, ...} => + let + val (l', r') = eval(size > gran, fn _ => mapReduce l g f id, fn _ => mapReduce r g f id) + in + f(f(l', g(k, v)), r') + end + + fun build ss i j = + let + + val t0 = Time.now () + (* val _ = Mergesort.sortInPlace (fn (i, j) => T.compare(#1 i, #1 j)) ss *) + + val idx = Seq.tabulate (fn i => i) (Seq.length ss) + fun cmp (x, y) = T.compare (#1 (Seq.nth ss x), #1 (Seq.nth ss y)) + + val idx' = idx + (* val idx' = Dedup.dedup (fn (i, j) => cmp (i, j) = EQUAL) (fn i => Util.hash64 (Word64.fromInt i)) (fn i =>Util.hash64 (Word64.fromInt (i + 1))) idx *) + + val _ = Mergesort.sortInPlace cmp idx' + + val ss' = Seq.map (Seq.nth ss) idx' + + val t1 = Time.now() + val _ = print ("sorting time = " ^ Time.fmt 4 (Time.-(t1, t0)) ^ "s\n") + val t2 = Time.now() + val r = build_sorted ss' i (Seq.length ss') + val t3 = Time.now() + val _ = print ("from sorted time = " ^ Time.fmt 4 (Time.-(t3, t2)) ^ "s\n") + in + r + end + + fun up_to m k = + case m of + Leaf => m + | Node {l, k = k', r, v, ...} => + case T.compare (k, k') of + LESS => up_to l k + | EQUAL => join2 l (singleton k' v) + | GREATER => join l k' v (up_to r k) + + fun aug_filter m h = + case m of + Leaf => m + | Node {l, k, v, r, a, size, ...} => + let + val (l', r') = eval (size > gran, fn _ => aug_filter l h, fn _ => aug_filter r h) + in + if h (T.g (k, v)) then join l' k v r' + else join2 l' r' + end + + (* case m of + Leaf => T.id + | Node {l, k, v, r, size, a, ...} => + case (T.compare (k, k1), T.compare (k, k2)) of + (LESS, _) => aug_range r k1 k2 + | (EQUAL, _) => T.f ((aug_range r k1 k2), T.g (k, v)) + | (_, EQUAL) => T.f ((aug_range l k1 k2), T.g (k, v)) + | (_, GREATER) => aug_range l k1 k2 + | (GREATER, LESS) => + let + val (lval, rval) = eval (size > gran, fn _ => aug_range l k1 k2, fn _ => aug_range r k1 k2) + in + T.f (lval, T.f (T.g (k, v), rval)) + end *) + + fun aug_project (ga : T.aug -> 'a) fa m (k1: T.key) (k2 : T.key) = + let + val default_val : 'a = ga T.id + fun until_root_in_range m k1 k2 = + case m of + Leaf => m + | Node {l, k, r, ...} => + case (T.compare (k, k1), T.compare (k, k2)) of + (LESS, _) => until_root_in_range r k1 k2 + | (_, GREATER) => until_root_in_range l k1 k2 + | _ => m + + fun compose_map_kv m k v = fa (ga (aug_val m), ga (T.g(k, v))) + + fun compose_kv_map k v m = fa (ga (T.g(k, v)), ga (aug_val m)) + + fun aug_proj_left m k acc = + case m of + Leaf => acc + | Node {l, k = k', v, r, ...} => + case T.compare (k, k') of + LESS => aug_proj_left l k acc + | EQUAL => fa (acc, compose_map_kv l k v) + | GREATER => aug_proj_left r k (fa (acc, compose_map_kv l k v)) + + fun aug_proj_right m k acc = + case m of + Leaf => acc + | Node {l, k = k', v, r, ...} => + case T.compare (k, k') of + LESS => aug_proj_right l k (fa (acc, compose_kv_map k v r)) + | EQUAL => fa (acc, compose_kv_map k v r) + | GREATER => aug_proj_right r k acc + + fun aug_proj m = + let + val sm = until_root_in_range m k1 k2 + in + case sm of + Leaf => default_val + | Node {l, k, v, r, ...} => + let + val ra = aug_proj_left r k2 default_val + val la = aug_proj_right l k1 default_val + val ka = ga (T.g (k, v)) + in + fa (fa (la, ka), ra) + end + end + in + aug_proj m + end + + fun aug_range m k1 k2 = aug_project (fn x => x) (T.f) m k1 k2 + + fun aug_left m k = + case m of + Leaf => T.id + | Node {l, k = k', v, r, ...} => + case T.compare(k, k') of + LESS => aug_left l k + | EQUAL => T.f (aug_val l, T.g(k, v)) + | GREATER => T.f (aug_val l, T.f (T.g(k, v), aug_left r k)) + + fun print_tree m indent = + case m of + Leaf => print (indent ^ "Leaf") + | Node {l, k, v, r, a, size} => + let + val _ = print "(" + val _ = print_tree l (indent^"") + val _ = print (", " ^(T.debug (k, v, a))) + val _ = print (", weight = " ^ (Int.toString size) ^ ",") + val _ = print_tree r (indent^"") + in + print ")" + end + +end diff --git a/tests/mpllib/Benchmark.sml b/tests/mpllib/Benchmark.sml new file mode 100644 index 000000000..e32a05f55 --- /dev/null +++ b/tests/mpllib/Benchmark.sml @@ -0,0 +1,83 @@ +structure Benchmark = +struct + + fun getTimes msg n f = + let + fun loop tms n = + let + val (result, tm) = Util.getTime f + in + print (msg ^ " " ^ Time.fmt 4 tm ^ "s\n"); + + if n <= 1 then (result, List.rev (tm :: tms)) + else loop (tm :: tms) (n - 1) + end + in + loop [] n + end + + fun run msg f = + let + val warmup = Time.fromReal (CommandLineArgs.parseReal "warmup" 0.0) + val rep = CommandLineArgs.parseInt "repeat" 1 + val _ = if rep >= 1 then () else Util.die "-repeat N must be at least 1" + + val _ = print ("warmup " ^ Time.fmt 4 warmup ^ "\n") + val _ = print ("repeat " ^ Int.toString rep ^ "\n") + + fun warmupLoop startTime = + if Time.>= (Time.- (Time.now (), startTime), warmup) then + () (* warmup done! *) + else + let val (_, tm) = Util.getTime f + in print ("warmup_run " ^ Time.fmt 4 tm ^ "s\n"); warmupLoop startTime + end + + val _ = + if Time.<= (warmup, Time.zeroTime) then + () + else + ( print ("====== WARMUP ======\n" ^ msg ^ "\n") + ; warmupLoop (Time.now ()) + ; print ("==== END WARMUP ====\n") + ) + + val _ = print (msg ^ "\n") + val s0 = RuntimeStats.get () + val t0 = Time.now () + val (result, tms) = getTimes "time" rep f + val t1 = Time.now () + val s1 = RuntimeStats.get () + val endToEnd = Time.- (t1, t0) + + fun stdev rtms avg = + let + val SS = List.foldr (fn (a, b) => (a - avg) * (a - avg) + b) 0.0 rtms + val sample = Real.fromInt (List.length rtms - 1) + in + Math.sqrt (SS / sample) + end + + val rtms = List.map Time.toReal tms + val total = List.foldl Time.+ Time.zeroTime tms + val avg = Time.toReal total / (Real.fromInt rep) + val std = if rep > 1 then stdev rtms avg else 0.0 + val tmax = Time.toReal + (List.foldl (fn (a, M) => if Time.< (a, M) then M else a) (List.hd tms) + (List.tl tms)) + val tmin = Time.toReal + (List.foldl (fn (a, m) => if Time.< (a, m) then a else m) (List.hd tms) + (List.tl tms)) + in + print "\n"; + print ("average " ^ Real.fmt (StringCvt.FIX (SOME 4)) avg ^ "s\n"); + print ("minimum " ^ Real.fmt (StringCvt.FIX (SOME 4)) tmin ^ "s\n"); + print ("maximum " ^ Real.fmt (StringCvt.FIX (SOME 4)) tmax ^ "s\n"); + print ("std dev " ^ Real.fmt (StringCvt.FIX (SOME 4)) std ^ "s\n"); + print ("total " ^ Time.fmt 4 total ^ "s\n"); + print ("end-to-end " ^ Time.fmt 4 endToEnd ^ "s\n"); + RuntimeStats.benchReport {before = s0, after = s1}; + result + end + +end diff --git a/tests/mpllib/BinarySearch.sml b/tests/mpllib/BinarySearch.sml new file mode 100644 index 000000000..6b07ef8b5 --- /dev/null +++ b/tests/mpllib/BinarySearch.sml @@ -0,0 +1,81 @@ +structure BinarySearch: +sig + type 'a seq = 'a ArraySlice.slice + val search: ('a * 'a -> order) -> 'a seq -> 'a -> int + + (* count the number of elements strictly less than the target *) + val countLess: ('a * 'a -> order) -> 'a seq -> 'a -> int + + (** Sometimes, you aren't looking for a particular element, but instead just + * some position in the sequence. The function ('a -> order) is used here to + * point towards the target position. + * + * Note that this is more general than the plain `search` function, because + * we can implement `search` in terms of `searchPosition`: + * fun search cmp s x = searchPosition s (fn y => cmp (x, y)) + *) + val searchPosition: 'a seq -> ('a -> order) -> int +end = +struct + + type 'a seq = 'a ArraySlice.slice + + fun search cmp s x = + let + fun loop lo hi = + case hi - lo of + 0 => lo + | n => + let + val mid = lo + n div 2 + val pivot = ArraySlice.sub (s, mid) + in + case cmp (x, pivot) of + LESS => loop lo mid + | EQUAL => mid + | GREATER => loop (mid+1) hi + end + in + loop 0 (ArraySlice.length s) + end + + + fun countLess cmp s x = + let + fun loop lo hi = + case hi - lo of + 0 => lo + | n => + let + val mid = lo + n div 2 + val pivot = ArraySlice.sub (s, mid) + in + case cmp (x, pivot) of + GREATER => loop (mid+1) hi + | _ => loop lo mid + end + in + loop 0 (ArraySlice.length s) + end + + + fun searchPosition s compareTargetAgainst = + let + fun loop lo hi = + case hi - lo of + 0 => lo + | n => + let + val mid = lo + n div 2 + val pivot = ArraySlice.sub (s, mid) + in + case compareTargetAgainst pivot of + LESS => loop lo mid + | EQUAL => mid + | GREATER => loop (mid+1) hi + end + in + loop 0 (ArraySlice.length s) + end + +end diff --git a/tests/mpllib/CheckSort.sml b/tests/mpllib/CheckSort.sml new file mode 100644 index 000000000..ba9cc112d --- /dev/null +++ b/tests/mpllib/CheckSort.sml @@ -0,0 +1,69 @@ +functor CheckSort + (val sort_func: ('a * 'a -> order) -> 'a Seq.t -> 'a Seq.t): +sig + datatype 'a error = + LengthChange (* output length differs from input *) + | MissingElem of int (* index of missing input element *) + | Inversion of int * int (* indices of two elements not in order in output *) + | Unstable of int * int (* indices of two equal swapped elements *) + + val check: + { input: 'a Seq.t + , compare: 'a * 'a -> order + , check_stable: bool + } + -> 'a error option (* NONE if correct *) +end = +struct + + structure DS = DelayedSeq + + type 'a seq = 'a Seq.t + + datatype 'a error = + LengthChange (* output length differs from input *) + | MissingElem of int (* index of missing input element *) + | Inversion of int * int (* indices of two elements not in order in output *) + | Unstable of int * int (* indices of two equal swapped elements *) + + type 'a check_input = + { input: 'a seq + , compare: 'a * 'a -> order + , check_stable: bool + } + + fun check ({input, compare, check_stable}: 'a check_input) = + let + val n = Seq.length input + val input' = Seq.mapIdx (fn (i, x) => (i, x)) input + fun compare' ((i1, k1), (i2, k2)) = compare (k1, k2) + + val result = sort_func compare' input' + + val noElemsMissing: bool = + DS.reduce (fn (a, b) => a andalso b) true + (DS.inject + ( DS.tabulate (fn _ => false) n + , DS.map (fn (i, k) => (i, true)) (DS.fromArraySeq result) + )) + + fun adjacentPairProblem ((i1, k1), (i2, k2)) = + case compare (k1, k2) of + LESS => NONE + | GREATER => SOME (Inversion (i1, i2)) + | EQUAL => + if check_stable andalso i1 > i2 then + SOME (Unstable (i1, i2)) + else + NONE + + fun problemAt i = + adjacentPairProblem (Seq.nth result i, Seq.nth result (i+1)) + fun isProblem i = Option.isSome (problemAt i) + in + case FindFirst.findFirst 1000 (0, n-1) isProblem of + NONE => NONE + | SOME i => problemAt i + end + +end diff --git a/tests/mpllib/ChunkedTreap.sml b/tests/mpllib/ChunkedTreap.sml new file mode 100644 index 000000000..8b7088ae0 --- /dev/null +++ b/tests/mpllib/ChunkedTreap.sml @@ -0,0 +1,186 @@ +functor ChunkedTreap( + structure A: + sig + type 'a t + type 'a array = 'a t + val tabulate: int * (int -> 'a) -> 'a t + val sub: 'a t * int -> 'a + val subseq: 'a t -> {start: int, len: int} -> 'a t + val update: 'a t * int * 'a -> 'a t + val length: 'a t -> int + end + + structure Key: + sig + type t + type key = t + val comparePriority: key * key -> order + val compare: key * key -> order + val toString: key -> string + end + + val leafSize: int +) : +sig + type 'a t + type 'a bst = 'a t + type key = Key.t + + val empty: unit -> 'a bst + val singleton: key * 'a -> 'a bst + + val toString: 'a bst -> string + + val size: 'a bst -> int + val join: 'a bst * 'a bst -> 'a bst + val updateKey: 'a bst -> (key * 'a) -> 'a bst + val lookup: 'a bst -> key -> 'a option +end = +struct + + type key = Key.t + + datatype 'a t = + Empty + | Chunk of (key * 'a) A.t + | Node of {left: 'a t, right: 'a t, key: key, value: 'a, size: int} + + type 'a bst = 'a t + + fun toString t = + case t of + Empty => "()" + | Chunk arr => + "(" ^ String.concatWith " " + (List.tabulate (A.length arr, fn i => Key.toString (#1 (A.sub (arr, i))))) + ^ ")" + | Node {left, key, right, ...} => + "(" ^ toString left ^ " " ^ Key.toString key ^ " " ^ toString right ^ ")" + + fun empty () = Empty + + fun singleton (k, v) = + Chunk (A.tabulate (1, fn _ => (k, v))) + + fun size Empty = 0 + | size (Chunk a) = A.length a + | size (Node {size=n, ...}) = n + + fun makeChunk arr {start, len} = + if len = 0 then + Empty + else + Chunk (A.subseq arr {start = start, len = len}) + + fun expose Empty = NONE + + | expose (Node {key, value, left, right, ...}) = + SOME (left, key, value, right) + + | expose (Chunk arr) = + let + val n = A.length arr + val half = n div 2 + val left = makeChunk arr {start = 0, len = half} + val right = makeChunk arr {start = half+1, len = n-half-1} + val (k, v) = A.sub (arr, half) + in + SOME (left, k, v, right) + end + + + fun nth t i = + case t of + Empty => raise Fail "ChunkedTreap.nth Empty" + | Chunk arr => A.sub (arr, i) + | Node {left, right, key, value, ...} => + if i < size left then + nth left i + else if i = size left then + (key, value) + else + nth right (i - size left - 1) + + + fun makeNode left (k, v) right = + let + val n = size left + size right + 1 + val node = Node {left=left, right=right, key=k, value=v, size=n} + in + if n <= leafSize then + Chunk (A.tabulate (n, nth node)) + else + node + end + + + fun join (t1, t2) = + if size t1 + size t2 = 0 then + Empty + else if size t1 + size t2 <= leafSize then + Chunk (A.tabulate (size t1 + size t2, fn i => + if i < size t1 then nth t1 i else nth t2 (i - size t1))) + else + case (expose t1, expose t2) of + (NONE, _) => t2 + | (_, NONE) => t1 + | (SOME (l1, k1, v1, r1), SOME (l2, k2, v2, r2)) => + case Key.comparePriority (k1, k2) of + GREATER => makeNode l1 (k1, v1) (join (r1, t2)) + | _ => makeNode (join (t1, l2)) (k2, v2) r2 + + local + fun fail k = + raise Fail ("ChunkedTreap.updateKey: key not found: " ^ Key.toString k) + in + fun updateKey t (k, v) = ((*print ("updateKey " ^ toString t ^ " (" ^ Key.toString k ^ ", ...)" ^ "\n");*) + case t of + Empty => fail k + + | Chunk arr => + let + val n = A.length arr + fun loop i = + if i >= n then + fail k + else case Key.compare (k, #1 (A.sub (arr, i))) of + EQUAL => A.update (arr, i, (k, v)) + | GREATER => loop (i+1) + | LESS => fail k + in + Chunk (loop 0) + end + + | Node {left, right, key, value, ...} => + case Key.compare (k, key) of + LESS => makeNode (updateKey left (k, v)) (key, value) right + | GREATER => makeNode left (key, value) (updateKey right (k, v)) + | EQUAL => makeNode left (k, v) right) + end + + + fun lookup t k = + case t of + Empty => NONE + + | Chunk arr => + let + val n = A.length arr + fun loop i = + if i >= n then + NONE + else case Key.compare (k, #1 (A.sub (arr, i))) of + EQUAL => SOME (#2 (A.sub (arr, i))) + | GREATER => loop (i+1) + | LESS => NONE + in + loop 0 + end + + | Node {left, right, key, value, ...} => + case Key.compare (k, key) of + LESS => lookup left k + | GREATER => lookup right k + | EQUAL => SOME value + +end diff --git a/tests/mpllib/Color.sml b/tests/mpllib/Color.sml new file mode 100644 index 000000000..b3b0b3992 --- /dev/null +++ b/tests/mpllib/Color.sml @@ -0,0 +1,176 @@ +structure Color = +struct + type channel = Word8.word + type pixel = {red: channel, green: channel, blue: channel} + + val white: pixel = {red=0w255, green=0w255, blue=0w255} + val black: pixel = {red=0w0, green=0w0, blue=0w0} + val red: pixel = {red=0w255, green=0w0, blue=0w0} + val blue: pixel = {red=0w0, green=0w0, blue=0w255} + + fun packInt ({red, green, blue}: pixel) = + let + fun b x = Word8.toInt x + in + (65536 * b red) + (256 * b green) + b blue + end + + fun compare (p1, p2) = Int.compare (packInt p1, packInt p2) + + fun equal (p1, p2) = (compare (p1, p2) = EQUAL) + + (* Based on the "low-cost approximation" given at + * https://www.compuphase.com/cmetric.htm *) + fun approxHumanPerceptionDistance + ({red=r1, green=g1, blue=b1}, {red=r2, green=g2, blue=b2}) = + let + fun c x = Word8.toInt x + val (r1, g1, b1, r2, g2, b2) = (c r1, c g1, c b1, c r2, c g2, c b2) + + val rmean = (r1 + r2) div 2 + val r = r1 - r2 + val g = g1 - g2 + val b = b1 - b2 + in + Math.sqrt (Real.fromInt + ((((512+rmean)*r*r) div 256) + 4*g*g + (((767-rmean)*b*b) div 256))) + end + + local + fun c x = Word8.toInt x + fun sq x = x * x + in + fun sqDistance ({red=r1, green=g1, blue=b1}, {red=r2, green=g2, blue=b2}) = + sq (c r2 - c r1) + sq (c g2 - c g1) + sq (c b2 - c b1) + end + + fun distance (p1, p2) = Math.sqrt (Real.fromInt (sqDistance (p1, p2))) + + fun to256 rchannel = + Word8.fromInt (Real.ceil (rchannel * 255.0)) + + fun from256 channel = + Real.fromInt (Word8.toInt channel) / 255.0 + + + (** hue in range [0,360) + * 0-------60-------120-------180-------240-------300-------360 + * red yellow green cyan blue purple red + * + * saturation in range [0,1] + * 0--------------1 + * grayscale vibrant + * + * value in range [0,1] + * 0--------------1 + * dark light + *) + fun hsv {h: real, s: real, v: real}: pixel = + let + val H = h + val S = s + val V = v + + (* from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB *) + val C = V * S + val H' = H / 60.0 + val X = C * (1.0 - Real.abs (Real.rem (H', 2.0) - 1.0)) + + val (R1, G1, B1) = + if H' < 1.0 then (C, X, 0.0) + else if H' < 2.0 then (X, C, 0.0) + else if H' < 3.0 then (0.0, C, X) + else if H' < 4.0 then (0.0, X, C) + else if H' < 5.0 then (X, 0.0, C) + else (C, 0.0, X) + + val m = V - C + in + {red = to256 (R1 + m), green = to256 (G1 + m), blue = to256 (B1 + m)} + end + + (* ======================================================================= *) + + type color = {red: real, green: real, blue: real, alpha: real} + + (** hue in range [0,360) + * 0-------60-------120-------180-------240-------300-------360 + * red yellow green cyan blue purple red + * + * saturation in range [0,1] + * 0--------------1 + * grayscale vibrant + * + * value in range [0,1] + * 0--------------1 + * dark light + * + * alpha in range [0,1] + * 0--------------1 + * transparent opaque + *) + fun hsva {h: real, s: real, v: real, a: real}: color = + let + val H = h + val S = s + val V = v + + (* from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB *) + val C = V * S + val H' = H / 60.0 + val X = C * (1.0 - Real.abs (Real.rem (H', 2.0) - 1.0)) + + val (R1, G1, B1) = + if H' < 1.0 then (C, X, 0.0) + else if H' < 2.0 then (X, C, 0.0) + else if H' < 3.0 then (0.0, C, X) + else if H' < 4.0 then (0.0, X, C) + else if H' < 5.0 then (X, 0.0, C) + else (C, 0.0, X) + + val m = V - C + in + {red = R1 + m, green = G1 + m, blue = B1 + m, alpha = a} + end + + fun overlayColor {fg: color, bg: color} = + let + val alpha = 1.0 - (1.0 - #alpha fg) * (1.0 - #alpha bg) + in + if alpha < 1e~6 then + (* essentially fully transparent, color doesn't matter *) + { red = 0.0, green = 0.0, blue = 0.0, alpha = alpha } + else + let + val red = + #red fg * #alpha fg / alpha + + #red bg * #alpha bg * (1.0 - #alpha fg) / alpha + + val green = + #green fg * #alpha fg / alpha + + #green bg * #alpha bg * (1.0 - #alpha fg) / alpha + + val blue = + #blue fg * #alpha fg / alpha + + #blue bg * #alpha bg * (1.0 - #alpha fg) / alpha + in + {red=red, green=green, blue=blue, alpha=alpha} + end + end + + (** Converts a color with transparency to a concrete pixel. Assumes white + * background. + *) + fun colorToPixel (color: color) : pixel = + let + val white = {red = 1.0, blue = 1.0, green = 1.0, alpha = 1.0} + (* alpha will be 1 *) + val {red, green, blue, ...} = overlayColor {fg = color, bg = white} + in + { red = to256 red, green = to256 green, blue = to256 blue} + end + + fun pixelToColor {red, green, blue} = + {red = from256 red, green = from256 green, blue = from256 blue, alpha = 1.0} + +end diff --git a/tests/mpllib/CommandLineArgs.sml b/tests/mpllib/CommandLineArgs.sml new file mode 100644 index 000000000..c89f89052 --- /dev/null +++ b/tests/mpllib/CommandLineArgs.sml @@ -0,0 +1,104 @@ +structure CommandLineArgs : +sig + (* each takes a key K and a default value D, looks for -K V in the + * command-line arguments, and returns V if it finds it, or D otherwise. *) + val parseString: string -> string -> string + val parseInt: string -> int -> int + val parseReal: string -> real -> real + val parseBool: string -> bool -> bool + + (** Look for every instance of -K V and return seq of the Vs. + * For example, if this is given on the commandline: + * -arg a -arg b -arg c -arg d + * then + * parseStrings "arg" ==> ["a", "b", "c", "d"] + *) + val parseStrings: string -> string list + + (* parseFlag K returns true if --K given on command-line *) + val parseFlag: string -> bool + + val positional: unit -> string list +end = +struct + + fun die msg = + ( TextIO.output (TextIO.stdErr, msg ^ "\n") + ; TextIO.flushOut TextIO.stdErr + ; OS.Process.exit OS.Process.failure + ) + + fun positional () = + let + fun loop found rest = + case rest of + [] => List.rev found + | [x] => List.rev (if not (String.isPrefix "-" x) then x::found else found) + | x::y::rest' => + if not (String.isPrefix "-" x) then + loop (x::found) (y::rest') + else if String.isPrefix "--" x then + loop found (y::rest') + else + loop found rest' + in + loop [] (CommandLine.arguments ()) + end + + fun search key args = + case args of + [] => NONE + | x :: args' => + if key = x + then SOME args' + else search key args' + + fun parseString key default = + case search ("-" ^ key) (CommandLine.arguments ()) of + NONE => default + | SOME [] => die ("Missing argument of \"-" ^ key ^ "\" ") + | SOME (s :: _) => s + + fun parseStrings key = + let + fun loop args = + case search ("-" ^ key) args of + NONE => [] + | SOME [] => die ("Missing argument of \"-" ^ key ^ "\"") + | SOME (v :: args') => v :: loop args' + in + loop (CommandLine.arguments ()) + end + + fun parseInt key default = + case search ("-" ^ key) (CommandLine.arguments ()) of + NONE => default + | SOME [] => die ("Missing argument of \"-" ^ key ^ "\" ") + | SOME (s :: _) => + case Int.fromString s of + NONE => die ("Cannot parse integer from \"-" ^ key ^ " " ^ s ^ "\"") + | SOME x => x + + fun parseReal key default = + case search ("-" ^ key) (CommandLine.arguments ()) of + NONE => default + | SOME [] => die ("Missing argument of \"-" ^ key ^ "\" ") + | SOME (s :: _) => + case Real.fromString s of + NONE => die ("Cannot parse real from \"-" ^ key ^ " " ^ s ^ "\"") + | SOME x => x + + fun parseBool key default = + case search ("-" ^ key) (CommandLine.arguments ()) of + NONE => default + | SOME [] => die ("Missing argument of \"-" ^ key ^ "\" ") + | SOME ("true" :: _) => true + | SOME ("false" :: _) => false + | SOME (s :: _) => die ("Cannot parse bool from \"-" ^ key ^ " " ^ s ^ "\"") + + fun parseFlag key = + case search ("--" ^ key) (CommandLine.arguments ()) of + NONE => false + | SOME _ => true + +end diff --git a/tests/mpllib/CountingSort.sml b/tests/mpllib/CountingSort.sml new file mode 100644 index 000000000..71719a7da --- /dev/null +++ b/tests/mpllib/CountingSort.sml @@ -0,0 +1,129 @@ +structure CountingSort :> +sig + type 'a seq = 'a ArraySlice.slice + + val sort : 'a seq + -> (int -> int) (* bucket id of ith element *) + -> int (* number of buckets *) + -> 'a seq * int seq (* sorted, bucket offsets *) +end = +struct + + structure A = Array + structure AS = ArraySlice + + type 'a seq = 'a ArraySlice.slice + + val for = Util.for + val loop = Util.loop + val forBackwards = Util.forBackwards + + fun seqSortInternal In Out Keys Counts genOffsets = + let + val n = AS.length In + val m = AS.length Counts + (* val _ = print ("seqSortInternal n=" ^ Int.toString n ^ " m=" ^ Int.toString m ^ "\n") *) + val sub = AS.sub + val update = AS.update + in + for (0, m) (fn i => update (Counts,i,0)); + + for (0, n) (fn i => + let + val j = Keys i + (* val _ = print ("update " ^ Int.toString j ^ "\n") *) + in + update (Counts, j, sub(Counts,j) + 1) + end); + + (* print ("counts: " ^ Seq.toString Int.toString Counts ^ "\n"); *) + + loop (0, m) 0 (fn (s,i) => + let + val t = sub(Counts, i) + in + update(Counts, i, s); + s + t + end); + + (* print ("counts: " ^ Seq.toString Int.toString Counts ^ "\n"); *) + + for (0, n) (fn i => + let + val j = Keys(i) + val k = sub(Counts, j) + in + update(Counts, j, k+1); + update(Out, k, sub(In, i)) + end); + + if genOffsets then + (forBackwards (0,m-1) (fn i => + update(Counts,i+1,sub(Counts,i))); + update(Counts,0,0); 0) + else + loop (0, m) 0 (fn (s,i) => + let + val t = sub(Counts, i) + in + (update(Counts, i, t - s); t) + end) + end + + fun seqSort(In, Keys, numBuckets) = + let + val Counts = AS.full(ForkJoin.alloc (numBuckets+1)) + val Out = AS.full(ForkJoin.alloc (AS.length In)) + in + seqSortInternal In Out Keys (Seq.subseq Counts (0,numBuckets)) true; + AS.update(Counts, numBuckets, AS.length In); + (Out, Counts) + end + + fun sort In Keys numBuckets = + let + val SeqThreshold = 8192 + val BlockFactor = 32 + val n = AS.length In + (* pad to avoid false sharing *) + val numBucketsPad = Int.max(numBuckets, 16) + val sqrt = Real.floor(Math.sqrt(Real.fromInt n)) + val numBlocks = n div (numBuckets * BlockFactor) + in + if (numBlocks <= 1 orelse n < SeqThreshold) then + seqSort(In, Keys, numBuckets) + else let + val blockSize = ((n-1) div numBlocks) + 1; + val m = numBlocks * numBucketsPad + val B = AS.full(ForkJoin.alloc(AS.length In)) + val Counts = AS.full(ForkJoin.alloc(m)) + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn i => + let + val start = Int.min(i * blockSize, n) + val len = Int.min((i+1)* blockSize, n) - start + in + seqSortInternal + (AS.subslice(In, start, SOME(len))) + (AS.subslice(B, start, SOME(len))) + (fn i => Keys(i+start)) + (AS.subslice(Counts,i*numBucketsPad,SOME(numBucketsPad))) + false; + () + end) + val (sourceOffsets, _) = Seq.scan op+ 0 Counts + val transCounts = SampleSort.transpose(Counts, numBlocks, + numBucketsPad) + val (destOffsets, _) = Seq.scan op+ 0 transCounts + val C = SampleSort.transposeBlocks(B, sourceOffsets, destOffsets, + Counts, numBlocks, numBucketsPad, n) + val bucketOffsets = + Seq.tabulate (fn i => + if (i = numBuckets) then n + else AS.sub (destOffsets, i * numBlocks)) + (numBuckets+1) + in + (C, bucketOffsets) + end + end + +end diff --git a/tests/mpllib/DelayedSeq.sml b/tests/mpllib/DelayedSeq.sml new file mode 100644 index 000000000..901235208 --- /dev/null +++ b/tests/mpllib/DelayedSeq.sml @@ -0,0 +1,414 @@ +functor MkDelayedSeq (Stream: STREAM) : SEQUENCE = +struct + + exception NYI + exception Range + exception Size + + (* structure Stream = DelayedStream *) + + val for = Util.for + val par = ForkJoin.par + val parfor = ForkJoin.parfor + val alloc = ForkJoin.alloc + + val gran = 5000 + val blockSize = 5000 + fun numBlocks n = Util.ceilDiv n blockSize + + fun blockStart b n = b * blockSize + fun blockEnd b n = Int.min (n, (b+1) * blockSize) + fun getBlockSize b n = blockEnd b n - blockStart b n + fun convertToBlockIdx i n = + (i div blockSize, i mod blockSize) + + structure A = + struct + open Array + type 'a t = 'a array + fun nth a i = sub (a, i) + end + + structure AS = + struct + open ArraySlice + type 'a t = 'a slice + fun nth a i = sub (a, i) + end + + + type 'a rad = int * int * (int -> 'a) + type 'a bid = int * (int -> 'a Stream.t) + datatype 'a seq = + Full of 'a AS.t + | Rad of 'a rad + | Bid of 'a bid + + type 'a t = 'a seq + + + fun radlength (start, stop, _) = stop-start + fun radnth (start, _, f) i = f (start+i) + + + fun length s = + case s of + Full slice => AS.length slice + | Rad rad => radlength rad + | Bid (n, _) => n + + + fun nth s i = + case s of + Full slice => AS.nth slice i + | Rad rad => radnth rad i + | Bid (n, getBlock) => + let + val (outer, inner) = convertToBlockIdx i n + in + Stream.nth (getBlock outer) inner + end + + + fun bidify (s: 'a seq) : 'a bid = + let + fun block start nth b = + Stream.tabulate (fn i => nth (start + b * blockSize + i)) + in + case s of + Full slice => + let + val (a, start, n) = AS.base slice + in + (n, block start (A.nth a)) + end + + | Rad (start, stop, nth) => + (stop-start, block start nth) + + | Bid xx => xx + end + + + fun applyIdx (s: 'a seq) (g: int * 'a -> unit) = + let + val (n, getBlock) = bidify s + in + parfor 1 (0, numBlocks n) (fn b => + let + val lo = blockStart b n + in + Stream.applyIdx (getBlockSize b n, getBlock b) (fn (j, x) => g (lo+j, x)) + end) + end + + + fun apply (s: 'a seq) (g: 'a -> unit) = + applyIdx s (fn (_, x) => g x) + + + fun reify s = + let + val a = alloc (length s) + in + applyIdx s (fn (i, x) => A.update (a, i, x)); + AS.full a + end + + + fun force s = Full (reify s) + + + fun radify s = + case s of + Full slice => + let + val (a, i, n) = AS.base slice + in + (i, i+n, A.nth a) + end + + | Rad xx => xx + + | Bid (n, blocks) => + radify (force s) + + + fun tabulate f n = + Rad (0, n, f) + + + fun fromList xs = + Full (AS.full (Array.fromList xs)) + + + fun % xs = + fromList xs + + + fun singleton x = + Rad (0, 1, fn _ => x) + + + fun $ x = + singleton x + + + fun empty () = + fromList [] + + + fun fromArraySeq a = + Full a + + + fun range (i, j) = + Rad (i, j, fn k => k) + + + fun toArraySeq s = + case s of + Full x => x + | _ => reify s + + + fun map f s = + case s of + Full _ => map f (Rad (radify s)) + | Rad (i, j, g) => Rad (i, j, f o g) + | Bid (n, getBlock) => Bid (n, Stream.map f o getBlock) + + + fun mapIdx f s = + case s of + Full _ => mapIdx f (Rad (radify s)) + | Rad (i, j, g) => Rad (0, j-i, fn k => f (k, g (i+k))) + | Bid (n, getBlock) => + Bid (n, fn b => + Stream.mapIdx (fn (i, x) => f (b*blockSize + i, x)) (getBlock b)) + + + fun enum s = + mapIdx (fn (i,x) => (i,x)) s + + + fun flatten (ss: 'a seq seq) : 'a seq = + let + val numChildren = length ss + val children: 'a rad AS.t = reify (map radify ss) + val offsets = + SeqBasis.scan gran op+ 0 (0, numChildren) (radlength o AS.nth children) + val totalLen = A.nth offsets numChildren + fun offset i = A.nth offsets i + + val getBlock = + Stream.makeBlockStreams + { blockSize = blockSize + , numChildren = numChildren + , offset = offset + , getElem = (fn i => fn j => radnth (AS.nth children i) j) + } + in + Bid (totalLen, getBlock) + end + + + fun mapOption (f: 'a -> 'b option) (s: 'a seq) = + let + val (n, getBlock) = bidify s + val nb = numBlocks n + val packed: 'b rad array = + SeqBasis.tabulate 1 (0, nb) (fn b => + radify (Full (Stream.pack f (getBlockSize b n, getBlock b))) + ) + val offsets = + SeqBasis.scan gran op+ 0 (0, nb) (radlength o A.nth packed) + val totalLen = A.nth offsets nb + fun offset i = A.nth offsets i + + val getBlock' = + Stream.makeBlockStreams + { blockSize = blockSize + , numChildren = nb + , offset = offset + , getElem = (fn i => fn j => radnth (A.nth packed i) j) + } + in + Bid (totalLen, getBlock') + end + + + fun filter p s = + mapOption (fn x => if p x then SOME x else NONE) s + + + fun inject (s, u) = + let + val a = reify s + val (base, i, _) = AS.base a + in + apply u (fn (j, x) => Array.update (base, i+j, x)); + Full a + end + + + fun bidZipWith f (s1, s2) = + let + val (n, getBlock1) = bidify s1 + val (_, getBlock2) = bidify s2 + in + Bid (n, fn b => Stream.zipWith f (getBlock1 b, getBlock2 b)) + end + + fun radZipWith f (s1, s2) = + let + val (lo1, hi1, nth1) = radify s1 + val (lo2, _, nth2) = radify s2 + in + Rad (0, hi1-lo1, fn i => f (nth1 (lo1+i), nth2 (lo2+i))) + end + + fun zipWith f (s1, s2) = + if length s1 <> length s2 then raise Size else + case (s1, s2) of + (Bid _, _) => bidZipWith f (s1, s2) + | (_, Bid _) => bidZipWith f (s1, s2) + | _ => radZipWith f (s1, s2) + + fun zip (s1, s2) = + zipWith (fn (x, y) => (x, y)) (s1, s2) + + + fun scan f z s = + let + val (n, getBlock) = bidify s + val nb = numBlocks n + val blockSums = + SeqBasis.tabulate 1 (0, nb) (fn b => + Stream.iterate f z (getBlockSize b n, getBlock b) + ) + val p = SeqBasis.scan gran f z (0, nb) (A.nth blockSums) + val t = A.nth p nb + val r = Bid (n, fn b => Stream.iteratePrefixes f (A.nth p b) (getBlock b)) + in + (r, t) + end + + + fun scanIncl f z s = + let + val (n, getBlock) = bidify s + val nb = numBlocks n + val blockSums = + SeqBasis.tabulate 1 (0, nb) (fn b => + Stream.iterate f z (getBlockSize b n, getBlock b) + ) + val p = SeqBasis.scan gran f z (0, nb) (A.nth blockSums) + in + Bid (n, fn b => + Stream.iteratePrefixesIncl f (A.nth p b) (getBlock b)) + end + + + fun reduce f z s = + case s of + Full xx => SeqBasis.reduce gran f z (0, length s) (AS.nth xx) + | Rad xx => SeqBasis.reduce gran f z (0, length s) (radnth xx) + | Bid (n, getBlock) => + let + val nb = numBlocks n + in + SeqBasis.reduce gran f z (0, nb) (fn b => + Stream.iterate f z (getBlockSize b n, getBlock b) + ) + end + + + fun iterate f z s = + case s of + Full xx => SeqBasis.foldl f z (0, length s) (AS.nth xx) + | Rad xx => SeqBasis.foldl f z (0, length s) (radnth xx) + | Bid (n, getBlock) => + Util.loop (0, numBlocks n) z (fn (z, b) => + Stream.iterate f z (getBlockSize b n, getBlock b)) + + + fun rev s = + let + val n = length s + val rads = radify s + in + tabulate (fn i => radnth rads (n-i-1)) n + end + + + fun append (s, t) = + let + val n = length s + val m = length t + + val rads = radify s + val radt = radify t + + fun elem i = if i < n then radnth rads i else radnth radt (i-n) + in + tabulate elem (n+m) + end + + + fun subseq s (i, len) = + if i < 0 orelse len < 0 orelse i+len > length s then + raise Subscript + else + let + val n = length s + val (start, stop, nth) = radify s + in + Rad (start+i, start+i+len, nth) + end + + + fun take s n = subseq s (0, n) + fun drop s n = subseq s (n, length s - n) + + + fun toList s = + List.rev (iterate (fn (elems, x) => x :: elems) [] s) + + fun toString f s = + "[" ^ String.concatWith "," (toList (map f s)) ^ "]" + + (* ===================================================================== *) + + datatype 'a listview = NIL | CONS of 'a * 'a seq + datatype 'a treeview = EMPTY | ONE of 'a | PAIR of 'a seq * 'a seq + + type 'a ord = 'a * 'a -> order + type 'a t = 'a seq + + fun filterIdx x = raise NYI + fun iterateIdx x = raise NYI + + fun argmax x = raise NYI + fun collate x = raise NYI + fun collect x = raise NYI + fun equal x = raise NYI + fun iteratePrefixes x = raise NYI + fun iteratePrefixesIncl x = raise NYI + fun merge x = raise NYI + fun sort x = raise NYI + fun splitHead x = raise NYI + fun splitMid x = raise NYI + fun update x = raise NYI + fun zipWith3 x = raise NYI + + fun filterSome x = raise NYI + fun foreach x = raise NYI + fun foreachG x = raise NYI + +end + + + +structure DelayedSeq = MkDelayedSeq (DelayedStream) +(* structure DelayedSeq = MkDelayedSeq (RecursiveStream) *) diff --git a/tests/mpllib/DelayedStream.sml b/tests/mpllib/DelayedStream.sml new file mode 100644 index 000000000..4ffc74569 --- /dev/null +++ b/tests/mpllib/DelayedStream.sml @@ -0,0 +1,225 @@ +structure DelayedStream :> STREAM = +struct + + (** A stream is a generator for a stateful trickle function: + * trickle = stream () + * x0 = trickle 0 + * x1 = trickle 1 + * x2 = trickle 2 + * ... + * + * The integer argument is just an optimization (it could be packaged + * up into the state of the trickle function, but doing it this + * way is more efficient). Requires passing `i` on the ith call + * to trickle. + *) + type 'a t = unit -> int -> 'a + type 'a stream = 'a t + + + fun nth stream i = + let + val trickle = stream () + + fun loop j = + let + val x = trickle j + in + if j = i then x else loop (j+1) + end + in + loop 0 + end + + + fun tabulate f = + fn () => f + + + fun map g stream = + fn () => + let + val trickle = stream () + in + g o trickle + end + + + fun mapIdx g stream = + fn () => + let + val trickle = stream () + in + fn idx => g (idx, trickle idx) + end + + + fun applyIdx (length, stream) g = + let + val trickle = stream () + fun loop i = + if i >= length then () else (g (i, trickle i); loop (i+1)) + in + loop 0 + end + + + fun resize arr = + let + val newCapacity = 2 * Array.length arr + val dst = ForkJoin.alloc newCapacity + in + Array.copy {src = arr, dst = dst, di = 0}; + dst + end + + + (** simple but less efficient: accumulate in list *) + (*fun pack f (length, stream) = + let + val trickle = stream () + + fun loop (data, count) i = + if i < length then + case f (trickle i) of + SOME y => + loop (y :: data, count+1) (i+1) + | NONE => + loop (data, count) (i+1) + else + (data, count) + + val (data, count) = loop ([], 0) 0 + in + ArraySlice.full (Array.fromList (List.rev data)) + (* ArraySlice.slice (data, 0, SOME count) *) + end*) + + + (** more efficient: accumulate in dynamic resizing array *) + fun pack f (length, stream) = + let + val trickle = stream () + + fun loop (data, next) i = + if i < length andalso next < Array.length data then + case f (trickle i) of + SOME y => + ( Array.update (data, next, y) + ; loop (data, next+1) (i+1) + ) + | NONE => + loop (data, next) (i+1) + + else if next >= Array.length data then + loop (resize data, next) i + + else + (data, next) + + val (data, count) = loop (ForkJoin.alloc 10, 0) 0 + in + ArraySlice.slice (data, 0, SOME count) + end + + + fun iterate g b (length, stream) = + let + val trickle = stream () + fun loop b i = + if i >= length then b else loop (g (b, trickle i)) (i+1) + in + loop b 0 + end + + + fun iteratePrefixes g b stream = + fn () => + let + val trickle = stream () + val stuff = ref b + in + fn idx => + let + val acc = !stuff + val elem = trickle idx + val acc' = g (acc, elem) + in + stuff := acc'; + acc + end + end + + + fun iteratePrefixesIncl g b stream = + fn () => + let + val trickle = stream () + val stuff = ref b + in + fn idx => + let + val acc = !stuff + val elem = trickle idx + val acc' = g (acc, elem) + in + stuff := acc'; + acc' + end + end + + + fun zipWith g (s1, s2) = + fn () => + let + val trickle1 = s1 () + val trickle2 = s2 () + in + fn idx => g (trickle1 idx, trickle2 idx) + end + + + fun makeBlockStreams + { blockSize: int + , numChildren: int + , offset: int -> int + , getElem: int -> int -> 'a + } = + let + fun getBlock blockIdx = + let + fun advanceUntilNonEmpty i = + if i >= numChildren orelse offset i <> offset (i+1) then + i + else + advanceUntilNonEmpty (i+1) + in + fn () => + let + val lo = blockIdx * blockSize + val firstOuterIdx = + OffsetSearch.indexSearch (0, numChildren, offset) lo + val outerIdx = ref firstOuterIdx + in + fn idx => + (let + val i = !outerIdx + val j = lo + idx - offset i + (* val j = !innerIdx *) + val elem = getElem i j + in + if offset i + j + 1 < offset (i+1) then + () + else + outerIdx := advanceUntilNonEmpty (i+1); + elem + end) + end + end + + in + getBlock + end + + +end diff --git a/tests/mpllib/DoubleBinarySearch.sml b/tests/mpllib/DoubleBinarySearch.sml new file mode 100644 index 000000000..e96160ad1 --- /dev/null +++ b/tests/mpllib/DoubleBinarySearch.sml @@ -0,0 +1,123 @@ +(* The function `split_count` splits sequences s and t into (s1, s2) and + * (t1, t2) such that the largest items of s1 and t1 are smaller than the + * smallest items of s2 and t2. The desired output size |s1|+|t1| is given + * as a parameter. + * + * Specifically, `split_count cmp (s, t) k` returns `(m, n)` where: + * (s1, s2) = (s[..m], s[m..]) + * (t1, t2) = (t[..n], t[n..]) + * m+n = k + * max(s1) <= min(t2) + * max(t1) <= min(s2) + * + * Note that there are many possible solutions, so we also mandate that `m` + * should be minimized. + * + * Work: O(log(|s|+|t|)) + * Span: O(log(|s|+|t|)) + *) +structure DoubleBinarySearch: +sig + type 'a seq = {lo: int, hi: int, get: int -> 'a} + + val split_count: ('a * 'a -> order) -> 'a seq * 'a seq -> int -> (int * int) + + val split_count_slice: ('a * 'a -> order) + -> 'a ArraySlice.slice * 'a ArraySlice.slice + -> int + -> (int * int) +end = +struct + + type 'a seq = {lo: int, hi: int, get: int -> 'a} + + fun leq cmp (x, y) = + case cmp (x, y) of + GREATER => false + | _ => true + + fun geq cmp (x, y) = + case cmp (x, y) of + LESS => false + | _ => true + + fun split_count cmp (s: 'a seq, t: 'a seq) k = + let + fun normalize_then_loop (slo, shi) (tlo, thi) count = + let + val slo_orig = slo + val tlo_orig = tlo + + (* maybe count is small *) + val shi = Int.min (shi, slo + count) + val thi = Int.min (thi, tlo + count) + + (* maybe count is large *) + val slack = (shi - slo) + (thi - tlo) - count + val slack = Int.min (slack, shi - slo) + val slack = Int.min (slack, thi - tlo) + + val slo = Int.max (slo, shi - slack) + val tlo = Int.max (tlo, thi - slack) + + val count = count - (slo - slo_orig) - (tlo - tlo_orig) + in + loop (slo, shi) (tlo, thi) count + end + + + and loop (slo, shi) (tlo, thi) count = + if shi - slo <= 0 then + (slo, tlo + count) + + else if thi - tlo <= 0 then + (slo + count, tlo) + + else if count = 1 then + if geq cmp (#get s slo, #get t tlo) then (slo, tlo + 1) + else (slo + 1, tlo) + + else + let + val m = count div 2 + val n = count - m + + (* |------|x|-------| + * ^ ^ ^ + * slo slo+m shi + * + * |------|y|-------| + * ^ ^ ^ + * tlo tlo+n thi + *) + + val leq_y_x = + n = 0 orelse slo + m >= shi + orelse leq cmp (#get t (tlo + n - 1), #get s (slo + m)) + in + if leq_y_x then + normalize_then_loop (slo, shi) (tlo + n, thi) (count - n) + else + normalize_then_loop (slo, shi) (tlo, tlo + n) count + end + + + val {lo = slo, hi = shi, ...} = s + val {lo = tlo, hi = thi, ...} = t + + val (m, n) = normalize_then_loop (slo, shi) (tlo, thi) k + in + (m - slo, n - tlo) + end + + + fun fromslice s = + let val (sarr, slo, slen) = ArraySlice.base s + in {lo = slo, hi = slo + slen, get = fn i => Array.sub (sarr, i)} + end + + + fun split_count_slice cmp (s, t) k = + split_count cmp (fromslice s, fromslice t) k + +end diff --git a/tests/mpllib/ExtraBinIO.sml b/tests/mpllib/ExtraBinIO.sml new file mode 100644 index 000000000..1e0a7a131 --- /dev/null +++ b/tests/mpllib/ExtraBinIO.sml @@ -0,0 +1,73 @@ +structure ExtraBinIO = +struct + + fun w8 file (w: Word8.word) = BinIO.output1 (file, w) + + fun w64b file (w: Word64.word) = + let + val w8 = w8 file + open Word64 + infix 2 >> andb + in + w8 (Word8.fromLarge (w >> 0w56)); + w8 (Word8.fromLarge (w >> 0w48)); + w8 (Word8.fromLarge (w >> 0w40)); + w8 (Word8.fromLarge (w >> 0w32)); + w8 (Word8.fromLarge (w >> 0w24)); + w8 (Word8.fromLarge (w >> 0w16)); + w8 (Word8.fromLarge (w >> 0w8)); + w8 (Word8.fromLarge w) + end + + fun w32b file (w: Word32.word) = + let + val w8 = w8 file + val w = Word32.toLarge w + open Word64 + infix 2 >> andb + in + w8 (Word8.fromLarge (w >> 0w24)); + w8 (Word8.fromLarge (w >> 0w16)); + w8 (Word8.fromLarge (w >> 0w8)); + w8 (Word8.fromLarge w) + end + + fun w32l file (w: Word32.word) = + let + val w8 = w8 file + val w = Word32.toLarge w + open Word64 + infix 2 >> andb + in + w8 (Word8.fromLarge w); + w8 (Word8.fromLarge (w >> 0w8)); + w8 (Word8.fromLarge (w >> 0w16)); + w8 (Word8.fromLarge (w >> 0w24)) + end + + fun w16b file (w: Word16.word) = + let + val w8 = w8 file + val w = Word16.toLarge w + open Word64 + infix 2 >> andb + in + w8 (Word8.fromLarge (w >> 0w8)); + w8 (Word8.fromLarge w) + end + + fun w16l file (w: Word16.word) = + let + val w8 = w8 file + val w = Word16.toLarge w + open Word64 + infix 2 >> andb + in + w8 (Word8.fromLarge w); + w8 (Word8.fromLarge (w >> 0w8)) + end + + fun wrgb file ({red, green, blue}: Color.pixel) = + ( w8 file red; w8 file green; w8 file blue ) + +end diff --git a/tests/mpllib/FindFirst.sml b/tests/mpllib/FindFirst.sml new file mode 100644 index 000000000..8302b8824 --- /dev/null +++ b/tests/mpllib/FindFirst.sml @@ -0,0 +1,39 @@ +structure FindFirst : +sig + val findFirstSerial : (int * int) -> (int -> bool) -> int option + + (* findFirst granularity (start, end) predicate *) + val findFirst : int -> (int * int) -> (int -> bool) -> int option +end = +struct + + fun findFirstSerial (i, j) p = + if i >= j then NONE + else if p i then SOME i + else findFirstSerial (i+1, j) p + + fun optMin (a, b) = + case (a, b) of + (SOME x, SOME y) => (SOME (Int.min (x, y))) + | (NONE, _) => b + | (_, NONE) => a + + fun findFirst grain (lo, hi) p = + let + fun try (i, j) = + if j - i <= grain then + findFirstSerial (i, j) p + else + SeqBasis.reduce grain optMin NONE (i, j) + (fn k => if p k then SOME k else NONE) + + fun loop (i, j) = + if i >= j then NONE else + case try (i, j) of + NONE => loop (j, Int.min (j + 2*(j-i), hi)) + | SOME x => SOME x + in + loop (lo, Int.min (lo+grain, hi)) + end + +end diff --git a/tests/mpllib/FlattenMerge.sml b/tests/mpllib/FlattenMerge.sml new file mode 100644 index 000000000..abe54390a --- /dev/null +++ b/tests/mpllib/FlattenMerge.sml @@ -0,0 +1,38 @@ +structure FlattenMerge: +sig + val merge: ('a * 'a -> order) -> 'a Seq.t * 'a Seq.t -> 'a Seq.t +end = +struct + + val serialGrain = CommandLineArgs.parseInt "MPLLib_Merge_serialGrain" 4000 + + fun merge_loop cmp (s1, s2) = + if Seq.length s1 = 0 then + TFlatten.leaf s2 + else if Seq.length s1 + Seq.length s2 <= serialGrain then + TFlatten.leaf (Merge.mergeSerial cmp (s1, s2)) + else + let + val n1 = Seq.length s1 + val n2 = Seq.length s2 + val mid1 = n1 div 2 + val pivot = Seq.nth s1 mid1 + val mid2 = BinarySearch.search cmp s2 pivot + + val l1 = Seq.take s1 mid1 + val r1 = Seq.drop s1 (mid1 + 1) + val l2 = Seq.take s2 mid2 + val r2 = Seq.drop s2 mid2 + + val (outl, outr) = + ForkJoin.par (fn _ => merge_loop cmp (l1, l2), fn _ => + merge_loop cmp (r1, r2)) + in + TFlatten.node + (TFlatten.node (outl, TFlatten.leaf (Seq.singleton pivot)), outr) + end + + fun merge cmp (s1, s2) = + TFlatten.flatten (merge_loop cmp (s1, s2)) + +end diff --git a/tests/mpllib/FuncSequence.sml b/tests/mpllib/FuncSequence.sml new file mode 100644 index 000000000..8e1434136 --- /dev/null +++ b/tests/mpllib/FuncSequence.sml @@ -0,0 +1,46 @@ +structure FuncSequence: +sig + type 'a t + type 'a seq = 'a t + + val empty: unit -> 'a seq + val length: 'a seq -> int + val take: 'a seq -> int -> 'a seq + val drop: 'a seq -> int -> 'a seq + val nth: 'a seq -> int -> 'a + val iterate: ('b * 'a -> 'b) -> 'b -> 'a seq -> 'b + val tabulate: (int -> 'a) -> int -> 'a seq + val reduce: ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a +end = +struct + (* (i, j, f) defines the sequence [ f(k) : i <= k < j ] *) + type 'a t = int * int * (int -> 'a) + type 'a seq = 'a t + + fun empty () = (0, 0, fn _ => raise Subscript) + fun length (i, j, _) = j - i + fun nth (i, j, f) k = f (i+k) + fun take (i, j, f) k = (i, i+k, f) + fun drop (i, j, f) k = (i+k, j, f) + + fun tabulate f n = (0, n, f) + + fun iterate f b s = + if length s = 0 then b + else iterate f (f (b, nth s 0)) (drop s 1) + + fun reduce f b s = + case length s of + 0 => b + | 1 => nth s 0 + | n => let + val half = n div 2 + val (l, r) = + ForkJoin.par (fn _ => reduce f b (take s half), + fn _ => reduce f b (drop s half)) + in + f (l, r) + end +end + + diff --git a/tests/mpllib/GIF.sml b/tests/mpllib/GIF.sml new file mode 100644 index 000000000..b97836dcd --- /dev/null +++ b/tests/mpllib/GIF.sml @@ -0,0 +1,722 @@ +structure GIF: +sig + type pixel = Color.pixel + type image = {height: int, width: int, data: pixel Seq.t} + + (* A GIF color palette is a table of up to 256 colors, and + * function for remapping the colors of an image. *) + structure Palette: + sig + type t = {colors: pixel Seq.t, remap: image -> int Seq.t} + + (* Selects a set of "well-spaced" colors sampled from the image. + * The first argument is a list of required colors, that must be + * included in the palette. Second number is desired palette size. + *) + val summarize: pixel list -> int -> image -> t + + (* Same as `summarize`, except now just an arbitrary sampling function + * is given instead of an image. + *) + val summarizeBySampling: pixel list -> int -> (int -> pixel) -> t + + (* Selects a quantized color palette. *) + val quantized: (int * int * int) -> t + + val remapColor: t -> pixel -> int + end + + structure LZW: + sig + (* First step of compression. Remap an image with the given color + * palette, and then generate the LZW-compressed code stream. + * This inserts clear- and EOI codes. + * + * arguments are + * 1. number of colors, and + * 2. color indices (from palette remap) + *) + val codeStream: int -> int Seq.t -> int Seq.t + + (* Second step of compression: pack the code stream into bits with + * flexible bit-lengths. This step also inserts sub-block sizes. + * The first argument is the number of colors. *) + val packCodeStream: int -> int Seq.t -> Word8.word Seq.t + end + + (* Write many images as an animation. All images must be the same dimension. *) + val writeMany: string (* output path *) + -> int (* microsecond delay between images *) + -> Palette.t + -> {width: int, height: int, numImages: int, getImage: int -> int Seq.t} + -> unit + + val write: string -> image -> unit +end = +struct + + structure AS = ArraySlice + + type pixel = Color.pixel + type image = {height: int, width: int, data: pixel Seq.t} + + fun err msg = + raise Fail ("GIF: " ^ msg) + + fun stderr msg = + (TextIO.output (TextIO.stdErr, msg); TextIO.output (TextIO.stdErr, "\n")) + + fun ceilLog2 n = + if n <= 0 then + err "ceilLog2: expected input at least 1" + else + (* Util.log2(x) computes 1 + floor(log_2(x)) *) + Util.log2 (n-1) + + structure Palette = + struct + + type t = {colors: pixel Seq.t, remap: image -> int Seq.t} + + fun remapColor ({remap, ...}: t) px = + Seq.nth (remap {width=1, height=1, data=Seq.fromList [px]}) 0 + + fun makeQuantized (rqq, gqq, bqq) = + let + fun bucketSize numBuckets = + Real.floor (1.0 + 255.0 / Real.fromInt numBuckets) + fun bucketShift numBuckets = + Word8.fromInt ((255 - (numBuckets-1)*(bucketSize numBuckets)) div 2) + + fun qi nb = fn c => Word8.toInt c div (bucketSize nb) + fun qc nb = fn i => bucketShift nb + Word8.fromInt (i * bucketSize nb) + + fun makeQ nb = + { numBuckets = nb + , channelToIdx = qi nb + , channelFromIdx = qc nb + } + + val R = makeQ rqq + val G = makeQ gqq + val B = makeQ bqq + val numQuantized = (* this should be at most 256 *) + List.foldl op* 1 (List.map #numBuckets [R, G, B]) + + fun channelIndices {red, green, blue} = + (#channelToIdx R red, #channelToIdx G green, #channelToIdx B blue) + + fun packChannelIndices (r, g, b) = + b + + g * (#numBuckets B) + + r * (#numBuckets B) * (#numBuckets G) + + fun colorOfQuantizeIdx i = + let + val b = i mod (#numBuckets B) + val g = (i div (#numBuckets B)) mod (#numBuckets G) + val r = (i div (#numBuckets B) div (#numBuckets G)) mod (#numBuckets R) + in + { red = #channelFromIdx R r + , green = #channelFromIdx G g + , blue = #channelFromIdx B b + } + end + in + (numQuantized, channelIndices, packChannelIndices, colorOfQuantizeIdx) + end + + fun quantized qpackage = + let + val (numQuantized, channelIndices, pack, colorOfQuantizeIdx) = + makeQuantized qpackage + in + { colors = Seq.tabulate colorOfQuantizeIdx numQuantized + , remap = fn ({data, ...}: image) => + AS.full (SeqBasis.tabulate 1000 (0, Seq.length data) (fn i => + pack (channelIndices (Seq.nth data i)))) + } + end + + fun summarizeBySampling requiredColors paletteSize (sample: int -> Color.pixel) = + if paletteSize <= 0 then + err "summarize: palette size must be at least 1" + else if paletteSize > 256 then + err "summarize: max palette size is 256" + else if List.length requiredColors > paletteSize then + err "summarize: Too many required colors" + else + let + val dist = Color.sqDistance + + val dimBits = 3 + val dim = Util.pow2 dimBits + val numBuckets = dim*dim*dim + fun chanIdx chan = + Word8.toInt (Word8.>> (chan, Word.fromInt (8 - dimBits))) + fun chanIdxs {red, green, blue} = + (chanIdx red, chanIdx green, chanIdx blue) + fun pack (r, g, b) = (dim*dim)*r + dim*g + b + + val table = Array.array (numBuckets, []) + val sz = ref 0 + fun tableSize () = !sz + + fun insert color = + let + val i = pack (chanIdxs color) + val inBucket = Array.sub (table, i) + in + Array.update (table, i, color :: inBucket); + sz := !sz + 1 + end + + fun bounds x = (Int.max (0, x-1), Int.min (dim, x+2)) + + fun minDist color = + let + val (r, g, b) = chanIdxs color + in + Util.loop (bounds r) (valOf Int.maxInt) (fn (md, r) => + Util.loop (bounds g) md (fn (md, g) => + Util.loop (bounds b) md (fn (md, b) => + List.foldl (fn (c, md) => Int.min (md, dist (c, color))) + md + (Array.sub (table, pack (r, g, b)) )))) + end + + val candidatesSize = 20 + + fun chooseColorsLoop i = + if tableSize () = paletteSize then () else + let + fun minDist' px = (px, minDist px) + fun chooseMax ((c1, dc1), (c2, dc2)) = + if dc1 > dc2 then (c1, dc1) else (c2, dc2) + val (c, _) = + Util.loop (0, candidatesSize) (Color.black, ~1) + (fn (best, j) => chooseMax (best, minDist' (sample (i+j)))) + in + insert c; + chooseColorsLoop (i + candidatesSize) + end + + (* First, demand that there are a few simple colors in there! *) + val _ = List.app insert requiredColors + + (* Now, loop until full *) + val _ = chooseColorsLoop 0 + + (* Compact the table *) + val buckets = AS.full table + val bucketSizes = Seq.map List.length buckets + val bucketOffsets = + AS.full (SeqBasis.scan 100 op+ 0 (0, numBuckets) (Seq.nth bucketSizes)) + val palette = ForkJoin.alloc paletteSize + val _ = + Util.for (0, numBuckets) (fn i => + ignore (Util.copyListIntoArray + (Seq.nth buckets i) + palette + (Seq.nth bucketOffsets i))) + val palette = AS.full palette + + (* remap by lookup into compacted table *) + fun remapOne color = + let + val (r, g, b) = chanIdxs color + + fun chooseMin ((c1, dc1), (c2, dc2)) = + if dc1 <= dc2 then (c1, dc1) else (c2, dc2) + + val (i, _) = + Util.loop (bounds r) (~1, valOf Int.maxInt) (fn (md, r) => + Util.loop (bounds g) md (fn (md, g) => + Util.loop (bounds b) md (fn (md, b) => + let + val bucketIdx = pack (r, g, b) + val bStart = Seq.nth bucketOffsets bucketIdx + val bEnd = Seq.nth bucketOffsets (bucketIdx+1) + in + Util.loop (bStart, bEnd) md (fn (md, i) => + chooseMin (md, (i, dist (color, Seq.nth palette i)))) + end))) + in + Int.max (0, i) + end + + fun remap {width, height, data} = + AS.full (SeqBasis.tabulate 100 (0, Seq.length data) + (remapOne o Seq.nth data)) + in + {colors = palette, remap = remap} + end + + fun summarize cs sz ({data, width, height}: image) = + let + val n = Seq.length data + fun sample i = Seq.nth data (Util.hash i mod n) + in + summarizeBySampling cs sz sample + end + + end + + (* =================================================================== *) + + structure CodeTable: + sig + type t + type idx = int + type code = int + + val new: int -> t (* `new numColors` *) + val clear: t -> unit + val insert: (code * idx) -> t -> bool (* returns false when full *) + val maybeLookup: (code * idx) -> t -> code option + end = + struct + type idx = int + type code = int + + exception Invalid + + type t = + { nextCode: int ref + , numColors: int + , table: (idx * code) list array + } + + fun new numColors = + { nextCode = ref (Util.boundPow2 numColors + 2) + , numColors = numColors + , table = Array.array (4096, []) + } + + fun clear {nextCode, numColors, table} = + ( Util.for (0, Array.length table) (fn i => Array.update (table, i, [])) + ; nextCode := Util.boundPow2 numColors + 2 + ) + + fun insert (code, idx) ({nextCode, numColors, table}: t) = + if !nextCode = 4095 then + false (* GIF limits the maximum code number to 4095 *) + else + ( Array.update (table, code, (idx, !nextCode) :: Array.sub (table, code)) + ; nextCode := !nextCode + 1 + ; true + ) + + fun maybeLookup (code, idx) ({table, numColors, ...}: t) = + case List.find (fn (i, c) => i = idx) (Array.sub (table, code)) of + SOME (_, c) => SOME c + | NONE => NONE + + end + + (* =================================================================== *) + + structure DynArray = + struct + type 'a t = 'a array * int + + fun new () = + (ForkJoin.alloc 100, 0) + + fun push x (data, nextIdx) = + if nextIdx < Array.length data then + (Array.update (data, nextIdx, x); (data, nextIdx+1)) + else + let + val data' = ForkJoin.alloc (2 * Array.length data) + in + Util.for (0, Array.length data) (fn i => + Array.update (data', i, Array.sub (data, i))); + Array.update (data', nextIdx, x); + (data', nextIdx+1) + end + + fun toSeq (data, nextIdx) = + AS.slice (data, 0, SOME nextIdx) + end + +(* + structure DynArrayList = + struct + type 'a t = int * 'a array * ('a array list) + + val chunkSize = 256 + + fun new () = + (0, ForkJoin.alloc chunkSize, []) + + fun push x (nextIdx, chunk, tail) = + ( Array.update (chunk, nextIdx, x) + ; if nextIdx+1 < chunkSize then + (nextIdx+1, chunk, tail) + else + (0, ForkJoin.alloc chunkSize, chunk :: tail) + ) + + fun toSeq (nextIdx, chunk, tail) = + let + val totalSize = nextIdx + (chunkSize * List.length tail) + val result = ForkJoin.alloc totalSize + + fun writeChunks cs i = + case cs of + [] => () + | c :: cs' => + ( Array.copy {src = c, dst = result, di = i - Array.length c} + ; writeChunks cs' (i - Array.length c) + ) + in + Util.for (0, nextIdx) (fn i => + Array.update (result, totalSize - nextIdx + i, Array.sub (chunk, i))); + writeChunks tail (totalSize - nextIdx); + AS.full result + end + end +*) + +(* + structure DynList = + struct + type 'a t = 'a list + fun new () = [] + fun push x list = x :: list + fun toSeq xs = Seq.rev (Seq.fromList xs) + end +*) + + (* =================================================================== *) + + + structure LZW = + struct + + structure T = CodeTable + structure DS = DynArray + + fun codeStream numColors colorIndices = + let + fun colorIdx i = Seq.nth colorIndices i + + val clear = Util.boundPow2 numColors + val eoi = clear + 1 + + val table = T.new numColors + + fun finish stream = + DS.toSeq (DS.push eoi stream) + + (* The buffer is implicit, represented instead by currentCode + * i is the next index into `colorIndices` *) + fun loop stream currentCode i = + if i >= Seq.length colorIndices then + finish (DS.push currentCode stream) + else + case T.maybeLookup (currentCode, colorIdx i) table of + SOME code => loop stream code (i+1) + | NONE => + if T.insert (currentCode, colorIdx i) table then + loop (DS.push currentCode stream) (colorIdx i) (i+1) + else + ( T.clear table + ; loop (DS.push clear (DS.push currentCode stream)) + (colorIdx i) (i+1) + ) + in + if Seq.length colorIndices = 0 then + err "empty color index sequence" + else + loop (DS.push clear (DS.new ())) (colorIdx 0) 1 + end + + (* val codeStream = fn image => fn palette => + let + val (result, tm) = Util.getTime (fn _ => codeStream image palette) + in + print ("computed codeStream in " ^ Time.fmt 4 tm ^ "s\n"); + result + end *) + + fun packCodeStream numColors codes = + let + val n = Seq.length codes + fun code i = Seq.nth codes i + val clear = Util.boundPow2 numColors + val eoi = clear+1 + val minCodeSize = ceilLog2 numColors + val firstCodeWidth = minCodeSize+1 + + (* Begin by calculating the bit width of each code. Since we know bit + * widths are reset at each clear code, we can parallelize by splitting + * the codestream into segments delimited by clear codes and processing + * the segments in parallel. + * + * Within a segment, the width is increased every time we generated + * a new code with power-of-two width. Every symbol in the code stream + * corresponds to a newly generated code. + *) + + val clears = + AS.full (SeqBasis.filter 2000 (0, n) (fn i => i) (fn i => code i = clear)) + val numClears = Seq.length clears + + val widths = ForkJoin.alloc n + val _ = Array.update (widths, 0, firstCodeWidth) + val _ = ForkJoin.parfor 1 (0, numClears) (fn c => + let + val i = 1 + Seq.nth clears c + val j = if c = numClears-1 then n else 1 + Seq.nth clears (c+1) + + (* max code in table, up to (but not including) index k *) + fun currentMaxCode k = + k - i (* num outputs since the table was cleared *) + + eoi (* the max code immediately after clearing the table *) + in + Util.loop (i, j) (firstCodeWidth, Util.pow2 firstCodeWidth) + (fn ((currWidth, cwPow2), k) => + ( Array.update (widths, k, currWidth) + ; if currentMaxCode (k+1) <> cwPow2 then + (currWidth, cwPow2) + else + (currWidth+1, cwPow2*2) + )); + () + end) + val widths = AS.full widths + + val totalBitWidth = Seq.reduce op+ 0 widths + val packedSize = Util.ceilDiv totalBitWidth 8 + + val packed = ForkJoin.alloc packedSize + + fun flushBuffer (oi, buffer, used) = + if used < 8 then + (oi, buffer, used) + else + ( Array.update (packed, oi, Word8.fromLarge buffer) + ; flushBuffer (oi+1, LargeWord.>> (buffer, 0w8), used-8) + ) + + (* Input index range [ci,cj) + * Output index range [oi, oj) + * `buffer` is a partially filled byte that has not yet been written + * to the packed. `used` (0 to 7) is how much of that byte is + * used. *) + fun pack (oi, oj) (ci, cj) (buffer: LargeWord.word) (used: int) = + if ci >= cj then + (if oi >= oj then + () + else if oi = oj-1 then + Array.update (packed, oi, Word8.fromLarge buffer) + else + err "cannot fill rest of packed region") + else + let + val thisCode = code ci + val thisWidth = Seq.nth widths ci + val buffer' = + LargeWord.orb (buffer, + LargeWord.<< (LargeWord.fromInt thisCode, Word.fromInt used)) + val used' = used + thisWidth + val (oi', buffer'', used'') = flushBuffer (oi, buffer', used') + in + pack (oi', oj) (ci+1, cj) buffer'' used'' + end + + val _ = pack (0, packedSize) (0, n) 0w0 0 + val packed = AS.full packed + val numBlocks = Util.ceilDiv packedSize 255 + val output = ForkJoin.alloc (packedSize + numBlocks + 1) + in + ForkJoin.parfor 10 (0, numBlocks) (fn i => + let + val size = if i < numBlocks-1 then 255 else packedSize - 255*i + in + Array.update (output, 256*i, Word8.fromInt size); + Util.for (0, size) (fn j => + Array.update (output, 256*i + 1 + j, Seq.nth packed (255*i + j))) + end); + + Array.update (output, packedSize + numBlocks, 0w0); + + AS.full output + end + end + + (* ====================================================================== *) + + fun checkToWord16 thing x = + if x >= 0 andalso x <= 65535 then + Word16.fromInt x + else + err (thing ^ " must be non-negative and less than 2^16"); + + fun packScreenDescriptorByte + { colorTableFlag: bool + , colorResolution: int + , sortFlag: bool + , colorTableSize: int + } = + let + open Word8 + infix 2 << orb andb + in + ((if colorTableFlag then 0w1 else 0w0) << 0w7) + orb + ((fromInt colorResolution andb 0wx7) << 0w4) + orb + ((if sortFlag then 0w1 else 0w0) << 0w3) + orb + (fromInt colorTableSize andb 0wx7) + end + + fun writeMany path delay palette {width, height, numImages, getImage} = + if numImages <= 0 then + err "Must be at least one image" + else + let + val width16 = checkToWord16 "width" width + val height16 = checkToWord16 "height" height + + val numberOfColors = Seq.length (#colors palette) + + val _ = + if numberOfColors <= 256 then () + else err "Must have at most 256 colors in the palette" + + val (imageData, tm) = Util.getTime (fn _ => + AS.full (SeqBasis.tabulate 1 (0, numImages) (fn i => + let + val img = getImage i + in + if Seq.length img <> height * width then + err "Not all images are the right dimensions" + else + LZW.packCodeStream numberOfColors + (LZW.codeStream numberOfColors img) + end))) + + (* val _ = print ("compressed image data in " ^ Time.fmt 4 tm ^ "s\n") *) + + val file = BinIO.openOut path + val w8 = ExtraBinIO.w8 file + val w32b = ExtraBinIO.w32b file + val w32l = ExtraBinIO.w32l file + val w16l = ExtraBinIO.w16l file + val wrgb = ExtraBinIO.wrgb file + in + (* ========================== + * "GIF89a" header: 6 bytes + *) + + List.app (w8 o Word8.fromInt) [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; + + (* =================================== + * logical screen descriptor: 7 bytes + *) + + w16l width16; + w16l height16; + + w8 (packScreenDescriptorByte + { colorTableFlag = true + , colorResolution = 1 + , sortFlag = false + , colorTableSize = (ceilLog2 numberOfColors) - 1 + }); + + w8 0w0; (* background color index. just use 0 for now. *) + + w8 0w0; (* pixel aspect ratio ?? *) + + (* =================================== + * global color table + *) + + Util.for (0, numberOfColors) (fn i => + wrgb (Seq.nth (#colors palette) i)); + + Util.for (numberOfColors, Util.boundPow2 numberOfColors) (fn i => + wrgb Color.black); + + (* ================================== + * application extension, for looping + * OPTIONAL. skip it if there is only one image. + *) + + if numImages = 1 then () else + List.app (w8 o Word8.fromInt) + [ 0x21, 0xFF, 0x0B, 0x4E, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32 + , 0x2E, 0x30, 0x03, 0x01, 0x00, 0x00, 0x00 + ]; + + (* ================================== + * IMAGE DATA + *) + + Util.for (0, numImages) (fn i => + let + val bytes = Seq.nth imageData i + in + (* ========================== + * graphics control extension. + * OPTIONAL. only needed if + * doing animation. + *) + + if numImages = 1 then () else + ( List.app (w8 o Word8.fromInt) [ 0x21, 0xF9, 0x04, 0x04 ] + ; w16l (Word16.fromInt delay) + ; w8 0w0 + ; w8 0w0 + ); + + (* ========================== + * image descriptor + *) + + w8 0wx2C; (* image separator *) + + w16l 0w0; (* image left *) + w16l 0w0; (* image top *) + + w16l width16; (* image width *) + w16l height16; (* image height *) + + w8 0w0; (* packed local color table descriptor (NONE FOR NOW) *) + + (* =========================== + * compressed image data + *) + + w8 (Word8.fromInt (ceilLog2 numberOfColors)); + Util.for (0, Seq.length bytes) (fn i => + w8 (Seq.nth bytes i)) + end); + + (* ================================ + * trailer + *) + + w8 0wx3B; + + BinIO.closeOut file + end + + fun write path img = + let + val palette = Palette.summarize [] 128 img + val img' = #remap palette img + in + writeMany path 0 palette + { width = #width img + , height = #height img + , numImages = 1 + , getImage = (fn _ => img') + } + end +end diff --git a/tests/mpllib/Geometry2D.sml b/tests/mpllib/Geometry2D.sml new file mode 100644 index 000000000..782d00a56 --- /dev/null +++ b/tests/mpllib/Geometry2D.sml @@ -0,0 +1,88 @@ +structure Geometry2D = +struct + + type point = real * real + + fun rtos x = Real.toString x + + fun toString (x, y) = + String.concat ["(", rtos x, ",", rtos y, ")"] + + fun samePoint (x1, y1) (x2, y2) = + Real.== (x1, x2) andalso Real.== (y1, y2) + + fun sq (x : real) = x*x + + fun distance ((x1,y1) : point) ((x2,y2) : point) = + Math.sqrt (sq (x2-x1) + sq (y2-y1)) + + fun quadrant ((cx, cy) : point) (x, y) = + if y < cy + then (if x < cx then 2 else 3) + else (if x < cx then 1 else 0) + (* *) + + structure Vector = + struct + type t = real * real + + val toString = toString + + fun add ((x1, y1), (x2, y2)) : t = (x1+x2, y1+y2) + fun sub ((x1, y1), (x2, y2)) : t = (x1-x2, y1-y2) + + fun dot ((x1, y1), (x2, y2)) : real = x1*x2 + y1*y2 + fun cross ((x1, y1), (x2, y2)) : real = x1*y2 - y1*x2 + + fun scaleBy a (x, y) : t = (a*x, a*y) + + fun length (x, y) = Math.sqrt (x*x + y*y) + + fun angle (u, v) = Math.atan2 (cross (u, v), dot (u, v)) + end + + structure Point = + struct + type t = point + + val toString = toString + + val add = Vector.add + val sub = Vector.sub + + fun minCoords ((a,b),(c,d)) = + (Real.min (a,c), Real.min (b,d)) + + fun maxCoords ((a,b),(c,d)) = + (Real.max (a,c), Real.max (b,d)) + + fun triArea (a, b, c) = + Vector.cross (sub (b, a), sub (c, a)) + + fun counterClockwise (a, b, c) = + triArea (a, b, c) > 0.0 + + (* Returns angle `r` inside the triangle formed by three points: + * b + * / \ + * / r \ + * a c + *) + fun triAngle (a, b, c) = + Vector.angle (sub (a, b), sub (c, b)) + + + fun inCircle (a, b, c) d = + let + fun onParabola ((x, y): point) : Geometry3D.Vector.t = + (x, y, x*x + y*y) + val ad = onParabola (Vector.sub (a, d)) + val bd = onParabola (Vector.sub (b, d)) + val cd = onParabola (Vector.sub (c, d)) + in + Geometry3D.Vector.dot (Geometry3D.Vector.cross (ad, bd), cd) > 0.0 + end + + end + +end diff --git a/tests/mpllib/Geometry3D.sml b/tests/mpllib/Geometry3D.sml new file mode 100644 index 000000000..08e47cfbe --- /dev/null +++ b/tests/mpllib/Geometry3D.sml @@ -0,0 +1,23 @@ +structure Geometry3D = +struct + + type point = real * real * real + + structure Vector = + struct + type t = real * real * real + + fun dot ((a1, a2, a3), (b1, b2, b3)) : real = + a1*b1 + a2*b2 + a3*b3 + + fun cross ((a1, a2, a3), (b1, b2, b3)) : t = + (a2*b3 - a3*b2, a3*b1 - a1*b3, a1*b2 - a2*b1) + + fun add ((a1, a2, a3), (b1, b2, b3)) : t = + (a1+b1, a2+b2, a3+b3) + + fun sub ((a1, a2, a3), (b1, b2, b3)) : t = + (a1-b1, a2-b2, a3-b3) + end + +end diff --git a/tests/mpllib/Hashset.sml b/tests/mpllib/Hashset.sml new file mode 100644 index 000000000..57f2d93cd --- /dev/null +++ b/tests/mpllib/Hashset.sml @@ -0,0 +1,111 @@ +structure Hashset: +sig + type 'a t + type 'a hashset = 'a t + + exception Full + + val make: + { hash: 'a -> int + , eq: 'a * 'a -> bool + , capacity: int + , maxload: real} -> 'a t + + val size: 'a t -> int + val capacity: 'a t -> int + val resize: 'a t -> 'a t + val insert: 'a t -> 'a -> bool + val to_list: 'a t -> 'a list +end = +struct + + +datatype 'a t = + S of + { data: 'a option array + , hash: 'a -> int + , eq: 'a * 'a -> bool + , maxload: real + } + + exception Full + + type 'a hashset = 'a t + + fun make {hash, eq, capacity, maxload} = + let + val data = SeqBasis.tabulate 5000 (0, capacity) (fn _ => NONE) + in + S {data=data, hash=hash, eq=eq, maxload = maxload} + end + + fun bcas (arr, i) (old, new) = + MLton.eq (old, Concurrency.casArray (arr, i) (old, new)) + + fun size (S {data, ...}) = + SeqBasis.reduce 10000 op+ 0 (0, Array.length data) (fn i => + if Option.isSome (Array.sub (data, i)) then 1 else 0) + + fun capacity (S {data, ...}) = Array.length data + + fun insert' (input as S {data, hash, eq, maxload}) x force = + let + val n = Array.length data + val start = (hash x) mod (Array.length data) + + val tolerance = + 2 * Real.ceil (1.0 / (1.0 - maxload)) + + fun loop i probes = + if not force andalso probes >= tolerance then + raise Full + else if i >= n then + loop 0 probes + else + let + val current = Array.sub (data, i) + in + case current of + SOME y => if eq (x, y) then false else loop (i+1) (probes+1) + | NONE => + if bcas (data, i) (current, SOME x) then + (* (Concurrency.faa sz 1; true) *) + true + else + loop i probes + end + + val start = (hash x) mod (Array.length data) + in + loop start 0 + end + + + fun insert s x = insert' s x false + + + fun resize (input as S {data, hash, eq, maxload}) = + let + val newcap = 2 * capacity input + val new = make {hash = hash, eq = eq, capacity = newcap, maxload = maxload} + in + ForkJoin.parfor 1000 (0, Array.length data) (fn i => + case Array.sub (data, i) of + NONE => () + | SOME x => (insert' new x true; ())); + + new + end + + + fun to_list (S {data, hash, eq, ...}) = + let + fun pushSome (elem, xs) = + case elem of + SOME x => x :: xs + | NONE => xs + in + Array.foldr pushSome [] data + end + +end diff --git a/tests/mpllib/Hashtable.sml b/tests/mpllib/Hashtable.sml new file mode 100644 index 000000000..19582a0b5 --- /dev/null +++ b/tests/mpllib/Hashtable.sml @@ -0,0 +1,127 @@ +structure Hashtable: +sig + type ('a, 'b) t + type ('a, 'b) hashtable = ('a, 'b) t + + val make: {hash: 'a -> int, eq: 'a * 'a -> bool, capacity: int} -> ('a, 'b) t + val insert: ('a, 'b) t -> ('a * 'b) -> unit + val insert_if_absent: ('a, 'b) t -> ('a * 'b) -> unit + + val lookup: ('a, 'b) t -> 'a -> 'b option + val to_list: ('a, 'b) t -> ('a * 'b) list + val keys_to_arr: ('a, 'b) t -> 'a array +end = +struct + + datatype ('a, 'b) t = + S of + { data: ('a * 'b) option array + , hash: 'a -> int + , eq: 'a * 'a -> bool + } + + type ('a, 'b) hashtable = ('a, 'b) t + + fun make {hash, eq, capacity} = + let + val data = SeqBasis.tabulate 5000 (0, capacity) (fn _ => NONE) + in + S {data=data, hash=hash, eq=eq} + end + + fun bcas (arr, i) (old, new) = + MLton.eq (old, Concurrency.casArray (arr, i) (old, new)) + + fun insert (S {data, hash, eq}) (k, v) = + let + val n = Array.length data + + fun loop i = + if i >= n then loop 0 else + let + val current = Array.sub (data, i) + val rightPlace = + case current of + NONE => true + | SOME (k', _) => eq (k, k') + in + if not rightPlace then + loop (i+1) + else if bcas (data, i) (current, SOME (k, v)) then + () + else + loop i + end + + val start = (hash k) mod (Array.length data) + in + loop start + end + + (* This function differs from the above in the case + where the key k is already in the hashtable. + If so, the function does not update the key's value + and is thus more efficient (saves cas). + *) + fun insert_if_absent (S {data, hash, eq}) (k, v) = + let + val n = Array.length data + + fun loop i = + if i >= n then loop 0 else + let + val current = Array.sub (data, i) + in + case current of + NONE => + if (bcas (data, i) (current, SOME (k, v))) then () + else loop i + | SOME (k', _) => + if eq (k, k') then () + else loop (i + 1) + end + + val start = (hash k) mod (Array.length data) + in + loop start + end + + fun lookup (S {data, hash, eq}) k = + let + val n = Array.length data + + fun loop i = + if i >= n then loop 0 else + case Array.sub (data, i) of + SOME (k', v) => if eq (k, k') then SOME v else loop (i+1) + | NONE => NONE + + val start = (hash k) mod (Array.length data) + in + loop start + end + + fun keys_to_arr (S{data, hash, eq}) = + let + val n = Array.length data + val gran = 10000 + val keys = SeqBasis.tabFilter gran (0, Array.length data) + (fn i => + case Array.sub (data, i) of + NONE => NONE + | SOME (k, _) => SOME k) + in + keys + end + + fun to_list (S {data, hash, eq}) = + let + fun pushSome (elem, xs) = + case elem of + SOME x => x :: xs + | NONE => xs + in + Array.foldr pushSome [] data + end + +end diff --git a/tests/mpllib/MatCOO.sml b/tests/mpllib/MatCOO.sml new file mode 100644 index 000000000..acfdc3d1f --- /dev/null +++ b/tests/mpllib/MatCOO.sml @@ -0,0 +1,681 @@ +(* MatCOO(I, R): + * - indices of type I.int + * - values of type R.real + * + * For example, the following defines matrices where the row and column indices + * will be arrays of C type int32_t*, and values of C type float* + * + * structure M = MatCOO(structure I = Int32 + * structure R = Real32) + * + * We can also use int64_t and doubles (or any other desired combination): + * + * structure M = MatCOO(structure I = Int64 + * structure R = Real64) + *) + +functor MatCOO + (structure I: INTEGER + structure R: + sig + include REAL + structure W: WORD + val castFromWord: W.word -> real + val castToWord: real -> W.word + end) = +struct + structure I = I + structure R = R + + (* SoA format for storing every nonzero value = mat[row,column], i.e.: + * (row, column, value) = (row_indices[i], col_indices[i], values[i]) + * + * so, number of nonzeros (nnz) + * = length(row_indices) + * = length(col_indices) + * = length(values) + * + * we assume row_indices is sorted + *) + datatype mat = + Mat of + { width: int + , height: int + , row_indices: I.int Seq.t + , col_indices: I.int Seq.t + , values: R.real Seq.t + } + + type t = mat + + fun width (Mat m) = #width m + fun height (Mat m) = #height m + + fun nnz (Mat m) = + Seq.length (#row_indices m) + + fun row_lo (mat as Mat m) = + if nnz mat = 0 then I.fromInt 0 else Seq.first (#row_indices m) + + fun row_hi (mat as Mat m) = + if nnz mat = 0 then I.fromInt 0 + else I.+ (I.fromInt 1, Seq.last (#row_indices m)) + + fun row_spread mat = + I.toInt (row_hi mat) - I.toInt (row_lo mat) + + fun split_seq s k = + (Seq.take s k, Seq.drop s k) + + fun undo_split_seq s1 s2 = + let + val (a1, i1, n1) = ArraySlice.base s1 + val (a2, i2, n2) = ArraySlice.base s2 + in + if MLton.eq (a1, a2) andalso i1 + n1 = i2 then + Seq.subseq (ArraySlice.full a1) (i1, n1 + n2) + else + raise Fail + ("undo_split_seq: arguments are not adjacent: " ^ Int.toString i1 + ^ " " ^ Int.toString n1 ^ " " ^ Int.toString i2 ^ " " + ^ Int.toString n2) + end + + + fun split_nnz k (mat as Mat m) = + let + val (r1, r2) = split_seq (#row_indices m) k + val (c1, c2) = split_seq (#col_indices m) k + val (v1, v2) = split_seq (#values m) k + + val m1 = Mat + { width = #width m + , height = #height m + , row_indices = r1 + , col_indices = c1 + , values = v1 + } + + val m2 = Mat + { width = #width m + , height = #height m + , row_indices = r2 + , col_indices = c2 + , values = v2 + } + in + (m1, m2) + end + + + (* split m -> (m1, m2) + * where m = m1 + m2 + * and nnz(m1) ~= frac * nnz(m) + * and nnz(m2) ~= (1-frac) * nnz(m) + *) + fun split frac mat = + let + val half = Real.floor (frac * Real.fromInt (nnz mat)) + val half = + if nnz mat <= 1 then half + else if half = 0 then half + 1 + else if half = nnz mat then half - 1 + else half + in + split_nnz half mat + end + + + (* might fail if the matrices were not created by a split *) + fun undo_split mat1 mat2 = + if nnz mat1 = 0 then + mat2 + else if nnz mat2 = 0 then + mat1 + else + let + val Mat m1 = mat1 + val Mat m2 = mat2 + in + if #width m1 <> #width m2 orelse #height m1 <> #height m2 then + raise Fail "undo_split: dimension mismatch" + else + Mat + { width = #width m1 + , height = #height m1 + , row_indices = undo_split_seq (#row_indices m1) (#row_indices m2) + , col_indices = undo_split_seq (#col_indices m1) (#col_indices m2) + , values = undo_split_seq (#values m1) (#values m2) + } + end + + + fun dump_info msg (Mat m) = + let + val info = String.concatWith " " + [ msg + , Seq.toString I.toString (#row_indices m) + , Seq.toString I.toString (#col_indices m) + , Seq.toString (R.fmt (StringCvt.FIX (SOME 1))) (#values m) + ] + in + print (info ^ "\n") + end + + + fun upd (a, i, x) = + ( (*print ("upd " ^ Int.toString i ^ "\n");*)Array.update (a, i, x)) + + + (* ======================================================================= + * write_mxv: serial and parallel versions + * + * The first and last row of the input `mat` might overlap with other + * parallel tasks, so these need to be returned separately and combined. + * + * All middle rows are "owned" by the call to write_mxv + *) + + datatype write_mxv_result = + SingleRowValue of R.real + | FirstLastRowValue of R.real * R.real + + + (* requires `row_lo mat < row_hi mat`, i.e., at least one row *) + fun write_mxv_serial mat vec output : write_mxv_result = + let + val Mat {row_indices, col_indices, values, ...} = mat + val n = nnz mat + in + if row_lo mat = I.- (row_hi mat, I.fromInt 1) then + (* no writes to output; only a single result row value *) + let + (* val _ = dump_info "write_mxv_serial (single row)" mat *) + val result = Util.loop (0, n) (R.fromInt 0) (fn (acc, i) => + R.+ (acc, R.* + (Seq.nth vec (I.toInt (Seq.nth col_indices i)), Seq.nth values i))) + in + SingleRowValue result + end + else + let + (* val _ = dump_info "write_mxv_serial (multi rows)" mat *) + fun single_row_loop r (i, acc) = + if i >= n orelse Seq.nth row_indices i <> r then + (i, acc) + else + let + val acc' = R.+ (acc, R.* + ( Seq.nth vec (I.toInt (Seq.nth col_indices i)) + , Seq.nth values i + )) + in + single_row_loop r (i + 1, acc') + end + + val last_row = I.- (row_hi mat, I.fromInt 1) + + fun advance_to_next_nonempty_row i' r = + let + val next_r = Seq.nth row_indices i' + in + Util.for (1 + I.toInt r, I.toInt next_r) (fn rr => + upd (output, rr, R.fromInt 0)); + next_r + end + + fun middle_loop r i = + if r = last_row then + i + else + let + val (i', row_value) = single_row_loop r (i, R.fromInt 0) + in + upd (output, I.toInt r, row_value); + middle_loop (advance_to_next_nonempty_row i' r) i' + end + + val (i, first_row_value) = + single_row_loop (row_lo mat) (0, R.fromInt 0) + val i = middle_loop (advance_to_next_nonempty_row i (row_lo mat)) i + val (_, last_row_value) = single_row_loop last_row (i, R.fromInt 0) + in + FirstLastRowValue (first_row_value, last_row_value) + end + end + + val nnzGrain = 5000 + + + fun write_mxv_combine_results (m1, m2) (result1, result2) output = + if I.- (row_hi m1, I.fromInt 1) = row_lo m2 then + case (result1, result2) of + (SingleRowValue r1, SingleRowValue r2) => SingleRowValue (R.+ (r1, r2)) + | (SingleRowValue r1, FirstLastRowValue (f2, l2)) => + FirstLastRowValue (R.+ (r1, f2), l2) + | (FirstLastRowValue (f1, l1), SingleRowValue r2) => + FirstLastRowValue (f1, R.+ (l1, r2)) + | (FirstLastRowValue (f1, l1), FirstLastRowValue (f2, l2)) => + (* overlap *) + ( (*print "fill in middle overlap\n" + ;*) upd (output, I.toInt (row_lo m2), R.+ (l1, f2)) + ; FirstLastRowValue (f1, l2) + ) + else + let + fun finish_l1 v = + upd (output, I.toInt (row_hi m1) - 1, v) + fun finish_f2 v = + upd (output, I.toInt (row_lo m2), v) + fun fill_middle () = + ForkJoin.parfor 5000 (I.toInt (row_hi m1), I.toInt (row_lo m2)) + (fn r => upd (output, r, R.fromInt 0)) + in + (* print "fill in middle, no overlap\n"; *) + case (result1, result2) of + (SingleRowValue r1, SingleRowValue r2) => + (fill_middle (); FirstLastRowValue (r1, r2)) + + | (SingleRowValue r1, FirstLastRowValue (f2, l2)) => + (fill_middle (); finish_f2 f2; FirstLastRowValue (r1, l2)) + + | (FirstLastRowValue (f1, l1), SingleRowValue r2) => + (finish_l1 l1; fill_middle (); FirstLastRowValue (f1, r2)) + + | (FirstLastRowValue (f1, l1), FirstLastRowValue (f2, l2)) => + ( finish_l1 l1 + ; fill_middle () + ; finish_f2 f2 + ; FirstLastRowValue (f1, l2) + ) + end + + + (* requires `row_lo mat < row_hi mat`, i.e., at least one row *) + fun write_mxv mat vec output : write_mxv_result = + if nnz mat <= nnzGrain then + write_mxv_serial mat vec output + else + let + val (m1, m2) = split 0.5 mat + val (result1, result2) = + ForkJoin.par (fn _ => write_mxv m1 vec output, fn _ => + write_mxv m2 vec output) + + in + write_mxv_combine_results (m1, m2) (result1, result2) output + end + + + fun mxv (mat: mat) (vec: R.real Seq.t) = + if nnz mat = 0 then + Seq.tabulate (fn _ => R.fromInt 0) (Seq.length vec) + else + let + val output: R.real array = ForkJoin.alloc (Seq.length vec) + val result = write_mxv mat vec output + in + (* print "top level: fill in front\n"; *) + ForkJoin.parfor 5000 (0, I.toInt (row_lo mat)) (fn r => + upd (output, r, R.fromInt 0)); + (* print "top-level: fill in middle\n"; *) + case result of + SingleRowValue r => upd (output, I.toInt (row_lo mat), r) + | FirstLastRowValue (f, l) => + ( upd (output, I.toInt (row_lo mat), f) + ; upd (output, I.toInt (row_hi mat) - 1, l) + ); + (* print "top-level: fill in back\n"; *) + ForkJoin.parfor 5000 (I.toInt (row_hi mat), Seq.length vec) (fn r => + upd (output, r, R.fromInt 0)); + ArraySlice.full output + end + + (* ======================================================================= *) + (* ======================================================================= *) + (* ======================================================================= + * to/from file + *) + + structure DS = DelayedSeq + + fun fromMatrixMarketFile path chars = + let + val lines = ParseFile.tokens (fn c => c = #"\n") chars + val numLines = DS.length lines + fun line i : char DS.t = DS.nth lines i + fun lineStr i : string = + let val ln = line i + in CharVector.tabulate (DS.length ln, DS.nth ln) + end + + fun lineIsComment i = + let val ln = line i + in DS.length ln > 0 andalso DS.nth ln 0 = #"%" + end + + val _ = + if + numLines > 0 + andalso + ParseFile.eqStr "%%MatrixMarket matrix coordinate real general" + (line 0) + then () + else raise Fail ("MatCOO.fromFile: not sure how to parse file: " ^ path) + + val firstNonCommentLineNum = + case FindFirst.findFirst 1000 (1, numLines) (not o lineIsComment) of + SOME i => i + | NONE => raise Fail ("MatCOO.fromFile: missing contents?") + + fun fail () = + raise Fail + ("MatCOO.fromFile: error parsing line " + ^ Int.toString (1 + firstNonCommentLineNum) + ^ ": expected ") + val (numRows, numCols, numValues) = + let + val lineNum = firstNonCommentLineNum + val lnChars = DS.toArraySeq (line lineNum) + val toks = ParseFile.tokens Char.isSpace lnChars + val nr = valOf (ParseFile.parseInt (DS.nth toks 0)) + val nc = valOf (ParseFile.parseInt (DS.nth toks 1)) + val nv = valOf (ParseFile.parseInt (DS.nth toks 2)) + in + (nr, nc, nv) + end + handle _ => fail () + + val _ = print ("num rows " ^ Int.toString numRows ^ "\n") + val _ = print ("num cols " ^ Int.toString numCols ^ "\n") + val _ = print ("num nonzero " ^ Int.toString numValues ^ "\n") + val _ = print ("parsing elements (may take a while...)\n") + + val row_indices = ForkJoin.alloc numValues + val col_indices = ForkJoin.alloc numValues + val values = ForkJoin.alloc numValues + + fun fail lineNum = + raise Fail + ("MatCOO.fromFile: error parsing line " ^ Int.toString (1 + lineNum) + ^ ": expected ") + + (* TODO: this is very slow *) + fun parseValue i = + let + val lineNum = firstNonCommentLineNum + 1 + i + val lnChars = DS.toArraySeq (line lineNum) + val toks = ParseFile.tokens Char.isSpace lnChars + val r = I.fromInt (valOf (ParseFile.parseInt (DS.nth toks 0))) + val c = I.fromInt (valOf (ParseFile.parseInt (DS.nth toks 1))) + val v = R.fromLarge IEEEReal.TO_NEAREST (valOf (ParseFile.parseReal + (DS.nth toks 2))) + + (* val ln = line lineNum + val chars = CharVector.tabulate (DS.length ln, DS.nth ln) + val toks = String.tokens Char.isSpace chars + val r = I.fromInt (valOf (Int.fromString (List.nth (toks, 0)))) + val c = I.fromInt (valOf (Int.fromString (List.nth (toks, 1)))) + val v = R.fromLarge IEEEReal.TO_NEAREST (valOf (Real.fromString + (List.nth (toks, 2)))) *) + in + (* if i mod 500000 = 0 then + print ("finished row " ^ Int.toString i ^ "\n") + else + (); *) + + (* coordinates are stored in .mtx files using 1-indexing, but we + * want 0-indexing + *) + Array.update (row_indices, i, I.- (r, I.fromInt 1)); + Array.update (col_indices, i, I.- (c, I.fromInt 1)); + Array.update (values, i, v) + end + handle _ => fail (firstNonCommentLineNum + 1 + i) + + val _ = ForkJoin.parfor 1000 (0, numValues) parseValue + val _ = print ("finished parsing elements\n") + val _ = print ("formatting...\n") + + val getSorted = + let + val idx = Seq.tabulate (fn i => i) numValues + in + StableSort.sortInPlace + (fn (i, j) => + I.compare + (Array.sub (row_indices, i), Array.sub (row_indices, j))) idx; + fn i => Seq.nth idx i + end + + val row_indices = + Seq.tabulate (fn i => Array.sub (row_indices, getSorted i)) numValues + val col_indices = + Seq.tabulate (fn i => Array.sub (col_indices, getSorted i)) numValues + val values = + Seq.tabulate (fn i => Array.sub (values, getSorted i)) numValues + + val _ = print ("done parsing " ^ path ^ "\n") + in + Mat + { width = numCols + , height = numRows + , row_indices = row_indices + , col_indices = col_indices + , values = values + } + end + + + (* MatrixCoordinateRealBin\n + * [8 bits unsigned: real value precision, either 32 or 64] + * [64 bits unsigned: number of rows] + * [64 bits unsigned: number of columns] + * [64 bits unsigned: number of elements] + * [element] + * [element] + * ... + * + * each element is as follows, where X is the real value precision (32 or 64) + * [64 bits unsigned: row index][64 bits unsigned: col index][X bits: value] + *) + fun writeBinFile mat path = + let + val file = TextIO.openOut path + val _ = TextIO.output (file, "MatrixCoordinateRealBin\n") + val _ = TextIO.closeOut file + + val file = BinIO.openAppend path + + fun w8 (w: Word8.word) = BinIO.output1 (file, w) + + fun w32 (w: Word64.word) = + let + open Word64 + infix 2 >> andb + in + w8 (Word8.fromLarge (w >> 0w24)); + w8 (Word8.fromLarge (w >> 0w16)); + w8 (Word8.fromLarge (w >> 0w8)); + w8 (Word8.fromLarge w) + end + + fun w64 (w: Word64.word) = + let + open Word64 + infix 2 >> andb + in + (* this will only work if Word64 = LargeWord, which is good. *) + w8 (Word8.fromLarge (w >> 0w56)); + w8 (Word8.fromLarge (w >> 0w48)); + w8 (Word8.fromLarge (w >> 0w40)); + w8 (Word8.fromLarge (w >> 0w32)); + w8 (Word8.fromLarge (w >> 0w24)); + w8 (Word8.fromLarge (w >> 0w16)); + w8 (Word8.fromLarge (w >> 0w8)); + w8 (Word8.fromLarge w) + end + + fun wr64 (r: R.real) = + w64 (R.W.toLarge (R.castToWord r)) + fun wr32 (r: R.real) = + w32 (R.W.toLarge (R.castToWord r)) + + val (wr, rsize) = + case R.W.wordSize of + 32 => (wr32, 0w32) + | 64 => (wr64, 0w64) + | _ => + raise Fail + "MatCOO.writeBinFile: only 32-bit and 64-bit reals supported" + in + w8 rsize; + w64 (Word64.fromInt (height mat)); + w64 (Word64.fromInt (width mat)); + w64 (Word64.fromInt (nnz mat)); + Util.for (0, nnz mat) (fn i => + let + val Mat {row_indices, col_indices, values, ...} = mat + val r = Seq.nth row_indices i + val c = Seq.nth col_indices i + val v = Seq.nth values i + in + w64 (Word64.fromInt (I.toInt r)); + w64 (Word64.fromInt (I.toInt c)); + wr v + end); + BinIO.closeOut file + end + + + fun fromBinFile path bytes = + let + val header = "MatrixCoordinateRealBin\n" + val header' = + if Seq.length bytes < String.size header then + raise Fail ("MatCOO.fromBinFile: missing or incomplete header") + else + CharVector.tabulate (String.size header, fn i => + Char.chr (Word8.toInt (Seq.nth bytes i))) + val _ = + if header = header' then + () + else + raise Fail + ("MatCOO.fromBinFile: expected MatrixCoordinateRealBin header") + + val bytes = Seq.drop bytes (String.size header) + + fun r64 off = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes off) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 1))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 2))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 3))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 4))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 5))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 6))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 7))) + in + w + end + + fun r32 off = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes off) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 1))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 2))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (off + 3))) + in + w + end + + fun r8 off = Seq.nth bytes off + fun rr32 off = + R.castFromWord (R.W.fromLarge (r32 off)) + fun rr64 off = + R.castFromWord (R.W.fromLarge (r64 off)) + + (* ==================================================================== + * parse binary contents + *) + + val rsize = Word8.toInt (r8 0) + + fun rsizeFail () = + raise Fail + ("MatCOO.fromBinFile: found " ^ Int.toString rsize + ^ "-bit reals, but expected " ^ Int.toString R.W.wordSize ^ "-bit") + + val (rr, rbytes) = + if rsize = R.W.wordSize then + case rsize of + 32 => (rr32, 4) + | 64 => (rr64, 8) + | _ => rsizeFail () + else + rsizeFail () + + val elemSize = 8 + 8 + rbytes + val elemStartOff = 1 + 8 + 8 + 8 + + val height = Word64.toInt (r64 (1 + 0)) + val width = Word64.toInt (r64 (1 + 8)) + val numValues = Word64.toInt (r64 (1 + 8 + 8)) + + val row_indices = ForkJoin.alloc numValues + val col_indices = ForkJoin.alloc numValues + val values = ForkJoin.alloc numValues + in + ForkJoin.parfor 5000 (0, numValues) (fn i => + let + val off = elemStartOff + i * elemSize + val r = I.fromInt (Word64.toInt (r64 off)) + val c = I.fromInt (Word64.toInt (r64 (off + 8))) + val v = rr (off + 8 + 8) + in + Array.update (row_indices, i, r); + Array.update (col_indices, i, c); + Array.update (values, i, v) + end); + + Mat + { width = width + , height = height + , row_indices = ArraySlice.full row_indices + , col_indices = ArraySlice.full col_indices + , values = ArraySlice.full values + } + end + + + fun fromFile path = + let + val file = TextIO.openIn path + val _ = print ("loading " ^ path ^ "\n") + + val h1 = "%%MatrixMarket" + val h2 = "MatrixCoordinateRealBin\n" + + val actualHeader = TextIO.inputN + (file, Int.max (String.size h1, String.size h2)) + in + TextIO.closeIn file; + + if String.isPrefix h1 actualHeader then + fromMatrixMarketFile path (ReadFile.contentsSeq path) + else if String.isPrefix h2 actualHeader then + fromBinFile path (ReadFile.contentsBinSeq path) + else + raise Fail ("unknown header " ^ actualHeader) + end + +end diff --git a/tests/mpllib/Merge.sml b/tests/mpllib/Merge.sml new file mode 100644 index 000000000..a2f23fcdc --- /dev/null +++ b/tests/mpllib/Merge.sml @@ -0,0 +1,98 @@ +structure Merge: +sig + type 'a seq = 'a ArraySlice.slice + + val writeMergeSerial: ('a * 'a -> order) (* compare *) + -> 'a seq * 'a seq (* (sorted) sequences to merge *) + -> 'a seq (* output *) + -> unit + + val writeMerge: ('a * 'a -> order) (* compare *) + -> 'a seq * 'a seq (* (sorted) sequences to merge *) + -> 'a seq (* output *) + -> unit + + val mergeSerial: ('a * 'a -> order) -> 'a seq * 'a seq -> 'a seq + val merge: ('a * 'a -> order) -> 'a seq * 'a seq -> 'a seq +end = +struct + + structure AS = ArraySlice + type 'a seq = 'a AS.slice + + val for = Util.for + val parfor = ForkJoin.parfor + val par = ForkJoin.par + val allocate = ForkJoin.alloc + + val serialGrain = CommandLineArgs.parseInt "MPLLib_Merge_serialGrain" 4000 + + fun sliceIdxs s i j = + AS.subslice (s, i, SOME (j - i)) + + fun writeMergeSerial cmp (s1, s2) t = + let + fun write i x = AS.update (t, i, x) + + val n1 = AS.length s1 + val n2 = AS.length s2 + + (* i1 index into s1 + * i2 index into s2 + * j index into output *) + fun loop i1 i2 j = + if i1 = n1 then + Util.foreach (sliceIdxs s2 i2 n2) (fn (i, x) => write (i + j) x) + else if i2 = n2 then + Util.foreach (sliceIdxs s1 i1 n1) (fn (i, x) => write (i + j) x) + else + let + val x1 = AS.sub (s1, i1) + val x2 = AS.sub (s2, i2) + in + case cmp (x1, x2) of + LESS => (write j x1; loop (i1 + 1) i2 (j + 1)) + | _ => (write j x2; loop i1 (i2 + 1) (j + 1)) + end + in + loop 0 0 0 + end + + fun mergeSerial cmp (s1, s2) = + let val out = AS.full (allocate (AS.length s1 + AS.length s2)) + in writeMergeSerial cmp (s1, s2) out; out + end + + fun writeMerge cmp (s1, s2) t = + if AS.length t <= serialGrain then + writeMergeSerial cmp (s1, s2) t + else if AS.length s1 = 0 then + Util.foreach s2 (fn (i, x) => AS.update (t, i, x)) + else + let + val n1 = AS.length s1 + val n2 = AS.length s2 + val mid1 = n1 div 2 + val pivot = AS.sub (s1, mid1) + val mid2 = BinarySearch.search cmp s2 pivot + + val l1 = sliceIdxs s1 0 mid1 + val r1 = sliceIdxs s1 (mid1 + 1) n1 + val l2 = sliceIdxs s2 0 mid2 + val r2 = sliceIdxs s2 mid2 n2 + + val _ = AS.update (t, mid1 + mid2, pivot) + val tl = sliceIdxs t 0 (mid1 + mid2) + val tr = sliceIdxs t (mid1 + mid2 + 1) (AS.length t) + in + par (fn _ => writeMerge cmp (l1, l2) tl, fn _ => + writeMerge cmp (r1, r2) tr); + () + end + + fun merge cmp (s1, s2) = + let val out = AS.full (allocate (AS.length s1 + AS.length s2)) + in writeMerge cmp (s1, s2) out; out + end + +end diff --git a/tests/mpllib/Mergesort.sml b/tests/mpllib/Mergesort.sml new file mode 100644 index 000000000..fc2a1438c --- /dev/null +++ b/tests/mpllib/Mergesort.sml @@ -0,0 +1,69 @@ +structure Mergesort: +sig + type 'a seq = 'a ArraySlice.slice + val sortInPlace: ('a * 'a -> order) -> 'a seq -> unit + val sort: ('a * 'a -> order) -> 'a seq -> 'a seq +end = +struct + + type 'a seq = 'a ArraySlice.slice + + structure AS = ArraySlice + + fun take s n = AS.subslice (s, 0, SOME n) + fun drop s n = AS.subslice (s, n, NONE) + + val par = ForkJoin.par + val allocate = ForkJoin.alloc + + (* in-place sort s, using t as a temporary array if needed *) + fun sortInPlace' cmp s t = + if AS.length s <= 1024 then + Quicksort.sortInPlace cmp s + else let + val half = AS.length s div 2 + val (sl, sr) = (take s half, drop s half) + val (tl, tr) = (take t half, drop t half) + in + (* recursively sort, writing result into t *) + par (fn _ => writeSort cmp sl tl, fn _ => writeSort cmp sr tr); + (* merge back from t into s *) + Merge.writeMerge cmp (tl, tr) s; + () + end + + (* destructively sort s, writing the result in t *) + and writeSort cmp s t = + if AS.length s <= 1024 then + ( Util.foreach s (fn (i, x) => AS.update (t, i, x)) + ; Quicksort.sortInPlace cmp t + ) + else let + val half = AS.length s div 2 + val (sl, sr) = (take s half, drop s half) + val (tl, tr) = (take t half, drop t half) + in + (* recursively in-place sort sl and sr *) + par (fn _ => sortInPlace' cmp sl tl, fn _ => sortInPlace' cmp sr tr); + (* merge into t *) + Merge.writeMerge cmp (sl, sr) t; + () + end + + fun sortInPlace cmp s = + let + val t = AS.full (allocate (AS.length s)) + in + sortInPlace' cmp s t + end + + fun sort cmp s = + let + val result = AS.full (allocate (AS.length s)) + in + Util.foreach s (fn (i, x) => AS.update (result, i, x)); + sortInPlace cmp result; + result + end + +end diff --git a/tests/mpllib/MeshToImage.sml b/tests/mpllib/MeshToImage.sml new file mode 100644 index 000000000..a4c373d76 --- /dev/null +++ b/tests/mpllib/MeshToImage.sml @@ -0,0 +1,278 @@ +structure MeshToImage: +sig + val toImage: + { mesh: Topology2D.mesh + , resolution: int + , cavities: (Geometry2D.point * Topology2D.cavity) Seq.t option + , background: Color.color + } + -> PPM.image +end = +struct + + structure T = Topology2D + structure G = Geometry2D + + fun inRange (a, b) x = + Real.min (a, b) <= x andalso x <= Real.max (a, b) + + fun xIntercept (x0,y0) (x1,y1) y = + if not (inRange (y0, y1) y) orelse Real.== (y0, y1) then + NONE + else + let + val x = x0 + (y - y0) * ((x1-x0)/(y1-y0)) + in + if inRange (x0, x1) x then SOME x else NONE + end + + fun rocmp (xo, yo) = + case (xo, yo) of + (SOME x, SOME y) => Real.compare (x, y) + | (NONE, SOME _) => GREATER + | (SOME _, NONE) => LESS + | _ => EQUAL + + fun sort3 cmp (a, b, c) = + let + fun lt (x, y) = case cmp (x, y) of LESS => true | _ => false + val (a, b) = if lt (a, b) then (a, b) else (b, a) + val (a, c) = if lt (a, c) then (a, c) else (c, a) + val (b, c) = if lt (b, c) then (b, c) else (c, b) + in + (a, b, c) + end + + fun toImage {mesh, resolution, cavities, background} = + let + val points = T.getPoints mesh + + val width = resolution + val height = resolution + + val niceGray = Color.hsva {h=0.0, s=0.0, v=0.88, a=1.0} + (* val white = Color.hsva {h=0.0, s=0.0, v=1.0, a=1.0} *) + val black = Color.hsva {h=0.0, s=0.0, v=0.0, a=1.0} + val red = Color.hsva {h=0.0, s=1.0, v=1.0, a=1.0} + + val niceRed = Color.hsva {h = 0.0, s = 0.55, v = 0.95, a = 0.55} + val niceBlue = Color.hsva {h = 240.0, s = 0.55, v = 0.95, a = 0.55} + + fun alphaGray a = + {red = 0.5, blue = 0.5, green = 0.5, alpha = a} + (* Color.hsva {h = 0.0, s = 0.0, v = 0.7, a = a} *) + + fun alphaRed a = + {red = 1.0, blue = 0.0, green = 0.0, alpha = a} + + val image = + { width = width + , height = height + , data = Seq.tabulate (fn _ => background) (width*height) + } + + fun set (i, j) x = + if 0 <= i andalso i < height andalso + 0 <= j andalso j < width + then ArraySlice.update (#data image, i*width + j, x) + else () + + fun setxy (x, y) z = + set (resolution - y - 1, x) z + + fun modify (i, j) f = + if 0 <= i andalso i < height andalso + 0 <= j andalso j < width + then + let + val k = i*width + j + val a = #data image + in + ArraySlice.update (a, k, f (ArraySlice.sub (a, k))) + end + else () + + fun modifyxy (x, y) f = + modify (resolution - y - 1, x) f + + fun overlay (x, y) color = + modifyxy (x, y) (fn bg => Color.overlayColor {fg = color, bg = bg}) + + val r = Real.fromInt resolution + fun px x = Real.floor (x * r + 0.5) + + fun vpos v = T.vdata mesh v + + fun ipart x = Real.floor x + fun fpart x = x - Real.realFloor x + fun rfpart x = 1.0 - fpart x + + (** input points should be in range [0,1] *) + fun aaLine colorFn (x0, y0) (x1, y1) = + if x1 < x0 then aaLine colorFn (x1, y1) (x0, y0) else + let + (** scale to resolution *) + val (x0, y0, x1, y1) = (r*x0 + 0.5, r*y0 + 0.5, r*x1 + 0.5, r*y1 + 0.5) + + fun plot (x, y, c) = + overlay (x, y) (colorFn c) + + val dx = x1-x0 + val dy = y1-y0 + val yxSlope = dy / dx + val xySlope = dx / dy + (* val xhop = Real.fromInt (Real.sign dx) *) + (* val yhop = Real.fromInt (Real.sign dy) *) + + (* fun y x = x0 + (x-x0) * slope *) + + (** (x,y) = current point on the line *) + fun normalLoop (x, y) = + if x > x1 then () else + ( plot (ipart x, ipart y , rfpart y) + ; plot (ipart x, ipart y + 1, fpart y) + ; normalLoop (x + 1.0, y + yxSlope) + ) + + fun steepUpLoop (x, y) = + if y > y1 then () else + ( plot (ipart x , ipart y, rfpart x) + ; plot (ipart x + 1, ipart y, fpart x) + ; steepUpLoop (x + xySlope, y + 1.0) + ) + + fun steepDownLoop (x, y) = + if y < y1 then () else + ( plot (ipart x , ipart y, rfpart x) + ; plot (ipart x + 1, ipart y, fpart x) + ; steepDownLoop (x - xySlope, y - 1.0) + ) + in + if Real.abs dx > Real.abs dy then + normalLoop (x0, y0) + else if y1 > y0 then + steepUpLoop (x0, y0) + else + steepDownLoop (x0, y0) + end + + fun adjust (x, y) = (r*x + 0.5, r*y + 0.5) + + fun fillTriangle color (p0, p1, p2) = + let + val (p0, p1, p2) = (adjust p0, adjust p1, adjust p2) + + (** min and max corners of bounding box *) + val (xlo, ylo) = List.foldl G.Point.minCoords p0 [p1, p2] + val (xhi, yhi) = List.foldl G.Point.maxCoords p0 [p1, p2] + + fun horizontalIntersect y = + let + val xa = xIntercept p0 p1 y + val xb = xIntercept p1 p2 y + val xc = xIntercept p0 p2 y + in + case sort3 rocmp (xa, xb, xc) of + (SOME xa, SOME xb, NONE) => SOME (xa, xb) + | _ => NONE + (* | (SOME xa, NONE, NONE) => (xa, xa) + | _ => raise Fail "MeshToImage.horizontalIntersect bug" *) + end + + fun loop y = + if y >= yhi then () else + let + val yy = ipart y + in + (case horizontalIntersect y of + SOME (xleft, xright) => + Util.for (ipart xleft, ipart xright + 1) + (fn xx => overlay (xx, yy) color) + | NONE => ()); + + loop (y+1.0) + end + in + loop (Real.realCeil ylo) + end + + in + (* draw all triangle edges as straight red lines *) + ForkJoin.parfor 1000 (0, T.numTriangles mesh) (fn i => + let + (** cut off anything that is outside the image (not important other than + * a little faster this way). + *) + (* fun constrain (x, y) = + (Real.min (1.0, Real.max (0.0, x)), Real.min (1.0, Real.max (0.0, y))) *) + (* fun vpos v = constrain (T.vdata mesh v) *) + + fun doLineIf b (u, v) = + if b then aaLine alphaGray (vpos u) (vpos v) else () + + val T.Tri {vertices=(u,v,w), neighbors=(a,b,c)} = T.tdata mesh i + in + (* skip "invalid" triangles *) + if u < 0 orelse v < 0 orelse w < 0 then () + else + (** This ensures that each line segment is only drawn once. The person + * responsible for drawing it is the triangle with larger id. + *) + ( doLineIf (i > a) (w, u) + ; doLineIf (i > b) (u, v) + ; doLineIf (i > c) (v, w) + ) + end); + + (* maybe fill in cavities *) + case cavities of NONE => () | SOME cavs => + ForkJoin.parfor 100 (0, Seq.length cavs) (fn i => + let + val (pt, (center, simps)) = Seq.nth cavs i + val triangles = center :: List.map (fn (t, _) => t) simps + + val perimeter = + List.map (T.vdata mesh) + ((let val (u,v,w) = T.verticesOfTriangle mesh center + in [u,v,w] + end) + @ + (List.map (fn s => T.firstVertex mesh (T.rotateClockwise s)) simps)) + + fun fillTri t = + let + val (v0,v1,v2) = T.verticesOfTriangle mesh t + val (p0,p1,p2) = (T.vdata mesh v0, T.vdata mesh v1, T.vdata mesh v2) + in + fillTriangle niceBlue (p0, p1, p2) + end + in + List.app fillTri triangles; + List.app (aaLine alphaRed pt) perimeter + end); + + (* mark input points as a pixel *) + ForkJoin.parfor 10000 (0, Seq.length points) (fn i => + let + val (x, y) = Seq.nth points i + val (x, y) = (px x, px y) + fun b spot = setxy spot black + in + (* skip "invalid" vertices *) + if T.triangleOfVertex mesh i < 0 then () + else + ( b (x-1, y) + ; b (x, y-1) + ; b (x, y) + ; b (x, y+1) + ; b (x+1, y) + ) + end); + + { width = #width image + , height = #height image + , data = Seq.map Color.colorToPixel (#data image) + } + end + +end diff --git a/tests/mpllib/MkComplex.sml b/tests/mpllib/MkComplex.sml new file mode 100644 index 000000000..2ff3b9df2 --- /dev/null +++ b/tests/mpllib/MkComplex.sml @@ -0,0 +1,111 @@ +signature COMPLEX = +sig + structure R: REAL + type r = R.real + + type t + + val toString: t -> string + + val make: (r * r) -> t + val view: t -> (r * r) + + val defaultReal: Real.real -> t + val defaultImag: Real.real -> t + + val real: r -> t + val imag: r -> t + val rotateBy: r -> t (* rotateBy x = e^(ix) *) + + val zeroThreshold: r + val realIsZero: r -> bool + + val isZero: t -> bool + val isNonZero: t -> bool + + val zero: t + val i: t + + val magnitude: t -> r + + val ~ : t -> t + val - : t * t -> t + val + : t * t -> t + val * : t * t -> t + + val scale: r * t -> t +end + + +functor MkComplex(R: REAL): COMPLEX = +struct + structure R = R + open R + type r = real + + val fromLarge = fromLarge IEEEReal.TO_NEAREST + + datatype t = C of {re: real, im: real} + + val rtos = fmt (StringCvt.FIX (SOME 8)) + + fun toString (C {re, im}) = + let + val (front, re) = if Int.< (R.sign re, 0) then ("-", R.~ re) else ("", re) + val (middle, im) = + if Int.< (R.sign im, 0) then ("-", R.~ im) else ("+", im) + in + front ^ rtos re ^ middle ^ rtos im ^ "i" + end + + fun make (re, im) = C {re = re, im = im} + + fun view (C {re, im}) = (re, im) + + val zeroThreshold = fromLarge 0.00000001 + fun realIsZero x = R.abs x < zeroThreshold + + fun magnitude (C {re, im}) = + R.Math.sqrt (R.+ (R.* (re, re), R.* (im, im))) + + fun isZero (C {re, im}) = realIsZero re andalso realIsZero im + + fun isNonZero c = + not (isZero c) + + fun rotateBy r = + C {re = Math.cos r, im = Math.sin r} + + fun real r = + C {re = r, im = fromLarge 0.0} + fun imag i = + C {re = fromLarge 0.0, im = i} + + fun defaultReal r = + real (fromLarge r) + fun defaultImag r = + imag (fromLarge r) + + val zero = C {re = fromLarge 0.0, im = fromLarge 0.0} + val i = C {re = fromLarge 0.0, im = fromLarge 1.0} + + fun neg (C {re, im}) = + C {re = ~re, im = ~im} + + fun add (C x, C y) = + C {re = #re x + #re y, im = #im x + #im y} + + fun sub (C x, C y) = + C {re = #re x - #re y, im = #im x - #im y} + + fun mul (C {re = a, im = b}, C {re = c, im = d}) = + C {re = a * c - b * d, im = a * d + b * c} + + fun scale (r, C {re, im}) = + C {re = r * re, im = r * im} + + val ~ = neg + val op- = sub + val op+ = add + val op* = mul +end diff --git a/tests/mpllib/MkGrep.sml b/tests/mpllib/MkGrep.sml new file mode 100644 index 000000000..d1438ac8e --- /dev/null +++ b/tests/mpllib/MkGrep.sml @@ -0,0 +1,105 @@ +functor MkGrep (Seq: SEQUENCE) : +sig + val grep: char ArraySequence.t (* pattern *) + -> char ArraySequence.t (* source text *) + -> (int * int) ArraySequence.t (* output line ranges *) +end = +struct + + structure ASeq = ArraySequence + + type 'a seq = 'a ASeq.t + +(* + fun lines (s: char seq) : (char seq) Seq.seq = + let + val n = ASeq.length s + val indices = Seq.tabulate (fn i => i) n + fun isNewline i = (ASeq.nth s i = #"\n") + val locs = Seq.filter isNewline indices + val m = Seq.length locs + + fun line i = + let + val lo = (if i = 0 then 0 else 1 + Seq.nth locs (i-1)) + val hi = (if i = m then n else Seq.nth locs i) + in + ASeq.subseq s (lo, hi-lo) + end + in + Seq.tabulate line (m+1) + end +*) + + (* check if line[i..] matches the pattern *) + fun checkMatch pattern line i = + (i + ASeq.length pattern <= ASeq.length line) andalso + let + val m = ASeq.length pattern + (* pattern[j..] matches line[i+j..] *) + fun matchesFrom j = + (j >= m) orelse + ((ASeq.nth line (i+j) = ASeq.nth pattern j) andalso matchesFrom (j+1)) + in + matchesFrom 0 + end + +(* + fun grep pat source = + let + val granularity = CommandLineArgs.parseOrDefaultInt "granularity" 1000 + (* val ff = FindFirst.findFirst granularity *) + val ff = FindFirst.findFirstSerial + fun containsPat line = + case ff (0, ASeq.length line) (checkMatch pat line) of + NONE => false + | SOME _ => true + + val linesWithPat = Seq.filter containsPat (lines source) + val newln = Seq.singleton #"\n" + + fun choose i = + if Util.even i + then Seq.fromArraySeq (Seq.nth linesWithPat (i div 2)) + else newln + in + Seq.toArraySeq (Seq.flatten (Seq.tabulate choose (2 * Seq.length linesWithPat))) + end +*) + + fun isNewline c = (c = #"\n") + val ff = FindFirst.findFirst 1000 + + fun grep pat s = + let + fun makeLine (start, stop) = ASeq.subseq s (start, stop-start) + fun containsPat (start, stop) = + case ff (0, stop-start) (checkMatch pat (makeLine (start, stop))) of + NONE => NONE + | SOME _ => SOME (start, stop) + + val s = Seq.fromArraySeq s + val n = Seq.length s + + val idx = Seq.filter (isNewline o Seq.nth s) (Seq.tabulate (fn i => i) n) + (* val idx = + Seq.mapOption + (fn i => if isNewline (Seq.nth s i) then SOME i else NONE) + (Seq.tabulate (fn i => i) n) *) + + val m = Seq.length idx + + fun line i = + let + val start = if i = 0 then 0 else Seq.nth idx (i-1) + val stop = if i = m then n else Seq.nth idx i + in + (start, stop) + end + + in + (* Seq.toArraySeq (Seq.filter containsPat (Seq.tabulate line (m+1))) *) + Seq.toArraySeq (Seq.mapOption containsPat (Seq.tabulate line (m+1))) + end + +end diff --git a/tests/mpllib/NearestNeighbors.sml b/tests/mpllib/NearestNeighbors.sml new file mode 100644 index 000000000..46889ed7f --- /dev/null +++ b/tests/mpllib/NearestNeighbors.sml @@ -0,0 +1,298 @@ +structure NearestNeighbors: +sig + type point = Geometry2D.point + type 'a seq = 'a ArraySlice.slice + + type tree + type t = tree * point seq + + (* makeTree leafSize points *) + val makeTree: int -> point seq -> t + + val nearestNeighbor: t -> point -> int (* id of nearest neighbor *) + val nearestNeighborOfId: t -> int -> int + + (* allNearestNeighbors grain quadtree *) + val allNearestNeighbors: int -> t -> int seq +end = +struct + + structure A = Array + structure AS = ArraySlice + + type 'a seq = 'a ArraySlice.slice + structure G = Geometry2D + type point = G.point + + fun par4 (a, b, c, d) = + let + val ((ar, br), (cr, dr)) = + ForkJoin.par (fn _ => ForkJoin.par (a, b), + fn _ => ForkJoin.par (c, d)) + in + (ar, br, cr, dr) + end + + datatype tree = + Leaf of { anchor : point + , width : real + , vertices : int seq (* indices of original point seq *) + } + | Node of { anchor : point + , width : real + , count : int + , children : tree seq + } + + type t = tree * point seq + + fun count t = + case t of + Leaf {vertices, ...} => AS.length vertices + | Node {count, ...} => count + + fun anchor t = + case t of + Leaf {anchor, ...} => anchor + | Node {anchor, ...} => anchor + + fun width t = + case t of + Leaf {width, ...} => width + | Node {width, ...} => width + + fun boxOf t = + case t of + Leaf {anchor=(x,y), width, ...} => (x, y, x+width, y+width) + | Node {anchor=(x,y), width, ...} => (x, y, x+width, y+width) + + fun indexApp grain f t = + let + fun downSweep offset t = + case t of + Leaf {vertices, ...} => + AS.appi (fn (i, v) => f (offset + i, v)) vertices + | Node {children, ...} => + let + fun q i = AS.sub (children, i) + fun qCount i = count (q i) + val offset0 = offset + val offset1 = offset0 + qCount 0 + val offset2 = offset1 + qCount 1 + val offset3 = offset2 + qCount 2 + in + if count t <= grain then + ( downSweep offset0 (q 0) + ; downSweep offset1 (q 1) + ; downSweep offset2 (q 2) + ; downSweep offset3 (q 3) + ) + else + ( par4 + ( fn _ => downSweep offset0 (q 0) + , fn _ => downSweep offset1 (q 1) + , fn _ => downSweep offset2 (q 2) + , fn _ => downSweep offset3 (q 3) + ) + ; () + ) + end + in + downSweep 0 t + end + + fun indexMap grain f t = + let + val result = ForkJoin.alloc (count t) + val _ = indexApp grain (fn (i, v) => A.update (result, i, f (i, v))) t + in + AS.full result + end + + fun flatten grain t = indexMap grain (fn (_, v) => v) t + + (* val lowerTime = ref Time.zeroTime + val upperTime = ref Time.zeroTime + fun addTm r t = + if Primitives.numberOfProcessors = 1 then + r := Time.+ (!r, t) + else () + fun clearAndReport r name = + (print (name ^ " " ^ Time.fmt 4 (!r) ^ "\n"); r := Time.zeroTime) *) + + (* Make a tree where all points are in the specified bounding box. *) + fun makeTreeBounded leafSize (verts : point seq) (idx : int seq) ((xLeft, yBot) : G.point) width = + if AS.length idx <= leafSize then + Leaf { anchor = (xLeft, yBot) + , width = width + , vertices = idx + } + else let + val qw = width/2.0 (* quadrant width *) + val center = (xLeft + qw, yBot + qw) + + val ((sorted, offsets), tm) = Util.getTime (fn () => + CountingSort.sort idx (fn i => + G.quadrant center (Seq.nth verts (Seq.nth idx i))) 4) + + (* val _ = + if AS.length idx >= 4 * leafSize then + addTm upperTime tm + else + addTm lowerTime tm *) + + fun quadrant i = + let + val start = AS.sub (offsets, i) + val len = AS.sub (offsets, i+1) - start + val childIdx = AS.subslice (sorted, start, SOME len) + val qAnchor = + case i of + 0 => (xLeft + qw, yBot + qw) + | 1 => (xLeft, yBot + qw) + | 2 => (xLeft, yBot) + | _ => (xLeft + qw, yBot) + in + makeTreeBounded leafSize verts childIdx qAnchor qw + end + + (* val children = Seq.tabulate (Perf.grain 1) quadrant 4 *) + val (a, b, c, d) = + if AS.length idx <= 100 then + (quadrant 0, quadrant 1, quadrant 2, quadrant 3) + else + par4 + ( fn _ => quadrant 0 + , fn _ => quadrant 1 + , fn _ => quadrant 2 + , fn _ => quadrant 3 ) + val children = AS.full (Array.fromList [a,b,c,d]) + in + Node { anchor = (xLeft, yBot) + , width = width + , count = AS.length idx + , children = children + } + end + + fun loop (lo, hi) b f = + if (lo >= hi) then b else loop (lo+1, hi) (f (b, lo)) f + + fun reduce grain f b (get, lo, hi) = + if hi - lo <= grain then + loop (lo, hi) b (fn (b, i) => f (b, get i)) + else let + val mid = lo + (hi-lo) div 2 + val (l,r) = ForkJoin.par + ( fn _ => reduce grain f b (get, lo, mid) + , fn _ => reduce grain f b (get, mid, hi) + ) + in + f (l, r) + end + + fun makeTree leafSize (verts : point seq) = + if AS.length verts = 0 then raise Fail "makeTree with 0 points" else + let + (* calculate the bounding box *) + fun maxPt ((x1,y1),(x2,y2)) = (Real.max (x1, x2), Real.max (y1, y2)) + fun minPt ((x1,y1),(x2,y2)) = (Real.min (x1, x2), Real.min (y1, y2)) + fun getPt i = Seq.nth verts i + val (xLeft,yBot) = reduce 10000 minPt (Real.posInf, Real.posInf) (getPt, 0, AS.length verts) + val (xRight,yTop) = reduce 10000 maxPt (Real.negInf, Real.negInf) (getPt, 0, AS.length verts) + val width = Real.max (xRight-xLeft, yTop-yBot) + + val idx = Seq.tabulate (fn i => i) (Seq.length verts) + val result = makeTreeBounded leafSize verts idx (xLeft, yBot) width + in + (* clearAndReport upperTime "upper sort time"; *) + (* clearAndReport lowerTime "lower sort time"; *) + (result, verts) + end + + (* ======================================================================== *) + + fun constrain (x : real) (lo, hi) = + if x < lo then lo + else if x > hi then hi + else x + + fun distanceToBox (x,y) (xLeft, yBot, xRight, yTop) = + G.distance (x,y) (constrain x (xLeft, xRight), constrain y (yBot, yTop)) + + val dummyBest = (~1, Real.posInf) + + + (** The function isSamePoint given as argument indicates whether or not some + * other index is the same as the input point p. This is important for + * querying nearest neighbors of points already in the set, for example + * nearestNeighborOfId below. For query points outside of the set, + * isSamePoint can always return false. + *) + fun nearestNeighbor_ (t : tree, pts) (p: G.point, isSamePoint: int -> bool) = + let + fun pt i = Seq.nth pts i + + fun refineNearest (qi, (bestPt, bestDist)) = + if isSamePoint qi then (bestPt, bestDist) else + let + val qDist = G.distance p (pt qi) + in + if qDist < bestDist + then (qi, qDist) + else (bestPt, bestDist) + end + + fun search (best as (_, bestDist : real)) t = + if distanceToBox p (boxOf t) > bestDist then best else + case t of + Leaf {vertices, ...} => + AS.foldl refineNearest best vertices + | Node {anchor=(x,y), width, children, ...} => + let + val qw = width/2.0 + val center = (x+qw, y+qw) + + (* search the quadrant that p is in first *) + val heuristicOrder = + case G.quadrant center p of + 0 => [0,1,2,3] + | 1 => [1,0,2,3] + | 2 => [2,1,3,0] + | _ => [3,0,2,1] + + fun child i = AS.sub (children, i) + fun refine (i, best) = search best (child i) + in + List.foldl refine best heuristicOrder + end + + val (best, _) = search dummyBest t + in + best + end + + + fun nearestNeighborOfId (tree, pts) pi = + nearestNeighbor_ (tree, pts) (Seq.nth pts pi, fn qi => pi = qi) + + fun nearestNeighbor (tree, pts) p = + nearestNeighbor_ (tree, pts) (p, fn _ => false) + + + fun allNearestNeighbors grain (t, pts) = + let + val n = Seq.length pts + val idxs = flatten 10000 t + val nn = ForkJoin.alloc n + in + ForkJoin.parfor grain (0, n) (fn i => + let + val j = Seq.nth idxs i + in + A.update (nn, j, nearestNeighborOfId (t, pts) j) + end); + AS.full nn + end + +end diff --git a/tests/mpllib/NewWaveIO.sml b/tests/mpllib/NewWaveIO.sml new file mode 100644 index 000000000..d066c246d --- /dev/null +++ b/tests/mpllib/NewWaveIO.sml @@ -0,0 +1,262 @@ +structure NewWaveIO: +sig + (* A sound is a sequence of samples at the given + * sample rate, sr (measured in Hz). + * Each sample is in range [-1.0, +1.0]. *) + type sound = {sr: int, data: real Seq.t} + + val readSound: string -> sound + val writeSound: sound -> string -> unit + + (* Essentially mu-law compression. Normalizes to [-1,+1] and compresses + * the dynamic range slightly. The boost parameter should be >= 1. *) + val compress: real -> sound -> sound +end = +struct + + type sound = {sr: int, data: real Seq.t} + + structure AS = ArraySlice + + fun err msg = + raise Fail ("NewWaveIO: " ^ msg) + + fun compress boost (snd as {sr, data}: sound) = + if boost < 1.0 then + err "Compression boost parameter must be at least 1" + else + let + (* maximum amplitude *) + val maxA = + SeqBasis.reduce 10000 Real.max 1.0 (0, Seq.length data) + (fn i => Real.abs (Seq.nth data i)) + + (* a little buffer of intensity to avoid distortion *) + val maxA' = 1.05 * maxA + + val scale = Math.ln (1.0 + boost) + + fun transfer x = + let + (* normalized *) + val x' = Real.abs (x / maxA') + in + (* compressed *) + Real.copySign (Math.ln (1.0 + boost * x') / scale, x) + end + in + { sr = sr + , data = Seq.map transfer data + } + end + + fun readSound path = + let + val bytes = ReadFile.contentsBinSeq path + + fun findChunk chunkId offset = + if offset > (Seq.length bytes - 8) then + err "unexpected end of file" + else if Parse.r32b bytes offset = chunkId then + offset (* found it! *) + else + let + val chunkSize = Word32.toInt (Parse.r32l bytes (offset+4)) + val chunkName = + CharVector.tabulate (4, fn i => + Char.chr (Word8.toInt (Seq.nth bytes (offset+i)))) + in + if chunkSize < 0 then + err ("error parsing chunk size of '" ^ chunkName ^ "' chunk") + else + findChunk chunkId (offset + 8 + chunkSize) + end + + (* ======================================================= + * RIFF header, 12 bytes + *) + + val _ = + if Seq.length bytes >= 12 then () + else err "not enough bytes for RIFF header" + + val offset = 0 + val riff = 0wx52494646 (* ascii "RIFF", big endian *) + val _ = + if Parse.r32b bytes offset = riff then () + else err "expected 'RIFF' chunk ID" + + (* the chunkSize should be the size of the "rest of the file" *) + val offset = 4 + val chunkSize = Word32.toInt (Parse.r32l bytes offset) + + val totalFileSize = 8 + chunkSize + val _ = + if Seq.length bytes >= totalFileSize then () + else err ("expected " ^ Int.toString totalFileSize ^ + " bytes but the file is only " ^ + Int.toString (Seq.length bytes)) + + val offset = 8 + val wave = 0wx57415645 (* ascii "WAVE" big endian *) + val _ = + if Parse.r32b bytes offset = wave then () + else err "expected 'WAVE' format" + + val offset = 12 + + (* ======================================================= + * fmt subchunk, should be at least 8+16 bytes total for PCM + *) + + val fmtId = 0wx666d7420 (* ascii "fmt " big endian *) + val fmtChunkStart = findChunk fmtId offset + val offset = fmtChunkStart + + val _ = + if Parse.r32b bytes offset = fmtId then () + else err "expected 'fmt ' chunk ID" + + val offset = offset+4 + val fmtChunkSize = Word32.toInt (Parse.r32l bytes offset) + val _ = + if fmtChunkSize >= 16 then () + else err "expected 'fmt ' chunk to be at least 16 bytes" + + val offset = offset+4 + val audioFormat = Word16.toInt (Parse.r16l bytes offset) + val _ = + if audioFormat = 1 then () + else err ("expected PCM audio format, but found 0x" + ^ Int.fmt StringCvt.HEX audioFormat) + + val offset = offset+2 + val numChannels = Word16.toInt (Parse.r16l bytes offset) + + val offset = offset+2 + val sampleRate = Word32.toInt (Parse.r32l bytes offset) + + val offset = offset+4 + val byteRate = Word32.toInt (Parse.r32l bytes offset) + + val offset = offset+4 + val blockAlign = Word16.toInt (Parse.r16l bytes offset) + + val offset = offset+2 + val bitsPerSample = Word16.toInt (Parse.r16l bytes offset) + val bytesPerSample = bitsPerSample div 8 + + val offset = fmtChunkStart+8+fmtChunkSize + + (* ======================================================= + * data subchunk, should be the rest of the file + *) + + val dataId = 0wx64617461 (* ascii "data" big endian *) + val dataChunkStart = findChunk dataId offset + val offset = dataChunkStart + + val _ = + if Parse.r32b bytes offset = dataId then () + else err "expected 'data' chunk ID" + + val offset = offset + 4 + val dataSize = Word32.toInt (Parse.r32l bytes offset) + val _ = + if dataChunkStart + 8 + dataSize <= totalFileSize then () + else err ("badly formatted data chunk: unexpected end-of-file") + + val dataStart = dataChunkStart + 8 + + val numSamples = (dataSize div numChannels) div bytesPerSample + + fun readSample8 pos = + Real.fromInt (Word8.toInt (Seq.nth bytes pos) - 128) / 256.0 + fun readSample16 pos = + Real.fromInt (Word16.toIntX (Parse.r16l bytes pos)) / 32768.0 + + val readSample = + case bytesPerSample of + 1 => readSample8 + | 2 => readSample16 + | _ => err "only 8-bit and 16-bit samples supported at the moment" + + (* jth sample of ith channel *) + fun readChannel i j = + readSample (dataStart + j * (numChannels * bytesPerSample) + + i * bytesPerSample) + + val rawData = + AS.full (SeqBasis.tabulate 1000 (0, numSamples) (fn j => + Util.loop (0, numChannels) 0.0 (fn (s, i) => s + readChannel i j))) + + val rawResult = {sr = sampleRate, data = rawData} + in + if numChannels = 1 then + rawResult + else + ( TextIO.output (TextIO.stdErr, + "[WARN] mixing " ^ Int.toString numChannels + ^ " channels down to mono\n") + ; compress 1.0 rawResult + ) + end + + (* ====================================================================== *) + + fun writeSound ({sr, data}: sound) path = + let + val srw = Word32.fromInt sr + + val file = BinIO.openOut path + + val w32b = ExtraBinIO.w32b file + val w32l = ExtraBinIO.w32l file + val w16l = ExtraBinIO.w16l file + + val totalBytes = + 44 + (Seq.length data * 2) + + val riffId = 0wx52494646 (* ascii "RIFF", big endian *) + val fmtId = 0wx666d7420 (* ascii "fmt " big endian *) + val wave = 0wx57415645 (* ascii "WAVE" big endian *) + val dataId = 0wx64617461 (* ascii "data" big endian *) + in + (* ============================ + * RIFF header, 12 bytes *) + w32b riffId; + w32l (Word32.fromInt (totalBytes - 8)); + w32b wave; + + (* ============================ + * fmt subchunk, 24 bytes *) + w32b fmtId; + w32l 0w16; (* 16 remaining bytes in subchunk *) + w16l 0w1; (* audio format PCM = 1 *) + w16l 0w1; (* 1 channel (mono) *) + w32l srw; (* sample rate *) + w32l (srw * 0w2); (* "byte rate" = sampleRate * numChannels * bytesPerSample *) + w16l 0w2; (* "block align" = numChannels * bytesPerSample *) + w16l 0w16; (* bits per sample *) + + (* ============================ + * data subchunk: rest of file *) + w32b dataId; + w32l (Word32.fromInt (2 * Seq.length data)); (* number of data bytes *) + + Util.for (0, Seq.length data) (fn i => + let + val s = Seq.nth data i + val s = + if s < ~1.0 then ~1.0 + else if s > 1.0 then 1.0 + else s + val s = Real.round (s * 32767.0) + val s = if s < 0 then s + 65536 else s + in + w16l (Word16.fromInt s) + end); + + BinIO.closeOut file + end +end diff --git a/tests/mpllib/OffsetSearch.sml b/tests/mpllib/OffsetSearch.sml new file mode 100644 index 000000000..32b56c9a2 --- /dev/null +++ b/tests/mpllib/OffsetSearch.sml @@ -0,0 +1,26 @@ +structure OffsetSearch: +sig + (** `indexSearch (start, stop, offsetFn) k` returns which inner sequence + * contains index `k`. The tuple arg defines a sequence of offsets. + *) + val indexSearch: int * int * (int -> int) -> int -> int +end = +struct + + fun indexSearch (start, stop, offset: int -> int) k = + case stop-start of + 0 => + raise Fail "OffsetSearch.indexSearch: should not have hit 0" + | 1 => + start + | n => + let + val mid = start + (n div 2) + in + if k < offset mid then + indexSearch (start, mid, offset) k + else + indexSearch (mid, stop, offset) k + end + +end diff --git a/tests/mpllib/OldDelayedSeq.sml b/tests/mpllib/OldDelayedSeq.sml new file mode 100644 index 000000000..e78ec6304 --- /dev/null +++ b/tests/mpllib/OldDelayedSeq.sml @@ -0,0 +1,922 @@ +structure OldDelayedSeq = +struct + + val for = Util.for + + val par = ForkJoin.par + val parfor = ForkJoin.parfor + val alloc = ForkJoin.alloc + + val gran = 5000 + + + val blockSize = 10000 + fun numBlocks n = Util.ceilDiv n blockSize + + + structure A = + struct + open Array + type 'a t = 'a array + fun nth a i = sub (a, i) + end + + + structure AS = + struct + open ArraySlice + type 'a t = 'a slice + fun nth a i = sub (a, i) + end + + + (* Using given offsets, find which inner sequence contains index [k] *) + fun indexSearch (start, stop, offset: int -> int) k = + case stop-start of + 0 => + raise Fail "OldDelayedSeq.indexSearch: should not have hit 0" + | 1 => + start + | n => + let + val mid = start + (n div 2) + in + if k < offset mid then + indexSearch (start, mid, offset) k + else + indexSearch (mid, stop, offset) k + end + + + (* ======================================================================= *) + + + structure Stream:> + sig + type 'a t + type 'a stream = 'a t + + val tabulate: (int -> 'a) -> 'a stream + val map: ('a -> 'b) -> 'a stream -> 'b stream + val mapIdx: (int * 'a -> 'b) -> 'a stream -> 'b stream + val zipWith: ('a * 'b -> 'c) -> 'a stream * 'b stream -> 'c stream + val iteratePrefixes: ('b * 'a -> 'b) -> 'b -> 'a stream -> 'b stream + val iteratePrefixesIncl: ('b * 'a -> 'b) -> 'b -> 'a stream -> 'b stream + + val applyIdx: int * 'a stream -> (int * 'a -> unit) -> unit + val iterate: ('b * 'a -> 'b) -> 'b -> int * 'a stream -> 'b + + val makeBlockStreams: + { numChildren: int + , offset: int -> int + , getElem: int -> int -> 'a + } + -> (int -> 'a stream) + + end = + struct + + (** A stream is a generator for a stateful trickle function: + * trickle = stream () + * x0 = trickle 0 + * x1 = trickle 1 + * x2 = trickle 2 + * ... + * + * The integer argument is just an optimization (it could be packaged + * up into the state of the trickle function, but doing it this + * way is more efficient). Requires passing `i` on the ith call + * to trickle. + *) + type 'a t = unit -> int -> 'a + type 'a stream = 'a t + + + fun tabulate f = + fn () => f + + + fun map g stream = + fn () => + let + val trickle = stream () + in + g o trickle + end + + + fun mapIdx g stream = + fn () => + let + val trickle = stream () + in + fn idx => g (idx, trickle idx) + end + + + fun applyIdx (length, stream) g = + let + val trickle = stream () + fun loop i = + if i >= length then () else (g (i, trickle i); loop (i+1)) + in + loop 0 + end + + + fun iterate g b (length, stream) = + let + val trickle = stream () + fun loop b i = + if i >= length then b else loop (g (b, trickle i)) (i+1) + in + loop b 0 + end + + + fun iteratePrefixes g b stream = + fn () => + let + val trickle = stream () + val stuff = ref b + in + fn idx => + let + val acc = !stuff + val elem = trickle idx + val acc' = g (acc, elem) + in + stuff := acc'; + acc + end + end + + + fun iteratePrefixesIncl g b stream = + fn () => + let + val trickle = stream () + val stuff = ref b + in + fn idx => + let + val acc = !stuff + val elem = trickle idx + val acc' = g (acc, elem) + in + stuff := acc'; + acc' + end + end + + + fun zipWith g (s1, s2) = + fn () => + let + val trickle1 = s1 () + val trickle2 = s2 () + in + fn idx => g (trickle1 idx, trickle2 idx) + end + + + fun makeBlockStreams + { numChildren: int + , offset: int -> int + , getElem: int -> int -> 'a + } = + let + fun getBlock blockIdx = + let + val lo = blockIdx * blockSize + val firstOuterIdx = indexSearch (0, numChildren, offset) lo + (* val firstInnerIdx = lo - offset firstOuterIdx *) + + fun advanceUntilNonEmpty i = + if i >= numChildren orelse offset i <> offset (i+1) then + i + else + advanceUntilNonEmpty (i+1) + in + fn () => + let + val outerIdx = ref firstOuterIdx + (* val innerIdx = ref firstInnerIdx *) + in + fn idx => + let + val i = !outerIdx + val j = lo + idx - offset i + (* val j = !innerIdx *) + val elem = getElem i j + in + if offset i + j + 1 < offset (i+1) then + (* innerIdx := j+1 *) () + else + ( outerIdx := advanceUntilNonEmpty (i+1) + (* ; innerIdx := 0 *) + ); + + elem + end + end + end + + in + getBlock + end + + + end + + + (* ======================================================================= *) + + + datatype 'a flat = + Full of 'a AS.t + + (** [Delay (start, stop, lookup)] + * start index is inclusive, stop is exclusive + * length is [stop-start] + * the ith element lives at [lookup (start+i)] + *) + | Delay of int * int * (int -> 'a) + + + datatype 'a seq = + Flat of 'a flat + + (** [Nest (length, getBlock)] + * The block sizes are implicit: [gran] + * The number of block is implicit: ceil(length / gran) + *) + | Nest of int * (int -> 'a Stream.t) + + + fun makeBlocks s = + case s of + Flat (Full slice) => + let + fun blockStream b = + Stream.tabulate (fn i => AS.nth slice (b*blockSize + i)) + in + blockStream + end + | Flat (Delay (start, _, f)) => + let + fun blockStream b = + Stream.tabulate (fn i => f (start + b*blockSize + i)) + in + blockStream + end + | Nest (_, blockStream) => + blockStream + + + fun subseq s (i, k) = + case s of + Flat (Delay (start, stop, f)) => Flat (Delay (start+i, start+i+k, f)) + | Flat (Full slice) => Flat (Full (AS.subslice (slice, i, SOME k))) + | _ => raise Fail "delay subseq (Nest) not implemented yet" + + + fun flatNth (s: 'a flat) k = + case s of + Full slice => + AS.nth slice k + | Delay (i, j, f) => + f (i+k) + + + fun nth (s: 'a seq) k = + case s of + Flat xs => + flatNth xs k + | Nest _ => + raise Fail "delay nth (Nest) not implemented yet" + + + fun flatLength s = + case s of + Full slice => + AS.length slice + | Delay (i, j, f) => + j-i + + + fun length s = + case s of + Flat xs => + flatLength xs + | Nest (n, _) => + n + + + fun flatSubseqIdxs s (i, j) = + case s of + Full slice => + Full (AS.subslice (slice, i, SOME (j-i))) + | Delay (start, stop, f) => + Delay (start+i, start+j, f) + + + fun flatIterateIdx (g: 'b * (int * 'a) -> 'b) (b: 'b) s = + case s of + Full slice => + SeqBasis.foldl g b (0, AS.length slice) (fn i => (i, AS.nth slice i)) + | Delay (i, j, f) => + SeqBasis.foldl g b (i, j) (fn k => (k-i, f k)) + + + fun iterateIdx g b s = + case s of + Flat xs => + flatIterateIdx g b xs + | Nest _ => + raise Fail "delay iterateIdx (Nest) NYI" + + + fun flatIterate g b s = + flatIterateIdx (fn (b, (_, x)) => g (b, x)) b s + + + fun iterate g b s = + iterateIdx (fn (b, (_, x)) => g (b, x)) b s + + + fun applyIdx (s: 'a seq) (g: int * 'a -> unit) = + case s of + Flat (Full slice) => + parfor gran (0, AS.length slice) (fn i => g (i, AS.nth slice i)) + | Flat (Delay (i, j, f)) => + parfor gran (0, j-i) (fn k => g (k, f (i+k))) + | Nest (n, getBlock) => + parfor 1 (0, numBlocks n) (fn i => + let + val lo = i*blockSize + val hi = Int.min (lo+blockSize, n) + in + Stream.applyIdx (hi-lo, getBlock i) (fn (j, x) => g (lo+j, x)) + end) + + + fun apply (s: 'a seq) (g: 'a -> unit) = + applyIdx s (fn (_, x) => g x) + + + fun unravelAndCopy (s: 'a seq): 'a array = + let + val n = length s + val result = alloc n + in + applyIdx s (fn (i, x) => A.update (result, i, x)); + result + end + + + fun force s = + case s of + Flat (Full _) => s + | _ => Flat (Full (AS.full (unravelAndCopy s))) + + + fun forceFlat s = + case s of + Flat xx => xx + | _ => Full (AS.full (unravelAndCopy s)) + + + fun tabulate f n = + Flat (Delay (0, n, f)) + + + fun fromList xs = + Flat (Full (AS.full (Array.fromList xs))) + + + fun % xs = + fromList xs + + + fun singleton x = + Flat (Delay (0, 1, fn _ => x)) + + + fun $ x = + singleton x + + + fun empty () = + fromList [] + + + fun fromArraySeq a = + Flat (Full a) + + + fun range (i, j) = + Flat (Delay (i, j, fn k => k)) + + + fun toArraySeq s = + case s of + Flat (Full x) => x + | _ => AS.full (unravelAndCopy s) + + + fun flatMap g s = + case s of + Full slice => + Delay (0, AS.length slice, g o AS.nth slice) + | Delay (i, j, f) => + Delay (i, j, g o f) + + + fun map g s = + case s of + Flat xs => + Flat (flatMap g xs) + | Nest (n, getBlock) => + Nest (n, Stream.map g o getBlock) + + + fun flatMapIdx g s = + case s of + Full slice => + Delay (0, AS.length slice, fn i => g (i, AS.nth slice i)) + | Delay (i, j, f) => + Delay (i, j, fn k => g (k-i, f i)) + + + fun mapIdx g s = + case s of + Flat xs => + Flat (flatMapIdx g xs) + | Nest (n, getBlock) => + Nest (n, fn i => + Stream.mapIdx (fn (j, x) => g (i*blockSize + j, x)) (getBlock i)) + + + fun enum s = + mapIdx (fn (i,x) => (i,x)) s + + + fun flatten (ss: 'a seq seq) = + let + val numChildren = length ss + val children: 'a flat array = unravelAndCopy (map forceFlat ss) + val offsets = + SeqBasis.scan gran op+ 0 (0, numChildren) (flatLength o A.nth children) + val totalLen = A.nth offsets numChildren + fun offset i = A.nth offsets i + val getBlock = + Stream.makeBlockStreams + { numChildren = numChildren + , offset = offset + , getElem = (fn i => fn j => flatNth (A.nth children i) j) + } + in + Nest (totalLen, getBlock) + end + + + fun flatRev s = + case s of + Full slice => + let + val n = AS.length slice + in + Delay (0, n, fn i => AS.nth slice (n-i-1)) + end + | Delay (i, j, f) => + Delay (i, j, fn k => f (j-k-1)) + + + fun rev s = + case s of + Flat xs => + Flat (flatRev xs) + | Nest _ => + raise Fail "delay rev (Nest) NYI" + + + fun reduceG newGran g b s = + case s of + Flat (Full slice) => + SeqBasis.reduce newGran g b (0, AS.length slice) (AS.nth slice) + | Flat (Delay (i, j, f)) => + SeqBasis.reduce newGran g b (i, j) f + | Nest (n, getBlock) => + let + val nb = numBlocks n + fun len i = + if i < nb-1 then blockSize else n - i*blockSize + in + SeqBasis.reduce 1 g b (0, nb) (fn i => + Stream.iterate g b (len i, getBlock i)) + end + + + fun reduce g b s = + reduceG gran g b s + + + (** ======================================================================= + * mapOption implementations + * + * first one delays output (like flattening) + * second one has eager output + *) + + fun mapOption1 (f: 'a -> 'b option) (s: 'a seq) : 'b seq = + let + val n = length s + val nb = numBlocks n + val getBlock: int -> 'a Stream.t = + makeBlocks s + + val results: 'b array = alloc n + + fun packBlock b = + let + val start = b*blockSize + val stop = Int.min (start+blockSize, n) + val size = stop-start + + fun doNext (off, x) = + case f x of + NONE => off + | SOME x' => (A.update (results, off, x'); off+1) + + val lastOffset = + Stream.iterate doNext start (size, getBlock b) + in + lastOffset - start + end + + val counts = SeqBasis.tabulate 1 (0, nb) packBlock + val offsets = SeqBasis.scan 10000 op+ 0 (0, A.length counts) (A.nth counts) + + val totalLen = A.nth offsets nb + fun offset i = A.nth offsets i + val getBlock = + Stream.makeBlockStreams + { numChildren = nb + , offset = offset + , getElem = (fn i => fn j => A.nth results (i*blockSize + j)) + } + in + Nest (totalLen, getBlock) + end + + + fun mapOption2 (f: 'a -> 'b option) (s: 'a seq) = + let + val n = length s + val nb = numBlocks n + val getBlock: int -> 'a Stream.t = + makeBlocks s + + val results: 'b array = alloc n + + fun packBlock b = + let + val start = b*blockSize + val stop = Int.min (start+blockSize, n) + val size = stop-start + + fun doNext (off, x) = + case f x of + NONE => off + | SOME x' => (A.update (results, off, x'); off+1) + + val lastOffset = + Stream.iterate doNext start (size, getBlock b) + in + lastOffset - start + end + + val counts = SeqBasis.tabulate 1 (0, nb) packBlock + val outOff = SeqBasis.scan 10000 op+ 0 (0, A.length counts) (A.nth counts) + val outSize = A.sub (outOff, nb) + + val result = alloc outSize + in + parfor (n div (Int.max (outSize, 1))) (0, nb) (fn i => + let + val soff = i * blockSize + val doff = A.sub (outOff, i) + val size = A.sub (outOff, i+1) - doff + in + Util.for (0, size) (fn j => + A.update (result, doff+j, A.sub (results, soff+j))) + end); + + Flat (Full (ArraySlice.full result)) + end + + + fun mapOption f s = + (* mapOption1 f s *) + mapOption2 f s + + + fun filterIdx p s = + case s of + Flat (Full slice) => + Flat (Full (AS.full (SeqBasis.filter gran + (0, AS.length slice) (* range *) + (AS.nth slice) (* index lookup *) + (fn k => p (k, AS.nth slice k)) (* index predicate *) + ))) + + | Flat (Delay (i, j, f)) => + Flat (Full (AS.full (SeqBasis.filter gran + (i, j) + f + (fn k => p (k, f k)) + ))) + + | _ => + filterIdx p (force s) + + + fun filter p s = + filterIdx (fn (_, x) => p x) s + + + fun inject (s, u) = + let + val base = unravelAndCopy s + in + apply u (fn (i, x) => A.update (base, i, x)); + Flat (Full (AS.full base)) + end + + + fun injectG _ (s, u) = + let + val base = unravelAndCopy s + in + apply u (fn (i, x) => A.update (base, i, x)); + Flat (Full (AS.full base)) + end + + + fun toList s = + iterate (fn (xs, x) => x :: xs) [] (rev s) + + + fun toString f s = + "<" ^ + String.concatWith "," + (iterate (fn (strs, next) => next :: strs) [] (map f (rev s))) + ^ ">" + + + fun append (s, t) = + flatten (tabulate (fn 0 => s | _ => t) 2) + + + (* Do the scan on a flat delayed sequence [f(i), f(i+1), ..., f(j-1)] *) + fun scanDelay g b (i, j, f) = + let + val n = j-i + val nb = numBlocks n + + val blockSums = + SeqBasis.tabulate 1 (0, nb) (fn blockIdx => + let + val blockStart = i + blockIdx*blockSize + val blockEnd = Int.min (j, blockStart + blockSize) + in + SeqBasis.foldl g b (blockStart, blockEnd) f + end) + + val partials = + SeqBasis.scan gran g b (0, nb) (A.nth blockSums) + + val total = A.nth partials nb + + fun getChild blockIdx = + let + val firstElem = A.nth partials blockIdx + val blockStart = i + blockIdx*blockSize + in + Stream.iteratePrefixes g firstElem + (Stream.tabulate (fn k => f (blockStart+k))) + end + in + ( Nest (n, getChild) + , total + ) + end + + + fun scanInclDelay g b (i, j, f) = + let + val n = j-i + val nb = numBlocks n + + val blockSums = + SeqBasis.tabulate 1 (0, nb) (fn blockIdx => + let + val blockStart = i + blockIdx*blockSize + val blockEnd = Int.min (j, blockStart + blockSize) + in + SeqBasis.foldl g b (blockStart, blockEnd) f + end) + + val partials = + SeqBasis.scan gran g b (0, nb) (A.nth blockSums) + + fun getChild blockIdx = + let + val firstElem = A.nth partials blockIdx + val blockStart = i + blockIdx*blockSize + in + Stream.iteratePrefixesIncl g firstElem + (Stream.tabulate (fn k => f (blockStart+k))) + end + in + Nest (n, getChild) + end + + + fun scanScan g b (n, getChild: int -> 'a Stream.t) = + let + val numChildren = Util.ceilDiv n blockSize + fun len i = + if i < numChildren-1 then blockSize else n - i*blockSize + + val childSums = + SeqBasis.tabulate 1 (0, numChildren) (fn childIdx => + Stream.iterate g b (len childIdx, getChild childIdx) + ) + + val partials = + SeqBasis.scan gran g b (0, numChildren) (A.nth childSums) + val total = A.nth partials numChildren + + fun getChild' childIdx = + let + val childStream = getChild childIdx + val initial = A.nth partials childIdx + in + Stream.iteratePrefixes g initial childStream + end + in + ( Nest (n, getChild') + , total + ) + end + + + fun scanScanIncl g b (n, getChild) = + let + val numChildren = Util.ceilDiv n blockSize + fun len i = + if i < numChildren-1 then blockSize else n - i*blockSize + + val childSums = + SeqBasis.tabulate 1 (0, numChildren) (fn childIdx => + Stream.iterate g b (len childIdx, getChild childIdx) + ) + + val partials = + SeqBasis.scan gran g b (0, numChildren) (A.nth childSums) + + fun getChild' childIdx = + let + val childStream = getChild childIdx + val initial = A.nth partials childIdx + in + Stream.iteratePrefixesIncl g initial childStream + end + in + Nest (n, getChild') + end + + + fun scan g b s = + case s of + Flat (Full slice) => + scanDelay g b (0, AS.length slice, AS.nth slice) + | Flat (Delay (i, j, f)) => + scanDelay g b (i, j, f) + | Nest (n, getChild) => + scanScan g b (n, getChild) + + + fun scanIncl g b s = + case s of + Flat (Full slice) => + scanInclDelay g b (0, AS.length slice, AS.nth slice) + | Flat (Delay (i, j, f)) => + scanInclDelay g b (i, j, f) + | Nest (n, getChild) => + scanScanIncl g b (n, getChild) + + + fun zipWithBothFlat g (i1, j1, f1) (i2, j2, f2) = + let + val n1 = j1-i1 + val n2 = j2-i2 + val n = Int.min (n1, n2) + in + Flat (Delay (0, n, fn i => g (f1 (i1+i), f2 (i2+i)))) + end + + + fun zipWithOneNest g (n1, getChild1) (i, j, f) = + let + val n2 = j-i + val _ = + if n1 = n2 then () else + raise Fail "OldDelayedSeq.zipWith lengths don't match" + + fun getChild childIdx = + let + val child1 = getChild1 childIdx + val lo = i + childIdx * blockSize + in + Stream.mapIdx (fn (k, x) => g (x, f (lo+k))) child1 + end + in + Nest (n1, getChild) + end + + + fun zipWithBothNest g (n1, getChild1) (n2, getChild2) = + let + val _ = + if n1 = n2 then () else + raise Fail "OldDelayedSeq.zipWith lengths don't match" + + fun getChild childIdx = + Stream.zipWith g (getChild1 childIdx, getChild2 childIdx) + in + Nest (n1, getChild) + end + + fun flip g (a, b) = g (b, a) + + fun zipWith g (s1, s2) = + case (s1, s2) of + (Flat (Delay xx), Flat (Delay yy)) => + zipWithBothFlat g xx yy + | (Flat (Full slice), Flat (Delay yy)) => + zipWithBothFlat g (0, AS.length slice, AS.nth slice) yy + | (Flat (Delay xx), Flat (Full slice)) => + zipWithBothFlat g xx (0, AS.length slice, AS.nth slice) + | (Flat (Full slice1), Flat (Full slice2)) => + zipWithBothFlat g + (0, AS.length slice1, AS.nth slice1) + (0, AS.length slice2, AS.nth slice2) + | (Nest xx, Flat (Full slice)) => + zipWithOneNest g xx (0, AS.length slice, AS.nth slice) + | (Nest xx, Flat (Delay yy)) => + zipWithOneNest g xx yy + | (Flat (Full slice), Nest xx) => + zipWithOneNest (flip g) xx (0, AS.length slice, AS.nth slice) + | (Flat (Delay yy), Nest xx) => + zipWithOneNest (flip g) xx yy + | (Nest xx, Nest yy) => + zipWithBothNest g xx yy + + + fun zip (a, b) = zipWith (fn x => x) (a, b) + + (* ===================================================================== *) + + exception NYI + exception Range + exception Size + + datatype 'a listview = NIL | CONS of 'a * 'a seq + datatype 'a treeview = EMPTY | ONE of 'a | PAIR of 'a seq * 'a seq + + type 'a ord = 'a * 'a -> order + type 'a t = 'a seq + + fun argmax x = raise NYI + fun collate x = raise NYI + fun collect x = raise NYI + fun drop x = raise NYI + fun equal x = raise NYI + fun iteratePrefixes x = raise NYI + fun iteratePrefixesIncl x = raise NYI + fun merge x = raise NYI + fun sort x = raise NYI + fun splitHead x = raise NYI + fun splitMid x = raise NYI + fun take x = raise NYI + fun update x = raise NYI + fun zipWith3 x = raise NYI + + fun filterSome x = raise NYI + fun foreach x = raise NYI + fun foreachG x = raise NYI + +end diff --git a/tests/mpllib/PPM.sml b/tests/mpllib/PPM.sml new file mode 100644 index 000000000..ba2481858 --- /dev/null +++ b/tests/mpllib/PPM.sml @@ -0,0 +1,280 @@ +(* Basic support for the netpbm .ppm file format. *) +structure PPM: +sig + type channel = Color.channel + type pixel = Color.pixel + + (* flat sequence; pixel (i, j) is at data[i*width + j] *) + type image = {height: int, width: int, data: pixel Seq.t} + type box = {topleft: int * int, botright: int * int} + + val elem: image -> (int * int) -> pixel + + val subimage: box -> image -> image + + (* `replace box image subimage` copies subimage into the image at the + * specified box *) + val replace: box -> image -> image -> image + + (* read the given .ppm file *) + val read: string -> image + + (* output this image to the given .ppm file *) + val write: string -> image -> unit + +end = +struct + + type 'a seq = 'a Seq.t + + type channel = Color.channel + type pixel = Color.pixel + type image = {height: int, width: int, data: pixel Seq.t} + type box = {topleft: int * int, botright: int * int} + + fun elem ({height, width, data}: image) (i, j) = + if i < 0 orelse i >= height orelse j < 0 orelse j >= width then + raise Subscript + else + Seq.nth data (i*width + j) + + fun subimage {topleft=(i1,j1), botright=(i2,j2)} image = + let + val w = j2-j1 + val h = i2-i1 + + fun newElem k = + let + val i = k div w + val j = k mod w + in + elem image (i1 + i, j1 + j) + end + in + { width = w + , height = h + , data = Seq.tabulate newElem (w * h) + } + end + + fun replace {topleft=(i1,j1), botright=(i2,j2)} image subimage = + let + fun newElem k = + let + val i = k div (#width image) + val j = k mod (#width image) + in + if i1 <= i andalso i < i2 andalso + j1 <= j andalso j < j2 + then elem subimage (i-i1, j-j1) + else elem image (i, j) + end + in + { width = #width image + , height = #height image + , data = Seq.tabulate newElem (#width image * #height image) + } + end + + (* utilities... *) + + fun niceify str = + if String.size str <= 10 then str + else String.substring (str, 0, 7) ^ "..." + + (* ============================== P3 format ============================== *) + + fun parse3 contents = + let + (* val tokens = Seq.fromList (String.tokens Char.isSpace contents) *) + (* val numToks = Seq.length tokens *) + val (numToks, tokRange) = Tokenize.tokenRanges Char.isSpace contents + fun tok i = + let + val (lo, hi) = tokRange i + in + Seq.subseq contents (lo, hi-lo) + end + fun strTok i = + Parse.parseString (tok i) + + val filetype = strTok 0 + val _ = + if filetype = "P3" then () + else raise Fail "should not happen" + + fun intTok thingName i = + let + fun err () = + raise Fail ("error parsing .ppm file: cannot parse " + ^ thingName ^ " from '" + ^ niceify (strTok i) ^ "'") + in + case Parse.parseInt (tok i) of + NONE => err () + | SOME x => if x >= 0 then x else err () + end + + val width = intTok "width" 1 + val height = intTok "height" 2 + val resolution = intTok "max color value" 3 + + val numPixels = width * height + val numChannels = 3 * width * height + + val _ = + if numToks = numChannels + 4 then () + else raise Fail ("error parsing .ppm file: too few color channels") + + fun normalize (c : int) = + Real.ceil ((Real.fromInt c / Real.fromInt resolution) * 255.0) + + fun chan i = + let + val c = intTok "channel" (i + 4) + val _ = + if c <= resolution then () + else raise Fail ("error parsing .ppm file: channel value " + ^ Int.toString c ^ " greater than resolution " + ^ Int.toString resolution) + in + Word8.fromInt (normalize c) + end + + fun pixel i = + {red = chan (3*i), green = chan (3*i + 1), blue = chan (3*i + 2)} + in + { width = width + , height = height + , data = Seq.tabulate pixel (width * height) + } + end + + (* ============================== P6 format ============================== *) + + fun parse6 contents = + let + val filetype = Parse.parseString (Seq.subseq contents (0, 2)) + val _ = + if filetype = "P6" then () + else raise Fail "should not happen" + + fun findFirst p i = + if i >= Seq.length contents then + NONE + else if p (Seq.nth contents i) then + SOME i + else + findFirst p (i+1) + + fun findToken start = + case findFirst (not o Char.isSpace) start of + NONE => NONE + | SOME i => + case findFirst Char.isSpace i of + NONE => SOME (i, Seq.length contents) + | SOME j => SOME (i, j) + + (* start must be on a space *) + fun chompToken start = + case findToken start of + NONE => NONE + | SOME (i, j) => SOME (Seq.subseq contents (i, j-i), j) + + fun chompInt thingName i = + case chompToken i of + NONE => raise Fail ("error parsing .ppm file: missing " ^ thingName) + | SOME (s, j) => + case Parse.parseInt s of + NONE => raise Fail ("error parsing .ppm file: cannot parse " + ^ thingName ^ " from '" + ^ niceify (Parse.parseString s) ^ "'") + | SOME x => + if x >= 0 then (x, j) + else raise Fail ("error parsing .ppm file: cannot parse " + ^ thingName ^ " from '" + ^ niceify (Parse.parseString s) ^ "'") + + val cursor = 2 + val _ = + if Seq.length contents > 2 andalso + Char.isSpace (Seq.nth contents 2) + then () + else raise Fail "error parsing .ppm file: unexpected format" + + val (width, cursor) = chompInt "width" cursor + val (height, cursor) = chompInt "height" cursor + val (resolution, cursor) = chompInt "max color value" cursor + + val numChannels = 3 * width * height + + val _ = + if resolution = 255 then () + else raise Fail "error parsing .ppm file: P6 max color value must be 255" + + val cursor = + case findFirst (not o Char.isSpace) cursor of + SOME i => i + | NONE => raise Fail "error parsing .ppm file: missing contents" + + val _ = + if Seq.length contents - cursor >= numChannels then () + else raise Fail "error parsing .ppm file: too few color channels" + + fun chan i = + Word8.fromInt (Char.ord (Seq.nth contents (cursor + i))) + + fun pixel i = + {red = chan (3*i), green = chan (3*i + 1), blue = chan (3*i + 2)} + in + { width = width + , height = height + , data = Seq.tabulate pixel (width * height) + } + end + + (* ================================= read ================================= *) + + fun read filepath = + let + val contents = ReadFile.contentsSeq filepath + in + case Parse.parseString (Seq.subseq contents (0, 2)) of + "P3" => parse3 contents + | "P6" => parse6 contents + | _ => raise Fail "error parsing .ppm file: unknown or unsupported format" + end + + (* ================================ write ================================ *) + (* for now, only writes to format P6 *) + + fun write filepath image = + let + val file = TextIO.openOut filepath + + fun dump str = TextIO.output (file, str) + fun dumpChan c = TextIO.output1 (file, Char.chr (Word8.toInt c)) + fun dumpPx i j = + let + val {red, green, blue} = elem image (i, j) + in + (dumpChan red; + dumpChan green; + dumpChan blue) + end + + fun dumpLoop i j = + if i >= #height image then () + else if j >= #width image then + dumpLoop (i+1) 0 + else + (dumpPx i j; dumpLoop i (j+1)) + in + dump "P6 "; + dump (Int.toString (#width image) ^ " "); + dump (Int.toString (#height image) ^ " "); + dump "255 "; + dumpLoop 0 0 + end + +end diff --git a/tests/mpllib/ParFuncArray.sml b/tests/mpllib/ParFuncArray.sml new file mode 100644 index 000000000..d85954562 --- /dev/null +++ b/tests/mpllib/ParFuncArray.sml @@ -0,0 +1,223 @@ +structure ParFuncArray: +sig + type 'a t + type 'a farray = 'a t + + val alloc: int -> 'a farray + val length: 'a farray -> int + val tabulate: int * (int -> 'a) -> 'a farray + val sub: 'a farray * int -> 'a + val update: 'a farray * int * 'a -> 'a farray +end = +struct + + (** ======================================================================== + * Log data structure + *) + + type version = int + + structure Log: + sig + type 'a t + type 'a log = 'a t + type capacity = int + + val new: capacity -> 'a log + + val getVersion: 'a log -> version -> 'a option + + (** Pushing onto a log might make it grow, in which case the old version + * should no longer be used. This is a performance optimization: we can + * store logs in an array and only update a log when it grows. (The + * alternative would be to wrap a log in ref, but this is an unnecessary + * indirection. + *) + val push: 'a log -> version * 'a -> 'a log + end = + struct + datatype 'a t = L of {data: (version * 'a) array, size: int} + type 'a log = 'a t + type capacity = int + + + fun new cap = + L {data = ForkJoin.alloc cap, size = 0} + (* L {data = SeqBasis.tabulate 10000 (0, cap) (fn _ => NONE), size = 0} *) + + + fun push (L {data, size}) (v, x) = + let + val i = size + in + if i < Array.length data then + (* ( Array.update (data, i, SOME (v,x)) *) + ( Array.update (data, i, (v,x)) + ; L {data = data, size = size+1} + ) + else + let + val data' = ForkJoin.alloc (2 * Array.length data) + in + ForkJoin.parfor 10000 (0, Array.length data) (fn j => + Array.update (data', j, Array.sub (data, j)) + ); +(* + ForkJoin.parfor 10000 (Array.length data, Array.length data') (fn j => + Array.update (data', j, NONE) + ); +*) + (* Array.update (data', i, SOME(v,x)); *) + Array.update (data', i, (v,x)); + (* print ("grown! " ^ Util.intToString (Array.length data') ^ "\n"); *) + L {data = data', size = i+1} + end + end + + + (* fun push (logs, logIdx) (v, x) = + case push' (Array.sub (logs, logIdx)) of + NONE => () + | SOME newlog => Array.update (logs, logIdx, newlog) *) + + + fun getVersion (L {data, size}) v = + if size = 0 then + NONE + else + let + val n = size + val _ = + if n <= Array.length data then () + else print ("getVersion: data length: " ^ Int.toString (Array.length data) ^ ", current size: " ^ Int.toString n ^ "\n") + +(* + fun loop i = + if i >= n then n + else if #1 (valOf (Array.sub (data, i))) <= v then + loop (i+1) + else + i + *) + in + if #1 ((*valOf*) (Array.sub (data, n-1))) < v then NONE else + let + val slice = ArraySlice.slice (data, 0, SOME n) + val idx = + BinarySearch.searchPosition slice + (fn (*SOME*) (v', _) => Int.compare (v, v') + (* | NONE => raise Fail "ParFuncArray.getVersion found empty slot" *) + ) + in + SOME (#2 ((*valOf*) (Array.sub (data, idx)))) + end + end + handle e => (print ("error during getVersion: " ^ exnMessage e ^ "\n"); raise e) + + end + + (** ======================================================================== + * Main functions + *) + + datatype 'a array_data = + AD of {vr: version ref, data: 'a array, logs: 'a Log.t array} + + type 'a t = version * 'a array_data + type 'a farray = 'a t + + + fun alloc n = + let + val version = 0 + val data = ForkJoin.alloc n + val logs = SeqBasis.tabulate 5000 (0, n) (fn _ => Log.new 1) + in + (version, AD {vr = ref version, data = data, logs = logs}) + end + + + fun length (_, AD {data, ...}) = Array.length data + + + fun sub (farr, i) = + if i < 0 orelse i >= length farr then + raise Subscript + else + let + val (v, AD {vr, data, logs}) = farr + val guess = Array.sub (data, i) + in + if v = !vr then + guess + else + case Log.getVersion (Array.sub (logs, i)) v of + NONE => guess + | SOME x => x + end + + + fun bcas r (old, new) = + old = Concurrency.cas r (old, new) + + + fun updateLog (logs, i) (v, x) = + let + val oldLog = Array.sub (logs, i) + val newLog = Log.push oldLog (v, x) + val oldLog' = Concurrency.casArray (logs, i) (oldLog, newLog) + in + if MLton.eq (oldLog, oldLog') then () + else raise Fail "updateLog failed somehow!" + end + + + fun update (farr, i, x) = + if i < 0 orelse i > length farr then + raise Subscript + else + let + val (v, ad as AD {vr, data, logs}) = farr + val currv = !vr + in + if + currv = v andalso + currv < Array.length data andalso + bcas vr (v, v+1) + then + (* We successfully claimed access for updating the data *) + ( updateLog (logs, i) (v, Array.sub (data, i)) + ; Array.update (data, i, x) + ; (v+1, ad) + ) + else (* We have to rebuid *) + let + val n = Array.length data + (* val _ = print ("rebuilding " ^ Util.intToString n ^ "\n") *) + val data' = SeqBasis.tabulate 1000 (0, n) (fn i => sub (farr, i)) + val logs' = SeqBasis.tabulate 5000 (0, n) (fn _ => Log.new 1) + in + Array.update (data', i, x); + (0, AD {vr = ref 0, data = data', logs = logs'}) + end + end + + + fun tabulate (n, f) = + let + val version = 0 + val data = ForkJoin.alloc n + val logs = SeqBasis.tabulate 5000 (0, n) (fn _ => Log.new 1) + in + ForkJoin.parfor 1000 (0, n) (fn i => + let + val x = f i + in + Array.update (data, i, f i) + (* updateLog (logs, i) (version, x) *) + end); + + (version, AD {vr = ref version, data = data, logs = logs}) + end + +end diff --git a/tests/mpllib/Parse.sml b/tests/mpllib/Parse.sml new file mode 100644 index 000000000..65e47afec --- /dev/null +++ b/tests/mpllib/Parse.sml @@ -0,0 +1,179 @@ +structure Parse = +struct + + fun parseDigit char = + let + val code = Char.ord char + val code0 = Char.ord #"0" + val code9 = Char.ord #"9" + in + if code < code0 orelse code9 < code then + NONE + else + SOME (code - code0) + end + + fun parseInt s = + let + val n = Seq.length s + fun c i = Seq.nth s i + + fun build x i = + if i >= n then SOME x else + case c i of + #"," => build x (i+1) + | #"_" => build x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => build (x * 10 + dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1) (build 0 1) + else if (c 0 = #"+") then + build 0 1 + else + build 0 0 + end + + fun parseReal s = + let + val n = Seq.length s + fun c i = Seq.nth s i + + fun buildAfterE x i = + Option.map (fn e => x * Math.pow (10.0, Real.fromInt e)) + (parseInt (Seq.subseq s (i, n-i))) + + fun buildAfterPoint m x i = + if i >= n then SOME x else + case c i of + #"," => buildAfterPoint m x (i+1) + | #"_" => buildAfterPoint m x (i+1) + | #"." => NONE + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildAfterPoint (m * 0.1) (x + m * (Real.fromInt dig)) (i+1) + + fun buildBeforePoint x i = + if i >= n then SOME x else + case c i of + #"," => buildBeforePoint x (i+1) + | #"_" => buildBeforePoint x (i+1) + | #"." => buildAfterPoint 0.1 x (i+1) + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildBeforePoint (x * 10.0 + Real.fromInt dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1.0) (buildBeforePoint 0.0 1) + else + buildBeforePoint 0.0 0 + end + + fun parseString s = + CharVector.tabulate (Seq.length s, Seq.nth s) + + (* read a Word16, big endian, starting at index i *) + fun r16b bytes i = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes i) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+1))) + in + Word16.fromLarge w + end + + (* read a Word32, big endian, starting at index i *) + fun r32b bytes i = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes i) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+1))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+2))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+3))) + in + Word32.fromLarge w + end + + (* read a Word64, big endian, starting at index i *) + fun r64b bytes i = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes i) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+1))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+2))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+3))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+4))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+5))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+6))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+7))) + in + w + end + + (* read a Word16, little endian, starting at index i *) + fun r16l bytes i = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes (i+1)) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes i)) + in + Word16.fromLarge w + end + + (* read a Word32, little endian, starting at index i *) + fun r32l bytes i = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes (i+3)) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+2))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+1))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes i)) + in + Word32.fromLarge w + end + + (* read a Word64, little endian, starting at index i *) + fun r64l bytes i = + let + infix 2 << orb + val op<< = Word64.<< + val op orb = Word64.orb + + val w = Word8.toLarge (Seq.nth bytes (i+7)) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+6))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+5))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+4))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+3))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+2))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes (i+1))) + val w = (w << 0w8) orb (Word8.toLarge (Seq.nth bytes i)) + in + w + end + +end diff --git a/tests/mpllib/ParseFile.sml b/tests/mpllib/ParseFile.sml new file mode 100644 index 000000000..cee33c0b1 --- /dev/null +++ b/tests/mpllib/ParseFile.sml @@ -0,0 +1,190 @@ +(** SAM_NOTE: copy/pasted... some repetition here with Parse. *) +structure ParseFile = +struct + + structure RF = ReadFile + structure Seq = ArraySequence + structure DS = DelayedSeq + + fun tokens (f: char -> bool) (cs: char Seq.t) : (char DS.t) DS.t = + let + val n = Seq.length cs + val s = DS.tabulate (Seq.nth cs) n + val indices = DS.tabulate (fn i => i) (n+1) + fun check i = + if (i = n) then not (f(DS.nth s (n-1))) + else if (i = 0) then not (f(DS.nth s 0)) + else let val i1 = f (DS.nth s i) + val i2 = f (DS.nth s (i-1)) + in (i1 andalso not i2) orelse (i2 andalso not i1) end + val ids = DS.filter check indices + val res = DS.tabulate (fn i => + let val (start, e) = (DS.nth ids (2*i), DS.nth ids (2*i+1)) + in DS.tabulate (fn i => Seq.nth cs (start+i)) (e - start) + end) + ((DS.length ids) div 2) + in + res + end + + fun eqStr str (chars : char DS.t) = + let + val n = String.size str + fun checkFrom i = + i >= n orelse + (String.sub (str, i) = DS.nth chars i andalso checkFrom (i+1)) + in + DS.length chars = n + andalso + checkFrom 0 + end + + fun parseDigit char = + let + val code = Char.ord char + val code0 = Char.ord #"0" + val code9 = Char.ord #"9" + in + if code < code0 orelse code9 < code then + NONE + else + SOME (code - code0) + end + + (* This implementation doesn't work with mpl :( + * Need to fix the basis library... *) + (* + fun parseReal chars = + let + val str = CharVector.tabulate (DS.length chars, DS.nth chars) + in + Real.fromString str + end + *) + + fun parseInt (chars : char DS.t) = + let + val n = DS.length chars + fun c i = DS.nth chars i + + fun build x i = + if i >= n then SOME x else + case c i of + #"," => build x (i+1) + | #"_" => build x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => build (x * 10 + dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1) (build 0 1) + else if (c 0 = #"+") then + build 0 1 + else + build 0 0 + end + + fun parseReal (chars : char DS.t) = + let + val n = DS.length chars + fun c i = DS.nth chars i + + fun buildAfterE x i = + let + val chars' = DS.subseq chars (i, n-i) + in + Option.map (fn e => x * Math.pow (10.0, Real.fromInt e)) + (parseInt chars') + end + + fun buildAfterPoint m x i = + if i >= n then SOME x else + case c i of + #"," => buildAfterPoint m x (i+1) + | #"_" => buildAfterPoint m x (i+1) + | #"." => NONE + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildAfterPoint (m * 0.1) (x + m * (Real.fromInt dig)) (i+1) + + fun buildBeforePoint x i = + if i >= n then SOME x else + case c i of + #"," => buildBeforePoint x (i+1) + | #"_" => buildBeforePoint x (i+1) + | #"." => buildAfterPoint 0.1 x (i+1) + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildBeforePoint (x * 10.0 + Real.fromInt dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1.0) (buildBeforePoint 0.0 1) + else + buildBeforePoint 0.0 0 + end + + fun readSequencePoint2d filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequencePoint2d" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun r i = Option.valOf (parseReal (tok (1 + i))) + + fun pt i = + (r (2*i), r (2*i+1)) + handle e => raise Fail ("error parsing point " ^ Int.toString i ^ " (" ^ exnMessage e ^ ")") + + val result = Seq.tabulate pt (n div 2) + in + result + end + + fun readSequenceInt filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequenceInt" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun p i = + Option.valOf (parseInt (tok (1 + i))) + handle e => raise Fail ("error parsing integer " ^ Int.toString i) + in + Seq.tabulate p n + end + + fun readSequenceReal filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequenceDouble" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun p i = + Option.valOf (parseReal (tok (1 + i))) + handle e => raise Fail ("error parsing double value " ^ Int.toString i) + in + Seq.tabulate p n + end + +end diff --git a/tests/mpllib/PureSeq.sml b/tests/mpllib/PureSeq.sml new file mode 100644 index 000000000..633b8938c --- /dev/null +++ b/tests/mpllib/PureSeq.sml @@ -0,0 +1,222 @@ +structure PureSeq :> +sig + type 'a seq = 'a VectorSlice.slice + type 'a t = 'a seq + + val nth: 'a seq -> int -> 'a + val length: 'a seq -> int + + val empty: unit -> 'a seq + val fromList: 'a list -> 'a seq + val fromSeq: 'a Seq.t -> 'a seq + + val tabulate: (int -> 'a) -> int -> 'a seq + val tabulateG: int -> (int -> 'a) -> int -> 'a seq + val map: ('a -> 'b) -> 'a seq -> 'b seq + + val filter: ('a -> bool) -> 'a seq -> 'a seq + val filterIdx: (int * 'a -> bool) -> 'a seq -> 'a seq + + val reduce: ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a + val scan: ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a seq * 'a + val scanIncl: ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a seq + + val subseq: 'a seq -> int * int -> 'a seq + val take: 'a seq -> int -> 'a seq + val drop: 'a seq -> int -> 'a seq + + val merge: ('a * 'a -> order) -> 'a seq * 'a seq -> 'a seq + val quicksort: ('a * 'a -> order) -> 'a seq -> 'a seq + + val summarize: int -> ('a -> string) -> 'a seq -> string + + val foreach: 'a seq -> (int * 'a -> unit) -> unit +end = +struct + + structure A = Array + structure AS = ArraySlice + structure V = Vector + structure VS = VectorSlice + + val gran = 5000 + + type 'a seq = 'a VS.slice + type 'a t = 'a seq + + val unsafeCast: 'a array -> 'a vector = VectorExtra.unsafeFromArray + + fun nth s i = VS.sub (s, i) + fun length s = VS.length s + fun empty () = VS.full (V.fromList []) + fun fromList xs = VS.full (V.fromList xs) + + fun subseq s (i, n) = VS.subslice (s, i, SOME n) + fun take s k = subseq s (0, k) + fun drop s k = subseq s (k, length s - k) + + fun tabulate f n = + VS.full (unsafeCast (SeqBasis.tabulate gran (0, n) f)) + + fun tabulateG gran f n = + VS.full (unsafeCast (SeqBasis.tabulate gran (0, n) f)) + + fun map f s = + tabulate (f o nth s) (length s) + + fun filter p s = + VS.full (unsafeCast (SeqBasis.filter gran (0, length s) (nth s) (p o nth s))) + + fun filterIdx p s = + VS.full (unsafeCast (SeqBasis.filter gran (0, length s) (nth s) + (fn i => p (i, nth s i)) + )) + + fun foreach s f = + ForkJoin.parfor gran (0, length s) (fn i => f (i, nth s i)) + + fun fromSeq xs = + tabulate (Seq.nth xs) (Seq.length xs) + + fun reduce f b s = + SeqBasis.reduce gran f b (0, length s) (nth s) + + fun scan f b s = + let + val n = length s + val v = VS.full (unsafeCast (SeqBasis.scan gran f b (0, n) (nth s))) + in + (take v n, nth v n) + end + + fun scanIncl f b s = + let + val n = length s + val v = VS.full (unsafeCast (SeqBasis.scan gran f b (0, n) (nth s))) + in + drop v 1 + end + + (** ======================================================================== + * Merge + * + * This is copied from the Merge.sml implementation and modified slightly + * to make it work with vectors. Really should just parameterize the + * Merge.sml by a Seq implementation... or do the func-sequence trick + * (pass the input sequences by length/nth functions) + *) + + fun sliceIdxs s i j = + VS.subslice (s, i, SOME (j-i)) + + fun arraySliceIdxs s i j = + AS.subslice (s, i, SOME (j-i)) + + fun search cmp s x = + let + fun loop lo hi = + case hi - lo of + 0 => lo + | n => + let + val mid = lo + n div 2 + val pivot = nth s mid + in + case cmp (x, pivot) of + LESS => loop lo mid + | EQUAL => mid + | GREATER => loop (mid+1) hi + end + in + loop 0 (length s) + end + + fun writeMergeSerial cmp (s1, s2) t = + let + fun write i x = AS.update (t, i, x) + + val n1 = length s1 + val n2 = length s2 + + (* i1 index into s1 + * i2 index into s2 + * j index into output *) + fun loop i1 i2 j = + if i1 = n1 then + foreach (sliceIdxs s2 i2 n2) (fn (i, x) => write (i+j) x) + else if i2 = n2 then + foreach (sliceIdxs s1 i1 n1) (fn (i, x) => write (i+j) x) + else + let + val x1 = nth s1 i1 + val x2 = nth s2 i2 + in + case cmp (x1, x2) of + LESS => (write j x1; loop (i1+1) i2 (j+1)) + | _ => (write j x2; loop i1 (i2+1) (j+1)) + end + in + loop 0 0 0 + end + + fun writeMerge cmp (s1: 'a seq, s2: 'a seq) (t: 'a AS.slice) = + if AS.length t <= gran then + writeMergeSerial cmp (s1, s2) t + else if length s1 = 0 then + foreach s2 (fn (i, x) => AS.update (t, i, x)) + else + let + val n1 = length s1 + val n2 = length s2 + val mid1 = n1 div 2 + val pivot = nth s1 mid1 + val mid2 = search cmp s2 pivot + + val l1 = sliceIdxs s1 0 mid1 + val r1 = sliceIdxs s1 (mid1+1) n1 + val l2 = sliceIdxs s2 0 mid2 + val r2 = sliceIdxs s2 mid2 n2 + + val _ = AS.update (t, mid1+mid2, pivot) + val tl = arraySliceIdxs t 0 (mid1+mid2) + val tr = arraySliceIdxs t (mid1+mid2+1) (AS.length t) + in + ForkJoin.par + (fn _ => writeMerge cmp (l1, l2) tl, + fn _ => writeMerge cmp (r1, r2) tr); + () + end + + fun merge cmp (s1, s2) = + let + val out = ForkJoin.alloc (length s1 + length s2) + in + writeMerge cmp (s1, s2) (AS.full out); + VS.full (unsafeCast out) + end + + fun quicksort cmp s = + let + val out = SeqBasis.tabulate gran (0, length s) (nth s) + in + Quicksort.sortInPlace cmp (AS.full out); + VS.full (unsafeCast out) + end + + fun summarize count toString xs = + let + val n = length xs + fun elem i = nth xs i + + val strs = + if count <= 0 then raise Fail "PureSeq.summarize needs count > 0" + else if count <= 2 orelse n <= count then + List.tabulate (n, toString o elem) + else + List.tabulate (count-1, toString o elem) @ + ["...", toString (elem (n-1))] + in + "[" ^ (String.concatWith ", " strs) ^ "]" + end + +end diff --git a/tests/mpllib/Quicksort.sml b/tests/mpllib/Quicksort.sml new file mode 100644 index 000000000..f00322190 --- /dev/null +++ b/tests/mpllib/Quicksort.sml @@ -0,0 +1,133 @@ +(* Author: The 210 Team + * + * Uses dual-pivot quicksort from: + * + * Dual-Pivot Quicksort Algorithm + * Vladimir Yaroslavskiy + * http://codeblab.com/wp-content/uploads/2009/09/DualPivotQuicksort.pdf + * 2009 + * + * Insertion sort is taken from the SML library ArraySort + *) + +structure Quicksort: +sig + type 'a seq = 'a ArraySlice.slice + val sortInPlaceG : int -> ('a * 'a -> order) -> 'a seq -> unit + val sortInPlace : ('a * 'a -> order) -> 'a seq -> unit + val sortG : int -> ('a * 'a -> order) -> 'a seq -> 'a seq + val sort : ('a * 'a -> order) -> 'a seq -> 'a seq +end = +struct + + type 'a seq = 'a ArraySlice.slice + + structure A = Array + structure AS = ArraySlice + + fun sortRange grainsize (array, start, n, compare) = + let + val sub = A.sub + val update = A.update + + fun item i = sub(array,i) + fun set(i,v) = update(array,i,v) + fun cmp(i,j) = compare(item i, item j) + + fun swap (i,j) = + let val tmp = item i + in set(i, item j); set(j, tmp) end + + (* same as swap(j,k); swap(i,j) *) + fun rotate(i,j,k) = + let val tmp = item k + in set(k, item j); set(j, item i); set(i, tmp) end + + fun insertSort (start, n) = + let val limit = start+n + fun outer i = + if i >= limit then () + else let fun inner j = + if j = start then outer(i+1) + else let val j' = j - 1 + in if cmp(j', j) = GREATER + then (swap(j,j'); inner j') + else outer(i+1) + end + in inner i end + in outer (start+1) end + + (* puts lesser pivot at start and larger at end *) + fun twoPivots(a, n) = + let fun sortToFront(size) = + let val m = n div (size + 1) + fun toFront(i) = + if (i < size) then (swap(a + i, a + m*(i+1)); toFront(i+1)) + else () + in (toFront(0); insertSort(a,size)) end + in if (n < 80) then + (if cmp(a, a+n-1) = GREATER then swap(a,a+n-1) else ()) + else (sortToFront(5); swap(a+1,a); swap(a+3,a+n-1)) + end + + (* splits based on two pivots (p1 and p2) into 3 parts: + less than p1, greater than p2, and the rest in the middle. + The pivots themselves end up at the two ends. + If the pivots are the same, returns a false flag to indicate middle + need not be sorted. *) + fun split3 (a, n) = + let + val (p1,p2) = (twoPivots(a,n); (a, a+n-1)) + fun right(r) = if cmp(r, p2) = GREATER then right(r-1) else r + fun loop(l,m,r) = + if (m > r) then (l,m) + else if cmp(m, p1) = LESS then (swap(m,l); loop(l+1, m+1, r)) + else (if cmp(m, p2) = GREATER then + (if cmp(r, p1) = LESS + then (rotate(l,m,r); loop(l+1, m+1, right(r-1))) + else (swap(m,r); loop(l, m+1, right(r-1)))) + else loop(l, m+1, r)) + val (l,m) = loop(a + 1, a + 1, right(a + n - 2)) + in (l, m, cmp(p1, p2) = LESS) end + + (* makes recursive calls in parallel if big enough *) + fun qsort (a, n) = + if (n < 16) then insertSort(a, n) + else let + val (l, m, doMid) = split3(a,n) + in if (n <= grainsize) then + (qsort (a, l-a); + (if doMid then qsort(l, m-l) else ()); + qsort (m, a+n-m)) + else let val par = ForkJoin.par + val left = (fn () => qsort (a, l-a)) + val mid = (fn () => qsort (l, m-l)) + val right = (fn () => qsort (m, a+n-m)) + val maybeMid = if doMid then (fn () => (par(mid,right);())) + else right + in par(left,maybeMid);() end + end + + in qsort (start,n) end + + (* sorts an array slice in place *) + fun sortInPlaceG grainsize compare aslice = + let val (a, i, n) = AS.base aslice + in sortRange grainsize (a, i, n, compare) + end + + fun sortG grainsize compare aslice = + let + val result = AS.full (ForkJoin.alloc (AS.length aslice)) + in + Util.foreach aslice (fn (i, x) => AS.update (result, i, x)); + sortInPlaceG grainsize compare result; + result + end + + val grainsize = 8192 + + fun sortInPlace c s = sortInPlaceG grainsize c s + fun sort c s = sortG grainsize c s + +end diff --git a/tests/mpllib/RadixSort.sml b/tests/mpllib/RadixSort.sml new file mode 100644 index 000000000..26a36a275 --- /dev/null +++ b/tests/mpllib/RadixSort.sml @@ -0,0 +1,145 @@ +(* Author: Lawrence Wang (lawrenc2@andrew.cmu.edu, github.com/larry98) + * + * The lsdSort and msdSort functions take the following arguments: + * - s : 'a ArraySequence.t + * the array (of strings) to sort + * - bucket : 'a ArraySequence.t -> int -> int -> int + * bucket s k i specifies which bucket the k'th digit of the i'th element + * of s should map to + * - numPasses : int + * the number of counting sort passes to make i.e. the number of digits + * in the strings of the array + * - numBuckets : int + * the number of buckets used in counting sort + * + * The quicksort function implements 3-way radix quicksort and takes the + * following arguments: + * - s : 'a ArraySequence.t + * the array (of strings) to sort + * - cmp : int -> 'a * 'a -> order + * cmp k (x, y) specifies the comparison between the kth digit of x with + * the kth digit of y + * - numPasses : int + * the maximum number of quicksort passes to make (commonly the maximum + * length of the strings being sorted) + *) +structure RadixSort :> +sig + val lsdSort : 'a Seq.t -> ('a Seq.t -> int -> int -> int) + -> int -> int -> 'a Seq.t + val msdSort : 'a Seq.t -> ('a Seq.t -> int -> int -> int) + -> int -> int -> 'a Seq.t + val quicksort : 'a Seq.t -> (int -> 'a * 'a -> order) -> int + -> 'a Seq.t +end = +struct + + structure AS = + struct + open ArraySlice + open Seq + + val GRAIN = 4096 + val ASupdate = ArraySlice.update + val alloc = ForkJoin.alloc + end + + fun lsdSort s bucket numPasses numBuckets = + let + fun loop s i = + if i < 0 then s + else loop (#1 (CountingSort.sort s (bucket s i) numBuckets)) (i - 1) + in + loop s (numPasses - 1) + end + + fun msdSort s bucket numPasses numBuckets = + let + val n = AS.length s + val result = ArraySlice.full (AS.alloc n) + fun msdSort' s pass lo hi = + if pass = numPasses then + ForkJoin.parfor AS.GRAIN (0, hi - lo) (fn i => + AS.ASupdate (result, lo + i, AS.nth s i) + ) + else + let + val (s', offsets) = CountingSort.sort s (bucket s pass) numBuckets + in + ForkJoin.parfor AS.GRAIN (0, numBuckets) (fn i => + let + val start = AS.nth offsets i + val len = if i = numBuckets - 1 then AS.length s' - start + else AS.nth offsets (i + 1) - start + val s'' = AS.subseq s' (start, len) + in + if len = 0 then () + else if len = 1 then + AS.ASupdate (result, lo + start, AS.nth s'' 0) + else + msdSort' s'' (pass + 1) (lo + start) (lo + start + len) + end + ) + end + val () = msdSort' s 0 0 n + in + result + end + + fun par3 (a, b, c) = + let + val ((ar, br), cr) = ForkJoin.par (fn _ => ForkJoin.par (a, b), c) + in + (ar, br, cr) + end + + fun quicksort s cmp numPasses = + let + val n = AS.length s + val result = ArraySlice.full (AS.alloc n) + (* TODO: Change to insertion sort if size of array is small *) + fun quicksort' s digit lo hi seed = + if hi = lo then () + else if hi - lo = 1 then AS.ASupdate (result, lo, AS.nth s 0) + else if digit = numPasses then + ForkJoin.parfor AS.GRAIN (0, hi - lo) (fn i => + AS.ASupdate (result, lo + i, AS.nth s i) + ) + else + let + val n' = hi - lo + val pivot = AS.nth s (seed mod n') + fun bucket i = + case cmp digit (AS.nth s i, pivot) of + LESS => 0 + | EQUAL => 1 + | GREATER => 2 + val (s', offsets) = CountingSort.sort s bucket 3 + val mid1 = AS.nth offsets 1 + val mid2 = AS.nth offsets 2 + val seed1 = Util.hash (seed + 1) + val seed2 = Util.hash (seed + 2) + val seed3 = Util.hash (seed + 3) + val s1 = AS.subseq s' (0, mid1) + val s2 = AS.subseq s' (mid1, mid2 - mid1) + val s3 = AS.subseq s' (mid2, n' - mid2) + val () = if hi - lo < 1024 then ( + quicksort' s1 digit lo (lo + mid1) seed1; + quicksort' s2 (digit + 1) (lo + mid1) (lo + mid2) seed2; + quicksort' s3 digit (lo + mid2) hi seed3 + ) else ( + let val ((), (), ()) = par3 ( + fn () => quicksort' s1 digit lo (lo + mid1) seed1, + fn () => quicksort' s2 (digit + 1) (lo + mid1) (lo + mid2) seed2, + fn () => quicksort' s3 digit (lo + mid2) hi seed3 + ) in () end + ) + in + () + end + val () = quicksort' s 0 0 n (Util.hash 0) + in + result + end + +end diff --git a/tests/mpllib/Rat.sml b/tests/mpllib/Rat.sml new file mode 100644 index 000000000..e8db17721 --- /dev/null +++ b/tests/mpllib/Rat.sml @@ -0,0 +1,145 @@ +structure Rat :> +sig + type t + type i = IntInf.int + + (* make(n, d) ~> n/d *) + val make: i * i -> t + val view: t -> i * i + + val normalize: t -> t + + val * : t * t -> t + val - : t * t -> t + val + : t * t -> t + val div: t * t -> t + + val max: t * t -> t + + val sign: t -> int + val compare: t * t -> order + + val approx: t -> Real64.real + + val toString: t -> string +end = +struct + + type i = IntInf.int + type t = i * i + + fun make (n, d) = (n, d) + fun view (n, d) = (n, d) + + fun gcd (a, b) = + if b = 0 then a else gcd (b, IntInf.mod (a, b)) + + fun normalize (n, d) = + if n = 0 then + (0, 1) + else + let + val same = IntInf.sameSign (n, d) + + val na = IntInf.abs n + val da = IntInf.abs d + + val g = gcd (na, da) + + val n' = IntInf.div (na, g) + val d' = IntInf.div (da, g) + in + if same then (n', d') else (IntInf.~ n', d') + end + + fun mul ((a, b): t, (c, d)) = (a * c, b * d) + + fun add ((a, b): t, (c, d)) = + (a * d + b * c, b * d) + + fun sub ((a, b): t, (c, d)) = + (a * d - b * c, b * d) + + fun divv ((a, b): t, (c, d)) = (a * d, b * c) + + + fun sign (n, d) = + if n = 0 then 0 else if IntInf.sameSign (n, d) then 1 else ~1 + + + fun compare (r1, r2) = + let + val diff = sub (r1, r2) + val s = sign diff + in + if s < 0 then LESS else if s = 0 then EQUAL else GREATER + end + + + fun max (r1, r2) = + case compare (r1, r2) of + LESS => r2 + | _ => r1 + + + fun itor x = + Real64.fromLargeInt (IntInf.toLarge x) + + + (* ========================================================================= + * approximate a rational with Real64 + * + * TODO: not sure what the best way to do this is. Kinda just threw something + * together. It's probably kinda messed up in a subtle way. + *) + + local + fun loopApprox acc (r, d) = + let + val r' = itor r + val d' = itor d + in + if Real64.isFinite r' andalso Real64.isFinite d' then + acc + r' / d' + else + let + val d2 = d div 2 + in + if r > d2 then + loopApprox (acc + 0.5) (normalize (r - d2, d)) + else + (* no idea how good this is... *) + loopApprox acc (normalize (r div 2, d div 2)) + end + end + in + fun approx (n, d) = + let + val (n, d) = normalize (n, d) + val s = Real64.fromInt (IntInf.sign n) + val n = IntInf.abs n + + (* abs(n/d) = m + r/d + * where: m is a natural number + * and: r/d is a proper fraction + *) + val (m, r) = IntInf.divMod (n, d) + val m = itor m + in + if not (Real64.isFinite m) then s * m + else s * (m + loopApprox 0.0 (normalize (r, d))) + end + end + + (* ======================================================================= *) + + fun toString (n, d) = + IntInf.toString n ^ "/" ^ IntInf.toString d + + (* ======================================================================= *) + + val op* = mul + val op+ = add + val op- = sub + val op div = divv +end diff --git a/tests/mpllib/RecursiveStream.sml b/tests/mpllib/RecursiveStream.sml new file mode 100644 index 000000000..8073f6d7c --- /dev/null +++ b/tests/mpllib/RecursiveStream.sml @@ -0,0 +1,181 @@ +structure RecursiveStream :> STREAM = +struct + + (** Intended use: when the stream is consumed, the indices are provided + * as input. + * + * For example, if `stream` represents elements x0,x1,... + * val S a = stream + * val (x0, S b) = a 0 + * val (x1, S c) = b 1 + * val (x2, S d) = c 2 + * ... + * + * Requiring the index in this way is just an optimization: we know that all + * streams require at least an index as state, so we can save storing one + * piece of state by instead providing this only as needed. + *) + datatype 'a stream = S of int -> 'a * ('a stream) + type 'a t = 'a stream + + fun nth stream i = + let + fun loop j (S g) = + let + val (first, rest) = g j + in + if j >= i then first else loop (j+1) rest + end + in + loop 0 stream + end + + fun tabulate f = S (fn i => (f i, tabulate f)) + + fun map f (S g) = + S (fn i => + let + val (first, rest) = g i + in + (f first, map f rest) + end) + + fun mapIdx f (S g) = + S (fn i => + let + val (first, rest) = g i + in + (f (i, first), mapIdx f rest) + end) + + fun zipWith f (S g, S h) = + S (fn i => + let + val (gfirst, grest) = g i + val (hfirst, hrest) = h i + in + (f (gfirst, hfirst), zipWith f (grest, hrest)) + end) + + fun iteratePrefixes f z (S g) = + S (fn i => + let + val (first, rest) = g i + val z' = f (z, first) + in + (z, iteratePrefixes f z' rest) + end) + + fun iteratePrefixesIncl f z (S g) = + S (fn i => + let + val (first, rest) = g i + val z' = f (z, first) + in + (z', iteratePrefixesIncl f z' rest) + end) + + fun applyIdx (n, stream) f = + let + fun loop i (S g) = + if i >= n then () else + let + val (first, rest) = g i + in + f (i, first); + loop (i+1) rest + end + in + loop 0 stream + end + + fun iterate f z (n, stream) = + let + fun loop acc i (S g) = + if i >= n then acc else + let + val (first, rest) = g i + val acc' = f (acc, first) + in + loop acc' (i+1) rest + end + in + loop z 0 stream + end + + fun resize arr = + let + val newCapacity = 2 * Array.length arr + val dst = ForkJoin.alloc newCapacity + in + Array.copy {src = arr, dst = dst, di = 0}; + dst + end + + fun pack f (length, stream) = + let + fun loop (data, next) i (S g) = + if i < length andalso next < Array.length data then + let + val (first, rest) = g i + in + case f first of + SOME y => + ( Array.update (data, next, y) + ; loop (data, next+1) (i+1) rest + ) + | NONE => + loop (data, next) (i+1) rest + end + + else if next >= Array.length data then + loop (resize data, next) i (S g) + + else + (data, next) + + val (data, count) = loop (ForkJoin.alloc 10, 0) 0 stream + in + ArraySlice.slice (data, 0, SOME count) + end + + + fun makeBlockStreams + { blockSize: int + , numChildren: int + , offset: int -> int + , getElem: int -> int -> 'a + } = + let + fun getBlock blockIdx = + let + fun advanceUntilNonEmpty i = + if i >= numChildren orelse offset i <> offset (i+1) then + i + else + advanceUntilNonEmpty (i+1) + + val lo = blockIdx * blockSize + + fun walk i = + S (fn idx => + let + val j = lo + idx - offset i + val elem = getElem i j + in + if offset i + j + 1 < offset (i+1) then + (elem, walk i) + else + (elem, walk (advanceUntilNonEmpty (i+1))) + end) + + val firstOuterIdx = + OffsetSearch.indexSearch (0, numChildren, offset) lo + in + walk firstOuterIdx + end + in + getBlock + end + +end diff --git a/tests/mpllib/SEQUENCE.sml b/tests/mpllib/SEQUENCE.sml new file mode 100644 index 000000000..4f7a2699e --- /dev/null +++ b/tests/mpllib/SEQUENCE.sml @@ -0,0 +1,73 @@ +signature SEQUENCE = +sig + type 'a t + type 'a seq = 'a t + (* type 'a ord = 'a * 'a -> order + datatype 'a listview = NIL | CONS of 'a * 'a seq + datatype 'a treeview = EMPTY | ONE of 'a | PAIR of 'a seq * 'a seq *) + + (* exception Range + exception Size *) + + val nth: 'a seq -> int -> 'a + val length: 'a seq -> int + val toList: 'a seq -> 'a list + val toString: ('a -> string) -> 'a seq -> string + val equal: ('a * 'a -> bool) -> 'a seq * 'a seq -> bool + + val empty: unit -> 'a seq + val singleton: 'a -> 'a seq + val tabulate: (int -> 'a) -> int -> 'a seq + val fromList: 'a list -> 'a seq + + val rev: 'a seq -> 'a seq + val append: 'a seq * 'a seq -> 'a seq + val flatten: 'a seq seq -> 'a seq + + val map: ('a -> 'b) -> 'a seq -> 'b seq + val mapOption: ('a -> 'b option) -> 'a seq -> 'b seq + val zip: 'a seq * 'b seq -> ('a * 'b) seq + val zipWith: ('a * 'b -> 'c) -> 'a seq * 'b seq -> 'c seq + val zipWith3: ('a * 'b * 'c -> 'd) -> 'a seq * 'b seq * 'c seq -> 'd seq + + val filter: ('a -> bool) -> 'a seq -> 'a seq + (* val filterSome: 'a option seq -> 'a seq *) + val filterIdx: (int * 'a -> bool) -> 'a seq -> 'a seq + + val enum: 'a seq -> (int * 'a) seq + val mapIdx: (int * 'a -> 'b) -> 'a seq -> 'b seq + (* val update: 'a seq * (int * 'a) -> 'a seq *) + val inject: 'a seq * (int * 'a) seq -> 'a seq + + val subseq: 'a seq -> int * int -> 'a seq + val take: 'a seq -> int -> 'a seq + val drop: 'a seq -> int -> 'a seq + (* val splitHead: 'a seq -> 'a listview *) + (* val splitMid: 'a seq -> 'a treeview *) + + val iterate: ('b * 'a -> 'b) -> 'b -> 'a seq -> 'b + (* val iteratePrefixes: ('b * 'a -> 'b) -> 'b -> 'a seq -> 'b seq * 'b *) + (* val iteratePrefixesIncl: ('b * 'a -> 'b) -> 'b -> 'a seq -> 'b seq *) + val reduce: ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a + val scan: ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a seq * 'a + val scanIncl: ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a seq + + (* val sort: 'a ord -> 'a seq -> 'a seq + val merge: 'a ord -> 'a seq * 'a seq -> 'a seq + val collect: 'a ord -> ('a * 'b) seq -> ('a * 'b seq) seq + val collate: 'a ord -> 'a seq ord + val argmax: 'a ord -> 'a seq -> int *) + + val $ : 'a -> 'a seq + val % : 'a list -> 'a seq + + val fromArraySeq: 'a ArraySlice.slice -> 'a seq + val toArraySeq: 'a seq -> 'a ArraySlice.slice + + val force: 'a seq -> 'a seq + + val applyIdx: 'a seq -> (int * 'a -> unit) -> unit + + (* val foreach: 'a seq -> (int * 'a -> unit) -> unit + val foreachG: int -> 'a seq -> (int * 'a -> unit) -> unit *) +end diff --git a/tests/mpllib/STREAM.sml b/tests/mpllib/STREAM.sml new file mode 100644 index 000000000..b3aa85ea8 --- /dev/null +++ b/tests/mpllib/STREAM.sml @@ -0,0 +1,27 @@ +signature STREAM = +sig + type 'a t + type 'a stream = 'a t + + val nth: 'a stream -> int -> 'a + + val tabulate: (int -> 'a) -> 'a stream + val map: ('a -> 'b) -> 'a stream -> 'b stream + val mapIdx: (int * 'a -> 'b) -> 'a stream -> 'b stream + val zipWith: ('a * 'b -> 'c) -> 'a stream * 'b stream -> 'c stream + val iteratePrefixes: ('b * 'a -> 'b) -> 'b -> 'a stream -> 'b stream + val iteratePrefixesIncl: ('b * 'a -> 'b) -> 'b -> 'a stream -> 'b stream + + val applyIdx: int * 'a stream -> (int * 'a -> unit) -> unit + val iterate: ('b * 'a -> 'b) -> 'b -> int * 'a stream -> 'b + + val pack: ('a -> 'b option) -> (int * 'a stream) -> 'b ArraySlice.slice + + val makeBlockStreams: + { blockSize: int + , numChildren: int + , offset: int -> int + , getElem: int -> int -> 'a + } + -> (int -> 'a stream) +end diff --git a/tests/mpllib/SampleSort.sml b/tests/mpllib/SampleSort.sml new file mode 100644 index 000000000..54328b9e4 --- /dev/null +++ b/tests/mpllib/SampleSort.sml @@ -0,0 +1,195 @@ +(* Author: Guy Blelloch + * + * This file is basically the cache-oblivious sorting algorithm from: + * + * Low depth cache-oblivious algorithms. + * Guy E. Blelloch, Phillip B. Gibbons and Harsha Vardhan Simhadri. + * Proc. ACM symposium on Parallelism in algorithms and architectures (SPAA), 2010 + * + * The main difference is that it does not recurse (using quicksort instead) + * and the merging with samples is sequential. + *) + +structure SampleSort :> +sig + type 'a seq = 'a ArraySlice.slice + + (* transpose (matrix, numRows, numCols) *) + val transpose : 'a seq * int * int -> 'a seq + + (* transposeBlocks (blockMatrix, srcOffsets, dstOffsets, counts, numRows, numCols, n) *) + val transposeBlocks : 'a seq * int seq * int seq * int seq * int * int * int -> 'a seq + + val sort : ('a * 'a -> order) -> 'a seq -> 'a seq +end = +struct + type 'a seq = 'a Seq.t + + structure A = Array + structure AS = ArraySlice + + val sortInPlace = Quicksort.sortInPlace + + val sub = A.sub + val update = A.update + + val par = ForkJoin.par + val for = Util.for + val parallelFor = ForkJoin.parfor + + fun for_l (lo, len) f = for (lo, lo + len) f + + fun matrixDandC baseCase (threshold, num_rows, num_cols) = + let fun r(rs, rl, cs, cl) = + if (rl*cl < threshold) then baseCase(rs, rl, cs, cl) + else if (cl > rl) then + (par (fn () => r(rs, rl, cs, cl div 2), + fn () => r(rs, rl, cs + (cl div 2), cl - (cl div 2))); ()) + else + (par (fn () => r(rs, rl div 2, cs, cl), + fn () => r(rs + (rl div 2), rl - (rl div 2), cs, cl)); ()) + in r(0, num_rows, 0, num_cols) end + + (* transposes a matrix *) + fun transpose(S, num_rows, num_cols) = + let + val seq_threshold = 8000 + val (SS, offset, n) = AS.base S + val _ = if (AS.length S) <> (num_rows * num_cols) then raise Size else () + val R = ForkJoin.alloc (num_rows * num_cols) + fun baseCase(row_start, row_len, col_start, col_len) = + for_l (row_start, row_len) (fn i => + for_l (col_start, col_len) (fn j => + update(R, j * num_rows + i, sub(SS,(i*num_cols + j + offset))))) + in (matrixDandC baseCase (seq_threshold, num_rows, num_cols); + AS.full(R)) + end + + (* transposes a matrix of blocks given source and destination pairs *) + fun transposeBlocks(S, source_offsets, dest_offsets, counts, num_rows, num_cols, n) = + let + val seq_threshold = 500 + val (SS, offset, n) = AS.base S + val R = ForkJoin.alloc n + fun baseCase(row_start, row_len, col_start, col_len) = + for (row_start, row_start + row_len) (fn i => + for (col_start, col_start + col_len) (fn j => let + val pa = offset + AS.sub (source_offsets, i*num_cols + j) + val pb = Seq.nth dest_offsets (j*num_rows + i) + val l = Seq.nth counts (i*num_cols + j) + in for (0,l) (fn k => update(R,pb+k,sub(SS, pa + k))) end)) + in (matrixDandC baseCase (seq_threshold, num_rows, num_cols); + AS.full(R)) + end + + (* merges a sequence of elements A with the samples S, putting counts in C *) + fun mergeWithSamples cmp (A, S, C) = + let + val num_samples = AS.length S + val n = AS.length A + fun merge(i,j) = + if (j = num_samples) then AS.update(C,j,n-i) + else + let fun merge'(i) = if (i < n andalso cmp(AS.sub (A, i), AS.sub (S, j)) = LESS) + then merge'(i+1) + else i + val k = merge'(i) + val _ = AS.update(C, j, k-i) + in merge(k,j+1) end + in merge(0,0) end + + fun sort cmp A = + let + val n = AS.length A + + (* parameters used in algorithm *) + val bucket_quotient = 3 + val block_quotient = 2 + val sqrt = Real.floor(Math.sqrt(Real.fromInt n)) + val num_blocks = sqrt div block_quotient + val block_size = ((n-1) div num_blocks) + 1 + val num_buckets = (sqrt div bucket_quotient) + 1 + val over_sample = 1 + ((n div num_buckets) div 500) + val sample_size = num_buckets * over_sample + val sample_stride = n div sample_size + val m = num_blocks*num_buckets + + (* val _ = print ("num_blocks " ^ Int.toString num_blocks ^ "\n") + val _ = print ("num_buckets " ^ Int.toString num_buckets ^ "\n") + val _ = print ("sample_size " ^ Int.toString sample_size ^ "\n") + val _ = print ("over_sample " ^ Int.toString over_sample ^ "\n") + val _ = print ("m " ^ Int.toString m ^ "\n") *) + + (* val t0 = Time.now () *) + + (* sort a sample of keys *) + val sample = Seq.tabulate (fn i => AS.sub (A, i*sample_stride)) sample_size + val _ = sortInPlace cmp sample + + (* val t1 = Time.now () + val _ = print ("sorted sample " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "\n") *) + + (* take a subsample *) + val sub_sample = Seq.tabulate (fn i => AS.sub (sample, (i+1)*over_sample)) (num_buckets-1) + + (* val t2 = Time.now () + val _ = print ("subsample " ^ Time.fmt 4 (Time.- (t2, t1)) ^ "\n") *) + + val counts = AS.full (ForkJoin.alloc m) + val B = AS.full (ForkJoin.alloc n) + + (* sort each block and merge with the pivots, giving a count of the number + of keys between each pivot in each block *) + val _ = + parallelFor 1 (0,num_blocks) (fn i => + let + val start = i * block_size + val len = Int.min((i+1)* block_size,n) - start + (* copy into B to avoid changing A *) + val _ = for (start, start+len) (fn j => AS.update(B, j, AS.sub (A, j))) + val B' = Seq.subseq B (start, len) + val _ = sortInPlace cmp B' + val counts' = Seq.subseq counts (i*num_buckets, num_buckets) + val _ = mergeWithSamples cmp (B', sub_sample, counts') + in () end) + + (* val t3 = Time.now () + val _ = print ("sort blocks " ^ Time.fmt 4 (Time.- (t3, t2)) ^ "\n") *) + + (* scan across the counts to get offset of each source bucket within each block *) + val (source_offsets,_) = Seq.scan op+ 0 counts + + (* transpose and scan across the counts to get offset of each + destination within each bucket *) + val tcounts = transpose(counts,num_blocks,num_buckets) + val (dest_offsets,_) = Seq.scan op+ 0 tcounts + + (* move data to correct destination *) + val C = transposeBlocks(B, source_offsets, dest_offsets, + counts, num_blocks, num_buckets, n) + + (* val t4 = Time.now () + val _ = print ("transpose data " ^ Time.fmt 4 (Time.- (t4, t3)) ^ "\n") *) + + (* get the start location of each bucket *) + fun bucket_offset i = + if (i = num_buckets) then n + else AS.sub (dest_offsets, i * num_blocks) + + (* sort the buckets *) + val _ = + parallelFor 1 (0, num_buckets) (fn i => + let + val start = bucket_offset i + val len = bucket_offset (i+1) - start + (* val start = AS.sub (bucket_offsets, i) + val len = (AS.sub (bucket_offsets, i+1)) - start *) + val _ = sortInPlace cmp (Seq.subseq C (start,len)) + in () end) + + (* val t5 = Time.now () + val _ = print ("sort buckets " ^ Time.fmt 4 (Time.- (t5, t4)) ^ "\n") *) + + in C end +end + diff --git a/tests/mpllib/SeqBasis.sml b/tests/mpllib/SeqBasis.sml new file mode 100644 index 000000000..1739c1f8c --- /dev/null +++ b/tests/mpllib/SeqBasis.sml @@ -0,0 +1,214 @@ +structure SeqBasis: +sig + type grain = int + + val tabulate: grain -> (int * int) -> (int -> 'a) -> 'a array + + val foldl: ('b * 'a -> 'b) + -> 'b + -> (int * int) + -> (int -> 'a) + -> 'b + + val foldr: ('b * 'a -> 'b) + -> 'b + -> (int * int) + -> (int -> 'a) + -> 'b + + val reduce: grain + -> ('a * 'a -> 'a) + -> 'a + -> (int * int) + -> (int -> 'a) + -> 'a + + val scan: grain + -> ('a * 'a -> 'a) + -> 'a + -> (int * int) + -> (int -> 'a) + -> 'a array (* length N+1, for both inclusive and exclusive scan *) + + val filter: grain + -> (int * int) + -> (int -> 'a) + -> (int -> bool) + -> 'a array + + val tabFilter: grain + -> (int * int) + -> (int -> 'a option) + -> 'a array +end = +struct + + type grain = int + + structure A = Array + structure AS = ArraySlice + + (* + fun upd a i x = Unsafe.Array.update (a, i, x) + fun nth a i = Unsafe.Array.sub (a, i) + *) + + fun upd a i x = A.update (a, i, x) + fun nth a i = A.sub (a, i) + + val parfor = ForkJoin.parfor + val par = ForkJoin.par + val allocate = ForkJoin.alloc + + fun tabulate grain (lo, hi) f = + let + val n = hi-lo + val result = allocate n + in + if lo = 0 then + parfor grain (0, n) (fn i => upd result i (f i)) + else + parfor grain (0, n) (fn i => upd result i (f (lo+i))); + + result + end + + fun foldl g b (lo, hi) f = + if lo >= hi then b else + let + val b' = g (b, f lo) + in + foldl g b' (lo+1, hi) f + end + + fun foldr g b (lo, hi) f = + if lo >= hi then b else + let + val hi' = hi-1 + val b' = g (b, f hi') + in + foldr g b' (lo, hi') f + end + + fun reduce grain g b (lo, hi) f = + if hi - lo <= grain then + foldl g b (lo, hi) f + else + let + val n = hi - lo + val k = grain + val m = 1 + (n-1) div k (* number of blocks *) + + fun red i j = + case j - i of + 0 => b + | 1 => foldl g b (lo + i*k, Int.min (lo + (i+1)*k, hi)) f + | n => let val mid = i + (j-i) div 2 + in g (par (fn _ => red i mid, fn _ => red mid j)) + end + in + red 0 m + end + + fun scan grain g b (lo, hi) (f : int -> 'a) = + if hi - lo <= grain then + let + val n = hi - lo + val result = allocate (n+1) + fun bump ((j,b),x) = (upd result j b; (j+1, g (b, x))) + val (_, total) = foldl bump (0, b) (lo, hi) f + in + upd result n total; + result + end + else + let + val n = hi - lo + val k = grain + val m = 1 + (n-1) div k (* number of blocks *) + val sums = tabulate 1 (0, m) (fn i => + let val start = lo + i*k + in foldl g b (start, Int.min (start+k, hi)) f + end) + val partials = scan grain g b (0, m) (nth sums) + val result = allocate (n+1) + in + parfor 1 (0, m) (fn i => + let + fun bump ((j,b),x) = (upd result j b; (j+1, g (b, x))) + val start = lo + i*k + in + foldl bump (i*k, nth partials i) (start, Int.min (start+k, hi)) f; + () + end); + upd result n (nth partials m); + result + end + + fun filter grain (lo, hi) f g = + let + val n = hi - lo + val k = grain + val m = 1 + (n-1) div k (* number of blocks *) + fun count (i, j) c = + if i >= j then c + else if g i then count (i+1, j) (c+1) + else count (i+1, j) c + val counts = tabulate 1 (0, m) (fn i => + let val start = lo + i*k + in count (start, Int.min (start+k, hi)) 0 + end) + val offsets = scan grain op+ 0 (0, m) (nth counts) + val result = allocate (nth offsets m) + fun filterSeq (i, j) c = + if i >= j then () + else if g i then (upd result c (f i); filterSeq (i+1, j) (c+1)) + else filterSeq (i+1, j) c + in + parfor 1 (0, m) (fn i => + let val start = lo + i*k + in filterSeq (start, Int.min (start+k, hi)) (nth offsets i) + end); + result + end + + fun tabFilter grain (lo, hi) (f : int -> 'a option) = + let + val n = hi - lo + val k = grain + val m = 1 + (n-1) div k (* number of blocks *) + val tmp = allocate n + + fun filterSeq (i,j,k) = + if (i >= j) then k + else case f i of + NONE => filterSeq(i+1, j, k) + | SOME v => (A.update(tmp, k, v); filterSeq(i+1, j, k+1)) + + val counts = tabulate 1 (0, m) (fn i => + let val last = filterSeq (lo + i*k, lo + Int.min((i+1)*k, n), i*k) + in last - i*k + end) + + val outOff = scan grain op+ 0 (0, m) (fn i => A.sub (counts, i)) + val outSize = A.sub (outOff, m) + + val result = allocate outSize + in + (* Choosing grain = n/outSize assumes that the blocks are all + * approximately the same amount full. We could do something more + * complex here, e.g. binary search to recursively split up the + * range into small pieces of all the same size. *) + parfor (n div (Int.max (outSize, 1))) (0, m) (fn i => + let + val soff = i * k + val doff = A.sub (outOff, i) + val size = A.sub (outOff, i+1) - doff + in + Util.for (0, size) (fn j => + A.update (result, doff+j, A.sub (tmp, soff+j))) + end); + result + end + +end diff --git a/tests/mpllib/SeqifiedMerge.sml b/tests/mpllib/SeqifiedMerge.sml new file mode 100644 index 000000000..eb3c3bf3d --- /dev/null +++ b/tests/mpllib/SeqifiedMerge.sml @@ -0,0 +1,59 @@ +structure SeqifiedMerge: +sig + val merge: ('a * 'a -> order) -> 'a Seq.t * 'a Seq.t -> 'a Seq.t +end = +struct + + val serialGrain = CommandLineArgs.parseInt "MPLLib_Merge_serialGrain" 4000 + + val unsafe_at_leaves = CommandLineArgs.parseFlag + "MPLLib_SeqifiedMerge_unsafe_at_leaves" + + fun merge_loop cmp (s1, s2) out = + if Seq.length s1 = 0 then + Seqifier.put (out, s2) + else if Seqifier.length out <= serialGrain then + if unsafe_at_leaves then + (* this is semantically safe (it does not violate any of the internal + * invariants of the Seqifier libary), but of course it appears to be + * syntactically unsafe from the perspective of the library interface. + * We are careful here to make sure we don't do anything really bad. + *) + ( Merge.writeMergeSerial cmp (s1, s2) + (Seqifier.unsafe_view_contents out) + ; Seqifier.unsafe_mark_put out + ) + else + (* this is completely safe, at a small performance cost, due to the + * need to create an intermediate sequence for the result of the + * `mergeSerial`. + *) + Seqifier.put (out, Merge.mergeSerial cmp (s1, s2)) + else + let + val n1 = Seq.length s1 + val n2 = Seq.length s2 + val mid1 = n1 div 2 + val pivot = Seq.nth s1 mid1 + val mid2 = BinarySearch.search cmp s2 pivot + + val (outl, out_tail) = Seqifier.split_at (out, mid1 + mid2) + val (outm, outr) = Seqifier.split_at (out_tail, 1) + val outm = Seqifier.put (outm, Seq.subseq s1 (mid1, 1)) + val l1 = Seq.take s1 mid1 + val r1 = Seq.drop s1 (mid1 + 1) + val l2 = Seq.take s2 mid2 + val r2 = Seq.drop s2 mid2 + val (outl, outr) = + ForkJoin.par (fn _ => merge_loop cmp (l1, l2) outl, fn _ => + merge_loop cmp (r1, r2) outr) + in + Seqifier.append (outl, Seqifier.append (outm, outr)) + end + + fun merge cmp (s1, s2) = + let val out = Seqifier.init_expect_length (Seq.length s1 + Seq.length s2) + in Seqifier.finalize (merge_loop cmp (s1, s2) out) + end + +end diff --git a/tests/mpllib/Seqifier.sml b/tests/mpllib/Seqifier.sml new file mode 100644 index 000000000..622f0d01f --- /dev/null +++ b/tests/mpllib/Seqifier.sml @@ -0,0 +1,206 @@ +(* Implements a parallel "sequence builder" data structure: + * type 'a seqifier + * type 'a t = 'a seqifier + * + * These can be used to write purely functional algorithms that directly write + * to an underlying (mutable) array, but the mutable array is hidden behind + * the interface and not exposed to the programmer. + * + * Example usage is: + * val sb = init_expect_length n (* O(1) *) + * val (sb1, sb2) = split_at (sb, i) (* O(1) *) + * val (sb1', sb2') = + * ForkJoin.par + * (fn () => put (sb1, X), (* O(|X|) work, O(log|X|) span *) + * fn () => put (sb2, Y)) (* O(|Y|) work, O(log|Y|) span *) + * ... + * val sb' = append (sb1', sb2') (* O(1) *) + * val result = finalize sb' (* O(1) *) + * + * The value semantics of these functions can be described in terms of a + * purely functional sequence with elements of type 'a option. Initially, + * every element is NONE. Calling `put (...)` returns a seqifier that is + * full of SOME(x) elements. Calling `finalize` checks that there are no + * NONEs, and returns a sequence of just the elements themselves. + * + * To achieve good cost bounds, seqifiers can only be "used" at most once. + * "Using" a seqifier means passing it as argument to one of the following + * functions: + * split_at + * put + * append + * finalize + * + * (Note that the function `length: 'a seqifier -> int` is read-only and does + * not constitute a "use"; this function is safe to call in any context.) + * + * Every time a seqifier is used, it is immediately invalidated. Any call to + * one of the functions above that receives an invalid seqifier as input + * will raise the exception UsedTwice. + * + * When appending two seqifiers, it is essential that they "came from" the + * same original seqifier and are physically adjacent to each other. Calling + * append will raise NonAdjacent otherwise. + * + * For example, this is okay: + * val (l, r) = split_at (x, i) + * val (l1, l2) = split_at (l, j) + * ... + * val foo = append (l1, append (l2, r)) + * + * But this would raise NonAdjacent: + * val (l, r) = split_at (x, i) + * val (l1, l2) = split_at (l, j) + * ... + * val foo = append (l1, r) + * + * When finalizing a seqifier, you may get the exception MaybeMissingPut. This + * occurs if one of the components of the seqifier was never covered by the + * result of a `put`. + * + * For example, this would raise MaybeMissingPut, because we never called + * `put` on the segment `r`. + * val x = init_expect_length n + * val (l, r) = split_at (x, i) + * val l' = put (x, ...) + * val result = finalize (append (l', r)) + * + * The MaybeMissingPut issue is checked conservatively by keeping track, for + * every seqifier, whether or not that seqifier has been fully put. This is set + * to `true` on the output of a `put`, and at each `append` we check if both + * sides are marked as fully put. Calling `split_at` just copies the boolean + * to both of the results. This approach is conservative because we don't + * individually track every index. (Specifically, at `append`, if at least + * one index has not yet been put, we mark the whole result as not put.) + *) +structure Seqifier: +sig + exception UsedTwice + exception NonAdjacent + exception MaybeMissingPut + + type 'a seqifier + type 'a t = 'a seqifier + + val length: 'a t -> int + + val init_expect_length: int -> 'a t + val split_at: 'a t * int -> 'a t * 'a t + val append: 'a t * 'a t -> 'a t + val put: 'a t * 'a Seq.t -> 'a t + val finalize: 'a t -> 'a Seq.t + + (* ======================================================================== + * UNSAFE FUNCTIONS + * These give direct access to the internals of the seqifier. + * Don't use unless you know what you are doing! + *) + + val unsafe_mark_put: 'a t -> 'a t + val unsafe_view_contents: 'a t -> 'a ArraySlice.slice + +end = +struct + + + datatype 'a t = + T of + { output: 'a array + , offset: int + , len: int + , fully_put: bool + , valid: Word8.word ref + } + + type 'a seqifier = 'a t + + + exception UsedTwice + exception NonAdjacent + exception MaybeMissingPut + + + fun unpack_and_mark_used (T {output, offset, len, fully_put, valid}) = + if !valid = 0w0 orelse Concurrency.cas valid (0w1, 0w0) = 0w0 then + raise UsedTwice + else + (output, offset, len, fully_put) + + + fun pack (output, offset, len, fully_put) = + T { output = output + , offset = offset + , len = len + , fully_put = fully_put + , valid = ref 0w1 + } + + + fun init_expect_length n = + pack (ForkJoin.alloc n, 0, n, false) + + + fun length (T {len, ...}) = len + + + fun split_at (t, i) = + let + val (output, offset, len, fp) = unpack_and_mark_used t + in + if i < 0 orelse i > len then + raise Subscript + else + (pack (output, offset, i, fp), pack (output, offset + i, len - i, fp)) + end + + + fun append (l, r) = + let + val (output1, offset1, len1, fp1) = unpack_and_mark_used l + val (output2, offset2, len2, fp2) = unpack_and_mark_used r + in + if not (MLton.eq (output1, output2) andalso offset1 + len1 = offset2) then + raise NonAdjacent + else + pack (output1, offset1, len1 + len2, fp1 andalso fp2) + end + + + fun put (t, s) = + let + val (output, offset, len, _) = unpack_and_mark_used t + in + if Seq.length s <> len then + raise Size + else + ( Seq.foreach s (fn (i, x) => Array.update (output, offset + i, x)) + ; pack (output, offset, len, true) + ) + end + + + fun finalize t = + let + val (output, offset, len, fp) = unpack_and_mark_used t + in + if not fp then raise MaybeMissingPut + else ArraySlice.slice (output, offset, SOME len) + end + + + (* ======================================================================= + * UNSAFE FUNCTIONS BELOW + *) + + fun unsafe_mark_put (T {output, offset, len, valid, ...}) = + T { output = output + , offset = offset + , len = len + , valid = valid + , fully_put = true + } + + fun unsafe_view_contents (T {output, offset, len, ...}) = + ArraySlice.slice (output, offset, SOME len) + +end diff --git a/tests/mpllib/Shuffle.sml b/tests/mpllib/Shuffle.sml new file mode 100644 index 000000000..f6a4bd227 --- /dev/null +++ b/tests/mpllib/Shuffle.sml @@ -0,0 +1,64 @@ +structure Shuffle :> +sig + type 'a seq = 'a ArraySlice.slice + val shuffle: 'a seq -> int -> 'a seq +end = +struct + open Seq + type 'a seq = 'a ArraySlice.slice + + (* inplace Knuth shuffle [l, r) *) + fun inplace_seq_shuffle s l r seed = + let + fun item i = AS.sub (s, i) + fun set (i, v) = AS.update (s, i, v) + (* get a random idx in [l, i] *) + fun rand_idx i = Int.mod (Util.hash (seed + i), i - l + 1) + l + fun swap (i,j) = + let + val tmp = item i + in + set(i, item j); set(j, tmp) + end + fun shuffle_helper li = + if r - li < 2 then () + else (swap (li, rand_idx li); shuffle_helper (li + 1)) + in + shuffle_helper l + end + + fun bucket_shuffle s seed = + let + fun log2_up n = Real.ceil (Math.log10 (Real.fromInt n) / (Math.log10 2.0)) + fun bit_and (n, mask) = Word.toInt (Word.andb (Word.fromInt n, mask)) + + val n = length s + val l = log2_up n + val bits = if n < Real.floor (Math.pow (2.0, 27.0)) then Int.div ((l - 7), 2) + else l - 17 + val num_buckets = Real.floor (Math.pow (2.0, Real.fromInt bits)) + val mask = Word.fromInt (num_buckets - 1) + fun rand_pos i = bit_and (Util.hash (seed + i), mask) + (* size of bucket_offsets = num_buckets + 1 *) + val (s', bucket_offsets) = CountingSort.sort s rand_pos num_buckets + fun bucket_shuffle i = inplace_seq_shuffle s' (nth bucket_offsets i) (nth bucket_offsets (i + 1)) seed + val _ = ForkJoin.parfor 1 (0, num_buckets) bucket_shuffle + in + s' + end + + fun shuffle s seed = + let + val n = length s + in + if n < 1000 then + let + val s' = (Seq.tabulate (Seq.nth s) n) + val _ = inplace_seq_shuffle s' 0 n seed + in + s' + end + else + bucket_shuffle s seed + end +end diff --git a/tests/mpllib/Signal.sml b/tests/mpllib/Signal.sml new file mode 100644 index 000000000..0a2fee511 --- /dev/null +++ b/tests/mpllib/Signal.sml @@ -0,0 +1,290 @@ +structure Signal: +sig + type sound = NewWaveIO.sound + val delay: real -> real -> sound -> sound + val allPass: real -> real -> sound -> sound + val reverb: sound -> sound +end = +struct + + type sound = NewWaveIO.sound + + structure A = Array + structure AS = ArraySlice + +(* + structure A = + struct + open A + val update = Unsafe.Array.update + val sub = Unsafe.Array.sub + end + + structure AS = + struct + open AS + fun update (s, i, x) = + let val (a, start, _) = base s + in A.update (a, start+i, x) + end + fun sub (s, i) = + let val (a, start, _) = base s + in A.sub (a, start+i) + end + end +*) + + fun delaySequential D a data = + let + val n = Seq.length data + val output = ForkJoin.alloc n + in + Util.for (0, n) (fn i => + if i < D then + A.update (output, i, Seq.nth data i) + else + A.update (output, i, Seq.nth data i + a * A.sub (output, i - D)) + ); + + AS.full output + end + + fun pow (a: real) n = + if n <= 1 then + a + else if n mod 2 = 0 then + pow (a*a) (n div 2) + else + a * pow (a*a) (n div 2) + + (* Granularity parameters *) + val blockWidth = CommandLineArgs.parseInt "comb-width" 600 + val blockHeight = CommandLineArgs.parseInt "comb-height" 50 + val combGran = CommandLineArgs.parseInt "comb-threshold" 10000 + (*val _ = print ("comb-width " ^ Int.toString blockWidth ^ "\n") + val _ = print ("comb-height " ^ Int.toString blockHeight ^ "\n") + val _ = print ("comb-threshold " ^ Int.toString combGran ^ "\n")*) + + (* Imagine laying out the data as a matrix, where sample s[i*D + j] is + * at row i, column j. + *) + fun delay' D alpha data = + if Seq.length data <= combGran then + delaySequential D alpha data + else + let + val n = Seq.length data + (* val _ = print ("delay' " ^ Int.toString D ^ " " ^ Int.toString n ^ "\n") *) + val output = ForkJoin.alloc n + + val numCols = D + val numRows = Util.ceilDiv n D + + fun getOutput i j = + A.sub (output, i*numCols + j) + + fun setOutput i j x = + let val idx = i*numCols + j + in if idx < n then A.update (output, idx, x) else () + end + + fun input i j = + let val idx = i*numCols + j + in if idx >= n then 0.0 else AS.sub (data, idx) + end + + val powAlpha = pow alpha blockHeight + + val numColumnStrips = Util.ceilDiv numCols blockWidth + val numRowStrips = Util.ceilDiv numRows blockHeight + + fun doColumnStrip c = + let + val jlo = blockWidth * c + val jhi = Int.min (numCols, jlo + blockWidth) + val width = jhi - jlo + val summaries = + AS.full (ForkJoin.alloc (width * numRowStrips)) + + fun doBlock b = + let + val ilo = blockHeight * b + val ihi = Int.min (numRows, ilo + blockHeight) + val ss = Seq.subseq summaries (width * b, width) + in + Util.for (0, width) (fn j => AS.update (ss, j, input ilo (jlo+j))); + + Util.for (ilo+1, ihi) (fn i => + Util.for (0, width) (fn j => + AS.update (ss, j, input i (jlo+j) + alpha * AS.sub (ss, j)) + ) + ) + end + + val _ = ForkJoin.parfor 1 (0, numRowStrips) doBlock + val summaries' = delay' width powAlpha summaries + + fun fillOutputBlock b = + let + val ilo = blockHeight * b + val ihi = Int.min (numRows, ilo + blockHeight) + in + if b = 0 then + Util.for (jlo, jhi) (fn j => setOutput 0 j (input 0 j)) + else + let + val ss = Seq.subseq summaries' (width * (b-1), width) + in + Util.for (0, width) (fn j => + setOutput ilo (jlo+j) (input ilo (jlo+j) + alpha * AS.sub (ss, j))) + end; + + Util.for (ilo+1, ihi) (fn i => + Util.for (jlo, jhi) (fn j => + setOutput i j (input i j + alpha * getOutput (i-1) j) + ) + ) + end + in + ForkJoin.parfor 1 (0, numRowStrips) fillOutputBlock + end + in + ForkJoin.parfor 1 (0, numColumnStrips) doColumnStrip; + + AS.full output + end + + fun delay ds alpha ({sr, data}: sound) = + let + val D = Real.round (ds * Real.fromInt sr) + in + {sr = sr, data = delay' D alpha data} + end + + fun allPass' D a data = + let + val combed = delay' D a data + + fun output j = + let + val k = j - D + in + (1.0 - a*a) * (if k < 0 then 0.0 else Seq.nth combed k) + - (a * Seq.nth data j) + end + in + Seq.tabulate output (Seq.length data) + end + + fun allPass ds a (snd as {sr, data}: sound) = + let + (* convert to samples *) + val D = Real.round (ds * Real.fromInt sr) + in + { sr = sr + , data = allPass' D a data + } + end + + val par = ForkJoin.par + + fun par4 (a, b, c, d) = + let + val ((ar, br), (cr, dr)) = + par (fn _ => par (a, b), fn _ => par (c, d)) + in + (ar, br, cr, dr) + end + + fun shiftBy n s i = + if i < n then + 0.0 + else if i < Seq.length s + n then + Seq.nth s (i-n) + else + 0.0 + + fun reverb ({sr, data=dry}: sound) = + let + val N = Seq.length dry + + (* Originally, I tuned the comb and allPass parameters + * based on numbers of samples at 44.1 kHz, which I chose + * to be relatively prime to one another. But now, to + * handle any sample rate, we need to convert these numbers + * of samples. Does it really matter if the sample delays are + * relatively prime? I'm not sure. For sample rates other + * than 44.1 kHz, they almost certainly won't be now. *) + + val srr = Real.fromInt sr + fun secondsToSamples sec = Real.round (sec * srr) + fun secondsAt441 samples = Real.fromInt samples / 44100.0 + fun adjust x = + if sr = 44100 then x else secondsToSamples (secondsAt441 x) + + val D1 = adjust 1931 + val D2 = adjust 2213 + val D3 = adjust 1747 + val D4 = adjust 1559 + + val DA1 = adjust 167 + val DA2 = adjust 191 + + val DE1 = adjust 1013 + val DE2 = adjust 1102 + val DE3 = adjust 1300 + + val DF = adjust 1500 + + (* ========================================== + * Fused reflections near 50ms + * (at 44.1kHz, 50ms is about 2200 samples) + * + * The basic design is taken from + * The Computer Music Tutorial (1996), page 481 + * Author: Curtis Roads + * + * The basic design is 4 comb filters (in parallel) + * which are then fed into two allpass filters, in series. + *) + + val (c1, c2, c3, c4) = + par4 (fn _ => delay' D1 0.7 dry, + fn _ => delay' D2 0.7 dry, + fn _ => delay' D3 0.7 dry, + fn _ => delay' D4 0.7 dry) + + fun combs i = + (Seq.nth c1 i + + Seq.nth c2 i + + Seq.nth c3 i + + Seq.nth c4 i) + + val fused = Seq.tabulate combs N + val fused = allPass' DA1 0.6 fused + val fused = allPass' DA2 0.6 fused + + (* ========================================== + * wet signal = dry + early + fused + * + * early reflections are single echos of + * the dry sound that occur after around + * 25ms delay + * + * the fused reflections start emerging after + * approximately 35ms + *) + + val wet = Seq.tabulate (fn i => + shiftBy 0 dry i + + 0.6 * (shiftBy DE1 dry i) + + 0.5 * (shiftBy DE2 dry i) + + 0.4 * (shiftBy DE3 dry i) + + 0.75 * (shiftBy DF fused i)) + (N + DF) + + in + NewWaveIO.compress 2.0 {sr=sr, data=wet} + end + +end diff --git a/tests/mpllib/StableMerge.sml b/tests/mpllib/StableMerge.sml new file mode 100644 index 000000000..d64933159 --- /dev/null +++ b/tests/mpllib/StableMerge.sml @@ -0,0 +1,100 @@ +structure StableMerge: +sig + type 'a seq = 'a ArraySlice.slice + + val writeMergeSerial: ('a * 'a -> order) (* compare *) + -> 'a seq * 'a seq (* (sorted) sequences to merge *) + -> 'a seq (* output *) + -> unit + + val writeMerge: ('a * 'a -> order) (* compare *) + -> 'a seq * 'a seq (* (sorted) sequences to merge *) + -> 'a seq (* output *) + -> unit + + val mergeSerial: ('a * 'a -> order) -> 'a seq * 'a seq -> 'a seq + val merge: ('a * 'a -> order) -> 'a seq * 'a seq -> 'a seq +end = +struct + + structure AS = ArraySlice + type 'a seq = 'a AS.slice + + val for = Util.for + val parfor = ForkJoin.parfor + val par = ForkJoin.par + val allocate = ForkJoin.alloc + + val serialGrain = + CommandLineArgs.parseInt "MPLLib_StableMerge_serialGrain" 4000 + + fun sliceIdxs s i j = + AS.subslice (s, i, SOME (j - i)) + + fun writeMergeSerial cmp (s1, s2) t = + let + fun write i x = AS.update (t, i, x) + + val n1 = AS.length s1 + val n2 = AS.length s2 + + (* i1 index into s1 + * i2 index into s2 + * j index into output *) + fun loop i1 i2 j = + if i1 = n1 then + Util.foreach (sliceIdxs s2 i2 n2) (fn (i, x) => write (i + j) x) + else if i2 = n2 then + Util.foreach (sliceIdxs s1 i1 n1) (fn (i, x) => write (i + j) x) + else + let + val x1 = AS.sub (s1, i1) + val x2 = AS.sub (s2, i2) + in + (* NOTE: this is stable *) + case cmp (x1, x2) of + GREATER => (write j x2; loop i1 (i2 + 1) (j + 1)) + | _ => (write j x1; loop (i1 + 1) i2 (j + 1)) + end + in + loop 0 0 0 + end + + fun mergeSerial cmp (s1, s2) = + let val out = AS.full (allocate (AS.length s1 + AS.length s2)) + in writeMergeSerial cmp (s1, s2) out; out + end + + fun writeMerge cmp (s1, s2) t = + if AS.length t <= serialGrain then + writeMergeSerial cmp (s1, s2) t + else if AS.length s1 = 0 then + Util.foreach s2 (fn (i, x) => AS.update (t, i, x)) + else + let + val n1 = AS.length s1 + val n2 = AS.length s2 + val mid1 = n1 div 2 + val pivot = AS.sub (s1, mid1) + val mid2 = BinarySearch.countLess cmp s2 pivot + + val l1 = sliceIdxs s1 0 mid1 + val r1 = sliceIdxs s1 (mid1 + 1) n1 + val l2 = sliceIdxs s2 0 mid2 + val r2 = sliceIdxs s2 mid2 n2 + + val _ = AS.update (t, mid1 + mid2, pivot) + val tl = sliceIdxs t 0 (mid1 + mid2) + val tr = sliceIdxs t (mid1 + mid2 + 1) (AS.length t) + in + par (fn _ => writeMerge cmp (l1, l2) tl, fn _ => + writeMerge cmp (r1, r2) tr); + () + end + + fun merge cmp (s1, s2) = + let val out = AS.full (allocate (AS.length s1 + AS.length s2)) + in writeMerge cmp (s1, s2) out; out + end + +end diff --git a/tests/mpllib/StableMergeLowSpan.sml b/tests/mpllib/StableMergeLowSpan.sml new file mode 100644 index 000000000..a24c2c0a4 --- /dev/null +++ b/tests/mpllib/StableMergeLowSpan.sml @@ -0,0 +1,69 @@ +structure StableMergeLowSpan: +sig + type 'a seq = 'a ArraySlice.slice + + val writeMerge: ('a * 'a -> order) (* compare *) + -> 'a seq * 'a seq (* (sorted) sequences to merge *) + -> 'a seq (* output *) + -> unit + + val merge: ('a * 'a -> order) -> 'a seq * 'a seq -> 'a seq +end = +struct + + structure AS = ArraySlice + type 'a seq = 'a AS.slice + fun slice_idxs s (i, j) = + AS.subslice (s, i, SOME (j - i)) + + + (* DoubleBinarySearch guarantees that it takes the _minimum_ number of + * elements from the first argument. For stability, we want to take the + * _maximum_ number of elements from s1; this is equivalent to taking the + * minimum from s2. So, we can just swap the order of the arguments we + * give to the search. + *) + fun split_count_take_max_left cmp (s1, s2) k = + let val (i2, i1) = DoubleBinarySearch.split_count_slice cmp (s2, s1) k + in (i1, i2) + end + + + val blockSizeFactor = + CommandLineArgs.parseReal "MPLLib_StableMergeLowSpan_blockSizeFactor" 1000.0 + + + fun log2 x = + Real64.Math.log10 (Real64.fromInt x) / Real64.Math.log10 2.0 + + + fun writeMerge cmp (s1, s2) output = + let + val n = AS.length s1 + AS.length s2 + val logn = if n <= 2 then 1.0 else log2 n + val blockSize = Real64.ceil (blockSizeFactor * logn) + val numBlocks = Util.ceilDiv n blockSize + in + ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val start = blockSize * b + val stop = Int.min (n, start + blockSize) + + val (i1, i2) = split_count_take_max_left cmp (s1, s2) start + val (j1, j2) = split_count_take_max_left cmp (s1, s2) stop + + val piece1 = slice_idxs s1 (i1, j1) + val piece2 = slice_idxs s2 (i2, j2) + val piece_output = slice_idxs output (start, stop) + in + StableMerge.writeMerge cmp (piece1, piece2) piece_output + end) + end + + + fun merge cmp (s1, s2) = + let val out = AS.full (ForkJoin.alloc (AS.length s1 + AS.length s2)) + in writeMerge cmp (s1, s2) out; out + end + +end diff --git a/tests/mpllib/StableSort.sml b/tests/mpllib/StableSort.sml new file mode 100644 index 000000000..a9429e788 --- /dev/null +++ b/tests/mpllib/StableSort.sml @@ -0,0 +1,81 @@ +structure StableSort: +sig + type 'a seq = 'a ArraySlice.slice + val sortInPlace: ('a * 'a -> order) -> 'a seq -> unit + val sort: ('a * 'a -> order) -> 'a seq -> 'a seq +end = +struct + + type 'a seq = 'a ArraySlice.slice + + structure AS = ArraySlice + + fun take s n = AS.subslice (s, 0, SOME n) + fun drop s n = AS.subslice (s, n, NONE) + + val par = ForkJoin.par + val allocate = ForkJoin.alloc + + (* in-place sort s, using t as a temporary array if needed *) + fun sortInPlace' cmp s t = + if AS.length s <= 1 then + () + else let + val half = AS.length s div 2 + val (sl, sr) = (take s half, drop s half) + val (tl, tr) = (take t half, drop t half) + in + (* recursively sort, writing result into t *) + if AS.length s <= 1024 then + (writeSort cmp sl tl; writeSort cmp sr tr) + else + ( par (fn _ => writeSort cmp sl tl, fn _ => writeSort cmp sr tr) + ; () + ); + + (* merge back from t into s *) + StableMerge.writeMerge cmp (tl, tr) s; + + () + end + + (* destructively sort s, writing the result in t *) + and writeSort cmp s t = + if AS.length s <= 1 then + Util.foreach s (fn (i, x) => AS.update (t, i, x)) + else let + val half = AS.length s div 2 + val (sl, sr) = (take s half, drop s half) + val (tl, tr) = (take t half, drop t half) + in + (* recursively in-place sort sl and sr *) + if AS.length s <= 1024 then + (sortInPlace' cmp sl tl; sortInPlace' cmp sr tr) + else + ( par (fn _ => sortInPlace' cmp sl tl, fn _ => sortInPlace' cmp sr tr) + ; () + ); + + (* merge into t *) + StableMerge.writeMerge cmp (sl, sr) t; + + () + end + + fun sortInPlace cmp s = + let + val t = AS.full (allocate (AS.length s)) + in + sortInPlace' cmp s t + end + + fun sort cmp s = + let + val result = AS.full (allocate (AS.length s)) + in + Util.foreach s (fn (i, x) => AS.update (result, i, x)); + sortInPlace cmp result; + result + end + +end diff --git a/tests/mpllib/TFlatten.sml b/tests/mpllib/TFlatten.sml new file mode 100644 index 000000000..a5f552bc4 --- /dev/null +++ b/tests/mpllib/TFlatten.sml @@ -0,0 +1,51 @@ +structure TFlatten: +sig + type 'a tree + type 'a t = 'a tree + + datatype 'a view = Leaf of 'a Seq.t | Node of 'a t * 'a t + + val size: 'a t -> int + val leaf: 'a Seq.t -> 'a t + val node: 'a t * 'a t -> 'a t + val view: 'a t -> 'a view + val flatten: 'a t -> 'a Seq.t +end = +struct + + datatype 'a tree = Leaf_ of 'a Seq.t | Node_ of int * 'a tree * 'a tree + type 'a t = 'a tree + + datatype 'a view = Leaf of 'a Seq.t | Node of 'a t * 'a t + + fun size (Leaf_ s) = Seq.length s + | size (Node_ (n, _, _)) = n + + fun leaf s = Leaf_ s + + fun node (l, r) = + Node_ (size l + size r, l, r) + + fun view (Leaf_ s) = Leaf s + | view (Node_ (_, l, r)) = Node (l, r) + + + fun flatten t = + let + val output = ForkJoin.alloc (size t) + fun traverse (c, offset) = + case c of + Leaf_ s => + ForkJoin.parfor 100 (0, Seq.length s) (fn i => + Array.update (output, offset + i, Seq.nth s i)) + | Node_ (_, l, r) => + ( ForkJoin.par (fn () => traverse (l, offset), fn () => + traverse (r, offset + size l)) + ; () + ) + in + traverse (t, 0); + ArraySlice.full output + end + +end diff --git a/tests/mpllib/TabFilterTree.sml b/tests/mpllib/TabFilterTree.sml new file mode 100644 index 000000000..0f1d98c15 --- /dev/null +++ b/tests/mpllib/TabFilterTree.sml @@ -0,0 +1,76 @@ +structure TabFilterTree = +struct + + structure A = Array + structure AS = ArraySlice + + structure ChunkList = + struct + val chunkSize = 256 + type 'a t = 'a array list * 'a array * int + fun new () = ([], ForkJoin.alloc chunkSize, 0) + + fun push ((elems, chunk, pos): 'a t) (x: 'a) = + if pos >= chunkSize then + push (chunk :: elems, ForkJoin.alloc chunkSize, 0) x + else + ( A.update (chunk, pos, x); (elems, chunk, pos+1) ) + + fun finish ((elems, chunk, pos): 'a t) = + (List.rev elems, chunk, pos) + + fun foreach offset (elems, lastChunk, lastLen) f = + case elems of + [] => AS.appi (fn (i, x) => f (offset+i, x)) (AS.slice (lastChunk, 0, SOME lastLen)) + | (chunk' :: elems') => + ( A.appi (fn (i, x) => f (offset+i, x)) chunk' + ; foreach (offset+chunkSize) (elems', lastChunk, lastLen) f + ) + end + + datatype 'a tree = + Leaf of int * 'a ChunkList.t + | Node of int * 'a tree * 'a tree + + fun size (Leaf (n, _)) = n + | size (Node (n, _, _)) = n + + fun tabFilter grain (lo, hi) (f: int -> 'a option) = + let + fun filterSeq (count, elems) (i, j) = + if i >= j then + (count, ChunkList.finish elems) + else + case f i of + NONE => filterSeq (count, elems) (i, j) + | SOME x => filterSeq (count+1, ChunkList.push elems x) (i+1, j) + + fun t i j = + if j-i <= grain then + Leaf (filterSeq (0, ChunkList.new ()) (i, j)) + else + let + val mid = i + (j-i) div 2 + val (l, r) = ForkJoin.par (fn _ => t i mid, fn _ => t mid j) + in + Node (size l + size r, l, r) + end + in + t lo hi + end + + fun foreach (t: 'a tree) (f: (int * 'a) -> unit) = + let + fun doit offset t = + case t of + Leaf (_, elems) => ChunkList.foreach offset elems f + | Node (_, l, r) => + ( ForkJoin.par (fn _ => doit offset l, + fn _ => doit (offset + size l) r) + ; () + ) + in + doit 0 t + end + +end diff --git a/tests/mpllib/Tokenize.sml b/tests/mpllib/Tokenize.sml new file mode 100644 index 000000000..6f35af15d --- /dev/null +++ b/tests/mpllib/Tokenize.sml @@ -0,0 +1,53 @@ +structure Tokenize: +sig + val tokenRanges: (char -> bool) -> char Seq.t -> int * (int -> (int * int)) + + val tokensSeq: (char -> bool) -> char Seq.t -> (char Seq.t) Seq.t + + val tokens: (char -> bool) -> char Seq.t -> string Seq.t +end = +struct + + fun tokenRanges f s = + let + val n = Seq.length s + fun check i = + if (i = n) then not (f(Seq.nth s (n-1))) + else if (i = 0) then not (f(Seq.nth s 0)) + else let val i1 = f (Seq.nth s i) + val i2 = f (Seq.nth s (i-1)) + in (i1 andalso not i2) orelse (i2 andalso not i1) end + val ids = ArraySlice.full + (SeqBasis.filter 10000 (0, n+1) (fn i => i) check) + val count = (Seq.length ids) div 2 + in + (count, fn i => (Seq.nth ids (2*i), Seq.nth ids (2*i+1))) + end + + fun tokensSeq f s = + let + val (n, g) = tokenRanges f s + fun token i = + let + val (lo, hi) = g i + in + Seq.subseq s (lo, hi-lo) + end + in + Seq.tabulate token n + end + + fun tokens f s = + let + val (n, g) = tokenRanges f s + fun token i = + let + val (lo, hi) = g i + val chars = Seq.subseq s (lo, hi-lo) + in + CharVector.tabulate (Seq.length chars, Seq.nth chars) + end + in + ArraySlice.full (SeqBasis.tabulate 1024 (0, n) token) + end +end diff --git a/tests/mpllib/Topology2D.sml b/tests/mpllib/Topology2D.sml new file mode 100644 index 000000000..98a45ac47 --- /dev/null +++ b/tests/mpllib/Topology2D.sml @@ -0,0 +1,1088 @@ +structure Topology2D: +sig + type vertex = int + type vertex_data = Geometry2D.point + + type triangle = int + datatype triangle_data = + Tri of + { vertices: vertex * vertex * vertex + , neighbors: triangle * triangle * triangle + } + + type mesh + val parseFile: string -> mesh + val numVertices: mesh -> int + val numTriangles: mesh -> int + val toString: mesh -> string + + val initialMeshWithBoundaryCircle + : {numVertices: int, numBoundaryVertices: int} + -> {center: Geometry2D.point, radius: real} + -> mesh + + val vdata: mesh -> vertex -> vertex_data + val tdata: mesh -> triangle -> triangle_data + val verticesOfTriangle: mesh -> triangle -> vertex * vertex * vertex + val neighborsOfTriangle: mesh -> triangle -> triangle * triangle * triangle + val triangleOfVertex: mesh -> vertex -> triangle + val getPoints: mesh -> Geometry2D.point Seq.t + (* val neighbor: triangle_data -> int -> triangle option *) + (* val locate: triangle_data -> triangle -> int option *) + + type simplex + + val find: mesh -> vertex -> simplex -> simplex + val findPoint: mesh -> Geometry2D.point -> simplex -> simplex + + val across: mesh -> simplex -> simplex option + val rotateClockwise: simplex -> simplex + val outside: mesh -> simplex -> vertex -> bool + val pointOutside: mesh -> simplex -> Geometry2D.point -> bool + val inCircle: mesh -> simplex -> vertex -> bool + val pointInCircle: mesh -> simplex -> Geometry2D.point -> bool + val firstVertex: mesh -> simplex -> vertex + + val split: mesh -> triangle -> Geometry2D.point -> mesh + val flip: mesh -> simplex -> mesh + + (** A cavity is a center triangle and a set of nearby connected simplices. + * The order of the nearby simplices is important: these must emanate + * from the center triangle. + *) + type cavity = triangle * (simplex list) + + val findCavityAndPerimeter: mesh + -> simplex (** where to start search *) + -> Geometry2D.point (** center of the cavity *) + -> cavity * (vertex list) + + val loopPerimeter: mesh + -> triangle (* triangle containing center point *) + -> Geometry2D.point (* center of the cavity *) + -> 'a + -> ('a * vertex -> 'a) + -> 'a + + val findCavity: mesh + -> triangle (* triangle containing center point *) + -> Geometry2D.point (* center point of the cavity *) + -> cavity + + val ripAndTentCavity: mesh + -> triangle (* center triangle *) + -> (vertex * Geometry2D.point) (* center of cavity and vertex id to use *) + -> triangle * triangle (* two new triangles to use *) + -> unit + + (** For each (c, p), replace cavity c with a tent using p as the center + * point. The center triangle of of the cavity must contain p. + *) + val ripAndTent: (cavity * Geometry2D.point) Seq.t -> mesh -> mesh + val ripAndTentOne: cavity * Geometry2D.point -> mesh -> mesh + + + (** The following are for imperative algorithms on meshes. *) + + val new: {numVertices: int, numTriangles: int} -> mesh + + val doSplit: mesh + -> triangle (* triangle to split *) + -> vertex * Geometry2D.point (* point inside triangle, and vertex identifier to use *) + -> triangle * triangle (* two new triangle identifiers to create *) + -> unit + + val doFlip: mesh -> simplex -> unit + + val copyData: {src: mesh, dst: mesh} -> unit + val copy: mesh -> mesh + +end = +struct + + structure AS = ArraySlice + structure G = Geometry2D + + fun upd s i x = AS.update (s, i, x) + fun nth s i = AS.sub (s, i) + + (** vertex and triangle identifiers are indices into a mesh *) + type vertex = int + type triangle = int + + val INVALID_ID = ~1 + + type vertex_data = G.point + + (** Triangles with vertices (u,v,w) and neighbors (a,b,c) must be in + * counter-clockwise order. + * + * u + * | \ --> a + * b <-- | w + * | / --> c + * v + * + * This is equivalent to any rotation, e.g. [(v,w,u),(b,c,a)]. But CCW + * order must be preserved. + *) + datatype triangle_data = + Tri of + { vertices: vertex * vertex * vertex + , neighbors: triangle * triangle * triangle + } + + + val dummyPt = (0.0, 0.0) + val dummyTriple = (INVALID_ID, INVALID_ID, INVALID_ID) + val dummyTri = + Tri {vertices = dummyTriple, neighbors = dummyTriple} + + + datatype mesh = + Mesh of + { vdata: vertex_data Seq.t + , verticesOfTriangle: (vertex * vertex * vertex) Seq.t + , neighborsOfTriangle: (triangle * triangle * triangle) Seq.t + , triangleOfVertex: triangle Seq.t + } + + fun new {numVertices, numTriangles} = + Mesh { vdata = Seq.tabulate (fn _ => dummyPt) numVertices + , triangleOfVertex = Seq.tabulate (fn _ => INVALID_ID) numVertices + , verticesOfTriangle = Seq.tabulate (fn _ => dummyTriple) numTriangles + , neighborsOfTriangle = Seq.tabulate (fn _ => dummyTriple) numTriangles + } + + fun copyData {src = Mesh src, dst = Mesh dst} = + let + val len = Seq.length + val lengthsOkay = + len (#vdata src) <= len (#vdata dst) andalso + len (#triangleOfVertex src) <= len (#triangleOfVertex dst) andalso + len (#verticesOfTriangle src) <= len (#verticesOfTriangle dst) andalso + len (#neighborsOfTriangle src) <= len (#neighborsOfTriangle dst) + val _ = + if lengthsOkay then () + else raise Fail "Topology2D.copyData: dst smaller than src" + in + ForkJoin.parfor 10000 (0, len (#vdata src)) (fn i => + upd (#vdata dst) i (nth (#vdata src) i)); + + ForkJoin.parfor 10000 (0, len (#triangleOfVertex src)) (fn i => + upd (#triangleOfVertex dst) i (nth (#triangleOfVertex src) i)); + + ForkJoin.parfor 10000 (0, len (#verticesOfTriangle src)) (fn i => + upd (#verticesOfTriangle dst) i (nth (#verticesOfTriangle src) i)); + + ForkJoin.parfor 10000 (0, len (#neighborsOfTriangle src)) (fn i => + upd (#neighborsOfTriangle dst) i (nth (#neighborsOfTriangle src) i)) + end + + fun tdata (Mesh mesh) t = + Tri { vertices = nth (#verticesOfTriangle mesh) t + , neighbors = nth (#neighborsOfTriangle mesh) t + } + + fun verticesOfTriangle (Mesh mesh) t = + nth (#verticesOfTriangle mesh) t + + fun neighborsOfTriangle (Mesh mesh) t = + nth (#neighborsOfTriangle mesh) t + + fun vdata (Mesh mesh) t = nth (#vdata mesh) t + + fun triangleOfVertex (Mesh mesh) v = nth (#triangleOfVertex mesh) v + + fun getPoints (Mesh {vdata, ...}) = vdata + + fun numVertices (Mesh {vdata, ...}) = Seq.length vdata + fun numTriangles (Mesh {verticesOfTriangle, ...}) = + Seq.length verticesOfTriangle + + fun copy mesh = + let + val n = numVertices mesh + val m = numTriangles mesh + val vdata = AS.full (ForkJoin.alloc n) + val triangleOfVertex = AS.full (ForkJoin.alloc n) + val verticesOfTriangle = AS.full (ForkJoin.alloc m) + val neighborsOfTriangle = AS.full (ForkJoin.alloc m) + + val mesh' = + Mesh { vdata = vdata + , triangleOfVertex = triangleOfVertex + , verticesOfTriangle = verticesOfTriangle + , neighborsOfTriangle = neighborsOfTriangle + } + in + copyData {src = mesh, dst = mesh'}; + mesh' + end + + + fun vertex (vertices as (a,b,c)) i = + case i of + 0 => a + | 1 => b + | _ => c + + + fun neighbor (neighbors as (a,b,c)) i = + let + val t' = + case i of + 0 => a + | 1 => b + | _ => c + in + if t' < 0 then NONE else SOME t' + end + + + fun locate (neighbors as (a,b,c)) (t: triangle) = + if a = t then SOME 0 + else if b = t then SOME 1 + else if c = t then SOME 2 + else NONE + + + fun hasEdge (vertices as (a,b,c)) (u,v) = + (u = a orelse u = b orelse u = c) + andalso + (v = a orelse v = b orelse v = c) + + + fun sortTriangleCCW mesh (Tri {vertices=(v1,v2,v3), neighbors=(t1,t2,t3)}) = + let + fun p v = vdata mesh v + val (v2, v3) = + if G.Point.counterClockwise (p v1, p v2, p v3) then + (v2, v3) + else + (v3, v2) + + fun checkHasEdge (u,v) t = + t <> INVALID_ID andalso hasEdge (verticesOfTriangle mesh t) (u,v) + + val (t1,t2,t3) = + if checkHasEdge (v1,v3) t1 then + (t1,t2,t3) + else if checkHasEdge (v1,v3) t2 then + (t2,t1,t3) + else + (t3,t1,t2) + + val (t2,t3) = + if checkHasEdge (v2,v1) t2 then + (t2,t3) + else + (t3,t2) + + in + Tri {vertices=(v1,v2,v3), neighbors=(t1,t2,t3)} + end + + + (** A simplex is an oriented triangle, which essentially just selects an + * edge of the triangle (the integer indicates which edge with the value + * 0, 1, or 2). The orientation allows us to define operations such as + * "across" which returns the simplex on the other side of the + * distinguished edge. + *) + type simplex = triangle * int + + + fun triangleOfSimplex ((t, _): simplex) = t + + + fun orientedTriangleData mesh (t, i) = + let + val Tri {vertices=(a,b,c), neighbors=(d,e,f)} = + tdata mesh t + in + case i of + 0 => Tri {vertices=(a,b,c), neighbors=(d,e,f)} + | 1 => Tri {vertices=(b,c,a), neighbors=(e,f,d)} + | _ => Tri {vertices=(c,a,b), neighbors=(f,d,e)} + end + + + fun across mesh ((t, i): simplex) : simplex option = + case neighbor (neighborsOfTriangle mesh t) i of + SOME t' => + (case locate (neighborsOfTriangle mesh t') t of + SOME i' => SOME (t', i') + | NONE => NONE) + | NONE => NONE + + + fun fastNeighbor (neighbors as (a,b,c)) i = + case i of + 0 => a + | 1 => b + | _ => c + + fun fastLocate (neighbors as (a,b,c)) (t: triangle) = + if a = t then 0 + else if b = t then 1 + else 2 + + fun fastAcross mesh ((t, i): simplex) = + let + val t' = fastNeighbor (neighborsOfTriangle mesh t) i + in + (t', fastLocate (neighborsOfTriangle mesh t') t) + end + + fun mod3 i = + if i > 2 then i-3 else i + + fun rotateClockwise ((t, i): simplex) : simplex = + (t, mod3 (i+1)) + + fun pointOutside mesh ((t, i): simplex) pt = + let + val vs = verticesOfTriangle mesh t + val p1 = vdata mesh (vertex vs (mod3 (i+2))) + val p2 = pt + val p3 = vdata mesh (vertex vs i) + in + G.Point.counterClockwise (p1, p2, p3) + end + + fun outside mesh (simp: simplex) v = + pointOutside mesh simp (vdata mesh v) + + fun pointInCircle mesh ((t, _): simplex) pt = + let + val (a,b,c) = verticesOfTriangle mesh t + val p1 = vdata mesh a + val p2 = vdata mesh b + val p3 = vdata mesh c + in + G.Point.inCircle (p1, p2, p3) pt + end + + fun inCircle mesh simp v = + pointInCircle mesh simp (vdata mesh v) + + fun firstVertex mesh ((t, i): simplex) = + vertex (verticesOfTriangle mesh t) i + + + (** ======================================================================== + * traversal and cavities + *) + + fun findPoint mesh pt current = + if pointOutside mesh current pt then + findPoint mesh pt (fastAcross mesh current) + else + let val current = rotateClockwise current in + if pointOutside mesh current pt then + findPoint mesh pt (fastAcross mesh current) + else + let val current = rotateClockwise current in + if pointOutside mesh current pt then + findPoint mesh pt (fastAcross mesh current) + else + current + end end + + + (** find: mesh -> vertex -> simplex -> simplex *) + fun find mesh v current = + findPoint mesh (vdata mesh v) current + + + type cavity = triangle * (simplex list) + + + fun loopPerimeter mesh center pt (b: 'a) (f: 'a * vertex -> 'a) = + let + fun loop b t = + if not (pointInCircle mesh t pt) then + b + else + let + val t = rotateClockwise t + val b = loopAcross b t + val b = f (b, firstVertex mesh t) + val t = rotateClockwise t + val b = loopAcross b t + in + b + end + + and loopAcross b t = + case across mesh t of + SOME t' => loop b t' + | NONE => b + + (* val center = findPoint mesh pt findStart *) + + val t = (center, 0) + + val b = f (b, firstVertex mesh t) + val b = loopAcross b t + + val t = rotateClockwise t + val b = f (b, firstVertex mesh t) + val b = loopAcross b t + + val t = rotateClockwise t + val b = f (b, firstVertex mesh t) + val b = loopAcross b t + in + b + end + + + fun findCavityAndPerimeter mesh findStart (pt: Geometry2D.point) = + let + fun loop (simps, verts) t = + if not (pointInCircle mesh t pt) then + (simps, verts) + else + let + val simps = t :: simps + val t = rotateClockwise t + val (simps, verts) = loopAcross (simps, verts) t + val verts = firstVertex mesh t :: verts + val t = rotateClockwise t + val (simps, verts) = loopAcross (simps, verts) t + in + (simps, verts) + end + + and loopAcross (simps, verts) t = + case across mesh t of + SOME t' => loop (simps, verts) t' + | NONE => (simps, verts) + + val center = findPoint mesh pt findStart + + val t = center + val (simps, verts) = ([], []) + + val verts = firstVertex mesh t :: verts + val (simps, verts) = loopAcross (simps, verts) t + + val t = rotateClockwise t + val verts = firstVertex mesh t :: verts + val (simps, verts) = loopAcross (simps, verts) t + + val t = rotateClockwise t + val verts = firstVertex mesh t :: verts + val (simps, verts) = loopAcross (simps, verts) t + + val cavity = (triangleOfSimplex center, List.rev simps) + in + (cavity, verts) + end + + + fun findCavity mesh center (pt: Geometry2D.point) = + let + fun loop simps t = + if not (pointInCircle mesh t pt) then + simps + else + let + val simps = t :: simps + val t = rotateClockwise t + val simps = loopAcross simps t + val t = rotateClockwise t + val simps = loopAcross simps t + in + simps + end + + and loopAcross simps t = + case across mesh t of + SOME t' => loop simps t' + | NONE => simps + + (* val center = findPoint mesh pt findStart *) + + val t = (center, 0) + val simps = [] + + val simps = loopAcross simps t + val t = rotateClockwise t + val simps = loopAcross simps t + val t = rotateClockwise t + val simps = loopAcross simps t + + val cavity = (center, List.rev simps) + in + cavity + end + + + (** ======================================================================== + * split triangle + *) + + exception FailedReplaceNeighbor + + fun replaceNeighbor (Mesh {neighborsOfTriangle, ...}) t (old, new) = + if t < 0 then () else + let + val (a,b,c) = nth neighborsOfTriangle t + val newNeighbors = + if old = a then (new, b, c) + else if old = b then (a, new, c) + else if old = c then (a, b, new) + else raise FailedReplaceNeighbor + in + upd neighborsOfTriangle t newNeighbors + end + + + fun updateTriangle + (Mesh {verticesOfTriangle, neighborsOfTriangle, ...}) t (Tri {vertices, neighbors}) + = + ( upd verticesOfTriangle t vertices + ; upd neighborsOfTriangle t neighbors + ) + + + (** split triangle t by putting a new vertex v at point p inside. This creates + * two new triangles, which will have ids t1 and t2. This function modifies + * the mesh by editing triangle data (t, ta0, ta1) and vertex data (v). + * + * BEFORE: AFTER: + * v1 v1 + * |\ |\\ + * | \ t1 | \ \ t1 + * | \ | \ \ + * | \ | \ t \ + * t2 | t v3 t2 |ta0 v --- v3 + * | / | / ta1 / + * | / | / / + * | / t3 | / / t3 + * |/ |// + * v2 v2 + *) + fun doSplit (mesh as Mesh {vdata, triangleOfVertex, ...}) t (v, p) (ta0, ta1) = + let + val Tri {vertices=(v1,v2,v3), neighbors=(t1,t2,t3)} = tdata mesh t + val newdata_t = + Tri {vertices=(v1,v,v3), neighbors=(t1,ta0,ta1)} + val newdata_ta0 = + Tri {vertices=(v2,v,v1), neighbors=(t2,ta1,t)} + val newdata_ta1 = + Tri {vertices=(v3,v,v2), neighbors=(t3,t,ta0)} + in + upd vdata v p; + upd triangleOfVertex v t; + if nth triangleOfVertex v2 <> t then () + else upd triangleOfVertex v2 ta0; + updateTriangle mesh t newdata_t; + updateTriangle mesh ta0 newdata_ta0; + updateTriangle mesh ta1 newdata_ta1; + replaceNeighbor mesh t2 (t,ta0); + replaceNeighbor mesh t3 (t,ta1) + end + + + fun split (Mesh {verticesOfTriangle, neighborsOfTriangle, vdata, triangleOfVertex}) (t: triangle) p = + let + val n = Seq.length vdata + val m = Seq.length neighborsOfTriangle + + (** allocate new with dummy values *) + val vdata' = Seq.append (vdata, Seq.singleton (nth vdata 0)) + val verticesOfTriangle' = Seq.append (verticesOfTriangle, Seq.fromList [dummyTriple, dummyTriple]) + val neighborsOfTriangle' = Seq.append (neighborsOfTriangle, Seq.fromList [dummyTriple, dummyTriple]) + val triangleOfVertex' = Seq.append (triangleOfVertex, Seq.singleton t) + val mesh' = + Mesh { vdata=vdata' + , verticesOfTriangle=verticesOfTriangle' + , neighborsOfTriangle=neighborsOfTriangle' + , triangleOfVertex=triangleOfVertex' + } + in + (* print ("splitting " ^ Int.toString t ^ " into: " ^ String.concatWith " " (List.map Int.toString [t,m,m+1]) ^ "\n"); *) + doSplit mesh' t (n, p) (m, m+1); + mesh' + end + + + (** ======================================================================== + * flip simplex + *) + + + (** Flip the shared edge identified by the simplex. + * + * BEFORE: AFTER: + * v3 v3 + * /|\ / \ + * t4 / | \ t3 t4 / \ t3 + * / | \ / t \ + * v4 t1 | t v2 v4 --------- v2 + * \ | / \ t1 / + * t5 \ | / t2 t5 \ / t2 + * \|/ \ / + * v1 v1 + *) + fun doFlip (mesh as Mesh {triangleOfVertex, ...}) (simp: simplex) = + let + val Tri {vertices=(v1,v2,v3), neighbors=(t1,t2,t3)} = + orientedTriangleData mesh simp + val Tri {vertices=(v3_,v4,v1_), neighbors=(t,t4,t5)} = + orientedTriangleData mesh (fastAcross mesh simp) + + (* val _ = + print ("flipping " ^ Int.toString t ^ " and " ^ Int.toString t1 ^ "\n") *) + + (** sanity check *) + (* val _ = + if v3 = v3_ andalso v1 = v1_ andalso t = triangleOfSimplex simp then () + else raise Fail "effed up flip" *) + + val newdata_t = + Tri {vertices=(v2,v3,v4), neighbors=(t1,t3,t4)} + + val newdata_t1 = + Tri {vertices=(v1,v2,v4), neighbors=(t5,t2,t)} + + fun replaceTriangleOfVertex v (old, new) = + if nth triangleOfVertex v <> old then () + else upd triangleOfVertex v new + in + updateTriangle mesh t newdata_t; + updateTriangle mesh t1 newdata_t1; + replaceTriangleOfVertex v1 (t, t1); + replaceTriangleOfVertex v3 (t1, t); + replaceNeighbor mesh t2 (t, t1); + replaceNeighbor mesh t4 (t1, t) + end + + fun flip (Mesh {verticesOfTriangle,neighborsOfTriangle,vdata,triangleOfVertex}) simp = + let + (** make a copy *) + val mesh' = + Mesh {verticesOfTriangle = Seq.map (fn x => x) verticesOfTriangle, + neighborsOfTriangle = Seq.map (fn x => x) neighborsOfTriangle, + vdata = Seq.map (fn x => x) vdata, + triangleOfVertex = Seq.map (fn x => x) triangleOfVertex} + in + doFlip mesh' simp; + mesh' + end + + (** ======================================================================== + * loop to find cavity, and do rip-and-tent + *) + +(* + fun ripAndTentCavity mesh center (v, pt) (ta0, ta1) = + let + val (_, simps) = findCavity mesh center pt + in + doSplit mesh center (v, pt) (ta0, ta1); + List.app (doFlip mesh) simps + end +*) + + + fun ripAndTentCavity mesh center (v, pt) (ta0, ta1) = + let + fun loop t = + if not (pointInCircle mesh t pt) then + () + else + let + val t1 = across mesh (rotateClockwise t) + val t2 = across mesh (rotateClockwise (rotateClockwise t)) + in + doFlip mesh t; + maybeLoop t1; + maybeLoop t2 + end + + and maybeLoop t = + case t of + SOME t => loop t + | NONE => () + + val t = (center, 0) + val t1 = across mesh t + val t2 = across mesh (rotateClockwise t) + val t3 = across mesh (rotateClockwise (rotateClockwise t)) + in + doSplit mesh center (v, pt) (ta0, ta1); + maybeLoop t1; + maybeLoop t2; + maybeLoop t3 + end + + +(* + fun ripAndTentCavity mesh center (v, pt) (ta0, ta1) = + let + fun maybePush x xs = + case x of + SOME x => (if pointInCircle mesh x pt then x :: xs else xs) + | NONE => xs + + fun loop ts = + case ts of + [] => () + | t :: ts => + let + val t1 = across mesh (rotateClockwise t) + val t2 = across mesh (rotateClockwise (rotateClockwise t)) + in + doFlip mesh t; + loop (maybePush t1 (maybePush t2 ts)) + end + + val t = (center, 0) + val t1 = across mesh t + val t2 = across mesh (rotateClockwise t) + val t3 = across mesh (rotateClockwise (rotateClockwise t)) + in + doSplit mesh center (v, pt) (ta0, ta1); + loop (maybePush t1 (maybePush t2 (maybePush t3 []))) + end +*) + + (** ======================================================================== + * purely functional rip-and-tent on cavities (returns new mesh) + *) + + + fun ripAndTentOne ((t, simps): cavity, pt: G.point) mesh = + (* List.foldl (fn (s: simplex, m: mesh) => flip m s) (split mesh t pt) simps *) + let + val mesh' = split mesh t pt + in + List.app (doFlip mesh') simps; + mesh' + end + + + fun ripAndTent cavities (Mesh {verticesOfTriangle, neighborsOfTriangle, vdata, triangleOfVertex}) = + let + val numVerts = Seq.length vdata + val numTriangles = Seq.length verticesOfTriangle + val numNewVerts = Seq.length cavities + val numNewTriangles = 2 * numNewVerts + + val vdata' = Seq.tabulate (fn i => + if i < numVerts then nth vdata i else dummyPt) + (numVerts + numNewVerts) + val verticesOfTriangle' = Seq.tabulate (fn i => + if i < numTriangles then nth verticesOfTriangle i else dummyTriple) + (numTriangles + numNewTriangles) + val neighborsOfTriangle' = Seq.tabulate (fn i => + if i < numTriangles then nth neighborsOfTriangle i else dummyTriple) + (numTriangles + numNewTriangles) + val triangleOfVertex' = Seq.tabulate (fn i => + if i < numVerts then nth triangleOfVertex i else INVALID_ID) + (numVerts + numNewVerts) + val mesh' = + Mesh { vdata=vdata' + , verticesOfTriangle=verticesOfTriangle' + , neighborsOfTriangle=neighborsOfTriangle' + , triangleOfVertex=triangleOfVertex' + } + in + ForkJoin.parfor 100 (0, Seq.length cavities) (fn i => + let + val (cavity as (center, simps), pt) = nth cavities i + val ta0 = numTriangles + 2*i + val ta1 = ta0 + 1 + in + doSplit mesh' center (numVerts+i, pt) (ta0, ta1); + List.app (doFlip mesh') simps + end); + + mesh' + end + + + (** ======================================================================== + * generating an initial boundary + *) + + fun initialMeshWithBoundaryCircle + {numVertices, numBoundaryVertices} + {center, radius} + = + let + val pi = Math.pi + val n = Real.fromInt numBoundaryVertices + + val numNonBoundaryVertices = numVertices - numBoundaryVertices + val numNonBoundaryTriangles = 2 * numNonBoundaryVertices + val numBoundaryTriangles = numBoundaryVertices-2 + val numTriangles = numNonBoundaryTriangles + numBoundaryTriangles + + fun boundaryPoint i = + let + val ri = Real.fromInt i + val x = radius * Math.cos (2.0 * pi * (ri / n)) + val y = radius * Math.sin (2.0 * pi * (ri / n)) + val offset: G.point = (x,y) + in + G.Vector.add (center, offset) + end + + fun vertexPoint i = + if i < numNonBoundaryVertices then (0.0, 0.0) + else boundaryPoint (i - numNonBoundaryVertices) + + fun verticesOfTriangle i = + if i < numNonBoundaryTriangles then + (INVALID_ID, INVALID_ID, INVALID_ID) + else + let + val j = i - numNonBoundaryTriangles + numNonBoundaryVertices + in + (j+1, j+2, numNonBoundaryVertices) + end + + fun neighborsOfTriangle i = + if i < numNonBoundaryTriangles then + (INVALID_ID, INVALID_ID, INVALID_ID) + else + ( if i = numNonBoundaryTriangles then INVALID_ID else i-1 + , INVALID_ID + , if i = numTriangles-1 then INVALID_ID else i+1 + ) + + fun triangleOfVertex i = + if i < numNonBoundaryVertices then + INVALID_ID + else + case (i-numNonBoundaryVertices) of + 0 => numNonBoundaryTriangles + | 1 => numNonBoundaryTriangles + | j => numNonBoundaryTriangles + j - 2 + in + Mesh + { vdata = Seq.tabulate vertexPoint numVertices + , triangleOfVertex = Seq.tabulate triangleOfVertex numVertices + , verticesOfTriangle = Seq.tabulate verticesOfTriangle numTriangles + , neighborsOfTriangle = Seq.tabulate neighborsOfTriangle numTriangles + } + end + + (** ======================================================================== + * parsing from file and string representation + *) + + fun sortMeshCCW (mesh as Mesh {vdata, triangleOfVertex, ...}) = + let + val numTriangles = numTriangles mesh + val verticesOfTriangle = AS.full (ForkJoin.alloc numTriangles) + val neighborsOfTriangle = AS.full (ForkJoin.alloc numTriangles) + val mesh' = + Mesh { vdata = vdata + , verticesOfTriangle = verticesOfTriangle + , neighborsOfTriangle = neighborsOfTriangle + , triangleOfVertex = triangleOfVertex + } + in + ForkJoin.parfor 1000 (0, numTriangles) (fn i => + updateTriangle mesh' i (sortTriangleCCW mesh (tdata mesh i))); + + mesh + end + + fun writeMax a i x = + let + fun loop old = + if x <= old then () else + let + val old' = Concurrency.casArray (a, i) (old, x) + in + if old' = old then () + else loop old' + end + in + loop (Array.sub (a, i)) + end + + + fun parseFile filename = + let + val chars = ReadFile.contentsSeq filename + + fun isNewline i = (Seq.nth chars i = #"\n") + + val nlPos = + AS.full (SeqBasis.filter 10000 (0, Seq.length chars) (fn i => i) isNewline) + val numLines = Seq.length nlPos + 1 + fun lineStart i = + if i = 0 then 0 else 1 + Seq.nth nlPos (i-1) + fun lineEnd i = + if i = Seq.length nlPos then Seq.length chars else Seq.nth nlPos i + fun line i = Seq.subseq chars (lineStart i, lineEnd i - lineStart i) + + val _ = + if numLines >= 3 then () + else raise Fail ("Topology2D: read mesh: missing or incomplete header") + + val _ = + if Parse.parseString (line 0) = "Mesh" then () + else raise Fail ("expected Mesh header") + + fun tryParse parser test thing lineNum = + let + fun whoops msg = + raise Fail ("Topology2D: line " + ^ Int.toString (lineNum+1) + ^ ": error while parsing " ^ thing + ^ (case msg of NONE => "" | SOME msg => ": " ^ msg)) + in + case (parser (line lineNum) handle exn => whoops (SOME (exnMessage exn))) of + SOME x => if test x then x else whoops (SOME "test failed") + | NONE => whoops NONE + end + + fun tryParseInt thing lineNum = + tryParse Parse.parseInt (fn x => x >= 0) thing lineNum + fun tryParseReal thing lineNum = + tryParse Parse.parseReal (fn x => true) thing lineNum + + + val numVertices = tryParseInt "num vertices" 1 + val numTriangles = tryParseInt "num triangles" 2 + + fun validVid x = (0 <= x andalso x < numVertices) + fun validTid x = (x = INVALID_ID orelse (0 <= x andalso x< numTriangles)) + fun validTriangle (Tri {vertices=(a,b,c), neighbors=(d,e,f)}) = + validVid a andalso validVid b andalso validVid c + andalso + validTid d andalso validTid e andalso validTid f + + + fun ff range test = FindFirst.findFirstSerial range test + fun ss x (i, j) = Seq.subseq x (i, j-i) + + fun vertexParser line = + let + fun isSpace i = Char.isSpace (Seq.nth line i) + val spaceIdx = valOf (ff (0, Seq.length line) isSpace) + in + SOME + ( valOf (Parse.parseReal (Seq.take line spaceIdx)) + handle Option => raise Fail "bad first value" + , valOf (Parse.parseReal (Seq.drop line (spaceIdx+1))) + handle Option => raise Fail "bad second value" + ) + end + + fun neighborsParser restOfLine = + let + (* val _ = print ("parsing neighbors: " ^ Parse.parseString restOfLine ^ "\n") *) + fun isSpace i = Char.isSpace (Seq.nth restOfLine i) + val n = Seq.length restOfLine + val spPos = AS.full (SeqBasis.filter 10000 (0, n) (fn i => i) isSpace) + val numNbrs = Seq.length spPos + 1 + (* val _ = print ("num neighbors: " ^ Int.toString numNbrs ^ "\n") *) + fun nbrStart i = + if i = 0 then 0 else 1 + Seq.nth spPos (i-1) + fun nbrEnd i = + if i = Seq.length spPos then Seq.length restOfLine else Seq.nth spPos i + fun nbr i = + if i >= numNbrs then INVALID_ID else + ((*print ("nbr " ^ Int.toString i ^ " start " ^ Int.toString (nbrStart i) ^ " end " ^ Int.toString (nbrEnd i) ^ "\n"); + print ("nbrstring: \"" ^ Parse.parseString (ss restOfLine (nbrStart i, nbrEnd i)) ^ "\"\n");*) + valOf (Parse.parseInt (ss restOfLine (nbrStart i, nbrEnd i))) + handle Option => raise Fail ("bad neighbor")) + in + (nbr 0, nbr 1, nbr 2) + end + + fun triangleParser line = + let + fun isSpace i = Char.isSpace (Seq.nth line i) + val n = Seq.length line + val sp1 = valOf (ff (0, n) isSpace) + val sp2 = valOf (ff (sp1+1, n) isSpace) + val sp3 = Option.getOpt (ff (sp2+1, n) isSpace, n) + val verts = + ( valOf (Parse.parseInt (ss line (0, sp1))) + handle Option => raise Fail "bad first vertex" + , valOf (Parse.parseInt (ss line (sp1+1, sp2))) + handle Option => raise Fail "bad second vertex" + , valOf (Parse.parseInt (ss line (sp2+1, sp3))) + handle Option => raise Fail "bad third vertex" + ) + val nbrs = + if sp3 = n then (INVALID_ID,INVALID_ID,INVALID_ID) + else neighborsParser (ss line (sp3+1, n)) + in + SOME (Tri {vertices=verts, neighbors=nbrs}) + end + + fun tryParseVertex lineNum = + tryParse vertexParser (fn _ => true) "vertex" lineNum + + fun tryParseTriangle lineNum = + tryParse triangleParser validTriangle "triangle" lineNum + + val _ = + if numLines >= numVertices + numTriangles + 3 then () + else raise Fail ("Topology2D: not enough vertices and/or triangles to parse") + + val vertices = AS.full (SeqBasis.tabulate 1000 (0, numVertices) + (fn i => tryParseVertex (3+i))) + + val verticesOfTriangle = AS.full (ForkJoin.alloc numTriangles) + val neighborsOfTriangle = AS.full (ForkJoin.alloc numTriangles) + val triangleOfVertex = ForkJoin.alloc numVertices + val _ = ForkJoin.parfor 10000 (0, numVertices) + (fn i => Array.update (triangleOfVertex, i, ~1)) + + val _ = ForkJoin.parfor 1000 (0, numTriangles) (fn i => + let + val Tri {vertices=(a,b,c), neighbors} = + tryParseTriangle (3+numVertices+i) + in + upd verticesOfTriangle i (a,b,c); + upd neighborsOfTriangle i neighbors; + writeMax triangleOfVertex a i; + writeMax triangleOfVertex b i; + writeMax triangleOfVertex c i + end) + in + sortMeshCCW (Mesh + { vdata = vertices + , verticesOfTriangle = verticesOfTriangle + , neighborsOfTriangle = neighborsOfTriangle + , triangleOfVertex = AS.full triangleOfVertex + }) + end + + + fun toString (mesh as Mesh {vdata=vertices, ...}) = + let + val nv = numVertices mesh + val nt = numTriangles mesh + + fun ptos (x,y) = Real.toString x ^ " " ^ Real.toString y + + fun ttos (Tri {vertices=(a,b,c), neighbors=(d,e,f)}) = + String.concatWith " " (List.map Int.toString [a,b,c,d,e,f]) + in + String.concatWith "\n" + ([ "Mesh" + , Int.toString nv + , Int.toString nt + ] + @ + List.tabulate (nv, ptos o Seq.nth vertices) + @ + List.tabulate (nt, ttos o tdata mesh)) + end + +end diff --git a/tests/mpllib/TreeMatrix.sml b/tests/mpllib/TreeMatrix.sml new file mode 100644 index 000000000..e6a94f7dd --- /dev/null +++ b/tests/mpllib/TreeMatrix.sml @@ -0,0 +1,148 @@ +structure TreeMatrix: +sig + (* square matrices of sidelength 2^n matrices only! *) + datatype matrix = + Node of int * matrix * matrix * matrix * matrix + | Leaf of int * real Array.array + + val tabulate: int -> (int * int -> real) -> matrix + val flatten: matrix -> real array + val sidelength: matrix -> int + val multiply: matrix * matrix -> matrix +end = +struct + + val par = ForkJoin.par + + fun par4 (a, b, c, d) = + let + val ((ar, br), (cr, dr)) = par (fn _ => par (a, b), fn _ => par (c, d)) + in + (ar, br, cr, dr) + end + + datatype matrix = + Node of int * matrix * matrix * matrix * matrix + | Leaf of int * real Array.array + + exception MatrixFormat + + fun sidelength mat = + case mat of + Leaf (n, s) => n + | Node (n, _, _, _, _) => n + + fun tabulate sidelen f = + let + fun tab n (row, col) = + if n <= 64 then + Leaf (n, Array.tabulate (n * n, fn i => f (row + i div n, col + i mod n))) + else + let + val half = n div 2 + val (m11, m12, m21, m22) = + par4 ( fn _ => tab half (row, col) + , fn _ => tab half (row, col + half) + , fn _ => tab half (row + half, col) + , fn _ => tab half (row + half, col + half) + ) + in + Node (n, m11, m12, m21, m22) + end + in + tab sidelen (0, 0) + end + + val upd = Array.update + + fun writeFlatten (result, start, rowskip) m = + case m of + Leaf (n, s) => + let fun idx i = start + (i div n)*rowskip + (i mod n) + in Array.appi (fn (i, x) => upd (result, idx i, x)) s + end + | Node (n, m11, m12, m21, m22) => + ( par4 ( fn _ => writeFlatten (result, start, rowskip) m11 + , fn _ => writeFlatten (result, start + n div 2, rowskip) m12 + , fn _ => writeFlatten (result, start + (n div 2) * rowskip, rowskip) m21 + , fn _ => writeFlatten (result, start + (n div 2) * (rowskip + 1), rowskip) m22 + ) + ; () + ) + + fun flatten m = + let + val n = sidelength m + val result = ForkJoin.alloc (n * n) + in + writeFlatten (result, 0, n) m; + result + end + + fun flatmultiply n (s, t, output) = + let + val sub = Array.sub + val a = s + val b = t + val aStart = 0 + val bStart = 0 + (* assume our lengths are good *) + (* loop with accumulator to compute dot product. r is an index into + * vector a (the row index) and c is an index into b (the col index) *) + fun loop rowStop acc r c = + if r = rowStop then acc + else let val acc' = acc + (sub (a, r) * sub (b, c)) + val r' = r + 1 + val c' = c + n + in loop rowStop acc' r' c' + end + fun cell c = + let + val (i, j) = (c div n, c mod n) + val rowStart = aStart + i*n + val rowStop = rowStart + n + val colStart = bStart + j + in + loop rowStop 0.0 rowStart colStart + end + fun update i = + let + val newv = cell i + val old = sub (output, i) + in + Array.update (output, i, newv + old) + end + fun loopi i hi = + if i >= hi then () else (update i; loopi (i + 1) hi) + in + loopi 0 (n * n) + end + + fun multiply' (a, b, c) = + case (a, b, c) of + (Leaf (n, s), Leaf (_, t), Leaf (_, c)) => flatmultiply n (s, t, c) + | (Node (n, a11, a12, a21, a22), + Node (_, b11, b12, b21, b22), + Node (_, c11, c12, c21, c22)) => + let + fun block (m1, m2, m3, m4, c) = + (multiply' (m1, m2, c); multiply' (m3, m4, c)) + in + par4 ( fn _ => block (a11, b11, a12, b21, c11) + , fn _ => block (a11, b12, a12, b22, c12) + , fn _ => block (a21, b11, a22, b21, c21) + , fn _ => block (a21, b12, a22, b22, c22) + ); + () + end + | _ => raise MatrixFormat + + fun multiply (a, b) = + let + val c = tabulate (sidelength a) (fn _ => 0.0) + in + multiply' (a, b, c); + c + end + +end diff --git a/tests/mpllib/Util.sml b/tests/mpllib/Util.sml new file mode 100644 index 000000000..97b9e02f8 --- /dev/null +++ b/tests/mpllib/Util.sml @@ -0,0 +1,354 @@ +structure Util: +sig + val getTime: (unit -> 'a) -> ('a * Time.time) + val reportTime: (unit -> 'a) -> 'a + + val closeEnough: real * real -> bool + + val die: string -> 'a + + val repeat: int * (unit -> 'a) -> ('a) + + val hash64: Word64.word -> Word64.word + val hash64_2: Word64.word -> Word64.word + val hash32: Word32.word -> Word32.word + val hash32_2: Word32.word -> Word32.word + val hash32_3: Word32.word -> Word32.word + val hash: int -> int + + val ceilDiv: int -> int -> int + + val pow2: int -> int + + (* this actually computes 1 + floor(log_2(n)), i.e. the number of + * bits required to represent n in binary *) + val log2: int -> int + + (* boundPow2 n == smallest power of 2 that is less-or-equal-to n *) + val boundPow2: int -> int + + val foreach: 'a ArraySlice.slice -> (int * 'a -> unit) -> unit + + (* if the array is short, then convert it to a string. otherwise only + * show the first few elements and the last element *) + val summarizeArray: int -> ('a -> string) -> 'a array -> string + val summarizeArraySlice: int -> ('a -> string) -> 'a ArraySlice.slice -> string + + (* `for (lo, hi) f` do f(i) sequentially for each lo <= i < hi + * forBackwards goes from hi-1 down to lo *) + val for: (int * int) -> (int -> unit) -> unit + val forBackwards: (int * int) -> (int -> unit) -> unit + + (* `loop (lo, hi) b f` + * for lo <= i < hi, iteratively do b = f (b, i) *) + val loop: (int * int) -> 'a -> ('a * int -> 'a) -> 'a + + val all: (int * int) -> (int -> bool) -> bool + val exists: (int * int) -> (int -> bool) -> bool + + val copyListIntoArray: 'a list -> 'a array -> int -> int + + val revMap: ('a -> 'b) -> 'a list -> 'b list + + val intToString: int -> string + + val equalLists: ('a * 'a -> bool) -> 'a list * 'a list -> bool + +end = +struct + + fun ceilDiv n k = 1 + (n-1) div k + + fun digitToChar d = Char.chr (d + 48) + + fun intToString x = + let + (** For binary precision p, number of decimal digits needed is upper + * bounded by: + * 1 + log_{10}(2^p) = 1 + p * log_{10}(2) + * ~= 1 + p * 0.30103 + * < 1 + p * 0.33333 + * = 1 + p / 3 + * Just for a little extra sanity, we'll do ceiling-div. + *) + val maxNumChars = 1 + ceilDiv (valOf Int.precision) 3 + val buf = ForkJoin.alloc maxNumChars + + val orig = x + + fun loop q i = + let + val i = i-1 + val d = ~(Int.rem (q, 10)) + val _ = Array.update (buf, i, digitToChar d) + val q = Int.quot (q, 10) + in + if q <> 0 then + loop q i + else if orig < 0 then + (Array.update (buf, i-1, #"~"); i-1) + else + i + end + + val start = loop (if orig < 0 then orig else ~orig) maxNumChars + in + CharVector.tabulate (maxNumChars-start, fn i => Array.sub (buf, start+i)) + end + + fun die msg = + ( TextIO.output (TextIO.stdErr, msg ^ "\n") + ; TextIO.flushOut TextIO.stdErr + ; OS.Process.exit OS.Process.failure + ) + + fun getTime f = + let + val t0 = Time.now () + val result = f () + val t1 = Time.now () + in + (result, Time.- (t1, t0)) + end + + fun reportTime f = + let + val (result, tm) = getTime f + in + print ("time " ^ Time.fmt 4 tm ^ "s\n"); + result + end + + fun closeEnough (x, y) = + Real.abs (x - y) <= 0.000001 + + (* NOTE: this actually computes 1 + floor(log_2(n)), i.e. the number of + * bits required to represent n in binary *) + fun log2 n = if (n < 1) then 0 else 1 + log2(n div 2) + + fun pow2 i = if (i<1) then 1 else 2*pow2(i-1) + + fun searchPow2 n m = if m >= n then m else searchPow2 n (2*m) + fun boundPow2 n = searchPow2 n 1 + + fun loop (lo, hi) b f = + if lo >= hi then b else loop (lo+1, hi) (f (b, lo)) f + + fun forBackwards (i, j) f = + if i >= j then () else (f (j-1); forBackwards (i, j-1) f) + + fun for (lo, hi) f = + if lo >= hi then () else (f lo; for (lo+1, hi) f) + + fun foreach s f = + ForkJoin.parfor 4096 (0, ArraySlice.length s) + (fn i => f (i, ArraySlice.sub (s, i))) + + fun all (lo, hi) f = + let + fun allFrom i = + (i >= hi) orelse (f i andalso allFrom (i+1)) + in + allFrom lo + end + + fun exists (lo, hi) f = + let + fun existsFrom i = + i < hi andalso (f i orelse existsFrom (i+1)) + in + existsFrom lo + end + + fun copyListIntoArray xs arr i = + case xs of + [] => i + | x :: xs => + ( Array.update (arr, i, x) + ; copyListIntoArray xs arr (i+1) + ) + + fun repeat (n, f) = + let + fun rep_help 1 = f() + | rep_help n = ((rep_help (n-1)); f()) + + val ns = if (n>0) then n else 1 + in + rep_help ns + end + + fun summarizeArraySlice count toString xs = + let + val n = ArraySlice.length xs + fun elem i = ArraySlice.sub (xs, i) + + val strs = + if count <= 0 then raise Fail "summarizeArray needs count > 0" + else if count <= 2 orelse n <= count then + List.tabulate (n, toString o elem) + else + List.tabulate (count-1, toString o elem) @ + ["...", toString (elem (n-1))] + in + "[" ^ (String.concatWith ", " strs) ^ "]" + end + + fun summarizeArray count toString xs = + summarizeArraySlice count toString (ArraySlice.full xs) + + fun revMap f xs = + let + fun loop acc xs = + case xs of + [] => acc + | x :: rest => loop (f x :: acc) rest + in + loop [] xs + end + + (* // from numerical recipes + * uint64_t hash64(uint64_t u) + * { + * uint64_t v = u * 3935559000370003845ul + 2691343689449507681ul; + * v ^= v >> 21; + * v ^= v << 37; + * v ^= v >> 4; + * v *= 4768777513237032717ul; + * v ^= v << 20; + * v ^= v >> 41; + * v ^= v << 5; + * return v; + * } + *) + + fun hash64 u = + let + open Word64 + infix 2 >> << xorb andb + val v = u * 0w3935559000370003845 + 0w2691343689449507681 + val v = v xorb (v >> 0w21) + val v = v xorb (v << 0w37) + val v = v xorb (v >> 0w4) + val v = v * 0w4768777513237032717 + val v = v xorb (v << 0w20) + val v = v xorb (v >> 0w41) + val v = v xorb (v << 0w5) + in + v + end + + (* uint32_t hash32(uint32_t a) { + * a = (a+0x7ed55d16) + (a<<12); + * a = (a^0xc761c23c) ^ (a>>19); + * a = (a+0x165667b1) + (a<<5); + * a = (a+0xd3a2646c) ^ (a<<9); + * a = (a+0xfd7046c5) + (a<<3); + * a = (a^0xb55a4f09) ^ (a>>16); + * return a; + * } + *) + + fun hash32 a = + let + open Word32 + infix 2 >> << xorb + val a = (a + 0wx7ed55d16) + (a << 0w12) + val a = (a xorb 0wxc761c23c) xorb (a >> 0w19) + val a = (a + 0wx165667b1) + (a << 0w5) + val a = (a + 0wxd3a2646c) xorb (a << 0w9) + val a = (a + 0wxfd7046c5) + (a << 0w3) + val a = (a xorb 0wxb55a4f09) xorb (a >> 0w16) + in + a + end + + (* uint32_t hash32_2(uint32_t a) { + * uint32_t z = (a + 0x6D2B79F5UL); + * z = (z ^ (z >> 15)) * (z | 1UL); + * z ^= z + (z ^ (z >> 7)) * (z | 61UL); + * return z ^ (z >> 14); + * } + *) + + fun hash32_2 a = + let + open Word32 + infix 2 >> << xorb orb + val z = (a + 0wx6D2B79F5) + val z = (z xorb (z >> 0w15)) * (z orb 0w1) + val z = z xorb (z + (z xorb (z >> 0w7)) * (z orb 0w61)) + in + z xorb (z >> 0w14) + end + + (* inline uint32_t hash32_3(uint32_t a) { + * uint32_t z = a + 0x9e3779b9; + * z ^= z >> 15; // 16 for murmur3 + * z *= 0x85ebca6b; + * z ^= z >> 13; + * z *= 0xc2b2ae3d; // 0xc2b2ae35 for murmur3 + * return z ^= z >> 16; + * } + *) + + fun hash32_3 a = + let + open Word32 + infix 2 >> << xorb orb + val z = a + 0wx9e3779b9 + val z = z xorb (z >> 0w15) (* 16 for murmur3 *) + val z = z * 0wx85ebca6b + val z = z xorb (z >> 0w13) + val z = z * 0wxc2b2ae3d (* 0wxc2b2ae35 for murmur3 *) + val z = z xorb (z >> 0w16) + in + z + end + + (* // a slightly cheaper, but possibly not as good version + * // based on splitmix64 + * inline uint64_t hash64_2(uint64_t x) { + * x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9); + * x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb); + * x = x ^ (x >> 31); + * return x; + * } + *) + + fun hash64_2 x = + let + open Word64 + infix 2 >> << xorb orb + val x = (x xorb (x >> 0w30)) * 0wxbf58476d1ce4e5b9 + val x = (x xorb (x >> 0w27)) * 0wx94d049bb133111eb + val x = x xorb (x >> 0w31) + in + x + end + + (* This chooses which hash function to use for generic integers, since + * integers are configurable at compile time. *) + val hash : int -> int = + case Int.precision of + NONE => (Word64.toInt o hash64 o Word64.fromInt) + | SOME 32 => (Word32.toIntX o hash32 o Word32.fromInt) + | SOME 64 => (Word64.toIntX o hash64 o Word64.fromInt) + | SOME p => (fn x => + let + val wp1 = Word.fromInt (p-1) + open Word64 + infix 2 >> << andb + val v = hash64 (fromInt x) + val v = v andb ((0w1 << wp1) - 0w1) + in + toInt v + end) + + + fun equalLists eq ([], []) = true + | equalLists eq (x :: xs, y :: ys) = + eq (x, y) andalso equalLists eq (xs, ys) + | equalLists _ _ = false + +end diff --git a/tests/mpllib/compat/PosixReadFile.sml b/tests/mpllib/compat/PosixReadFile.sml new file mode 100644 index 000000000..7f6e73199 --- /dev/null +++ b/tests/mpllib/compat/PosixReadFile.sml @@ -0,0 +1,60 @@ +structure PosixReadFile = +struct + + fun contentsSeq' (readByte: Word8.word -> 'a) path = + let + val (file, length) = + let + open Posix.FileSys + val file = openf (path, O_RDONLY, O.fromWord 0w0) + in + (file, Position.toInt (ST.size (fstat file))) + end + + open Posix.IO + + val bufferSize = 100000 + val buffer = Word8ArrayExtra.alloc bufferSize + val result = ArrayExtra.alloc length + (* val result = Word8ArrayExtra.alloc length *) + + (* fun copyToResult i n = + Word8ArraySlice.copy + { src = Word8ArraySlice.slice (buffer, 0, SOME n) + , dst = result + , di = i + } *) + + fun copyToResult i n = + Word8ArraySlice.appi (fn (j, b) => + Unsafe.Array.update (result, i+j, readByte b)) + (Word8ArraySlice.slice (buffer, 0, SOME n)) + + fun dumpFrom i = + if i >= length then () else + let + val bytesRead = readArr (file, Word8ArraySlice.full buffer) + in + copyToResult i bytesRead; + dumpFrom (i + bytesRead) + end + in + dumpFrom 0; + close file; + ArraySlice.full result + end + + fun contentsSeq path = + contentsSeq' (Char.chr o Word8.toInt) path + + fun contentsBinSeq path = + contentsSeq' (fn w => w) path + + fun contents filename = + let + val chars = contentsSeq filename + in + CharVector.tabulate (ArraySlice.length chars, + fn i => ArraySlice.sub (chars, i)) + end +end diff --git a/tests/mpllib/compat/PosixWriteFile.sml b/tests/mpllib/compat/PosixWriteFile.sml new file mode 100644 index 000000000..bceb2f1ad --- /dev/null +++ b/tests/mpllib/compat/PosixWriteFile.sml @@ -0,0 +1,13 @@ +structure PosixWriteFile = +struct + fun dump (filename: string, contents: string) = + let + open Posix.FileSys + val f = creat (filename, S.flags [S.iwusr, S.irusr, S.irgrp, S.iroth]) + val contentslice = (Word8VectorSlice.full (Byte.stringToBytes contents)) + in + (Posix.IO.writeVec (f, contentslice); + Posix.IO.close f; + ()) + end +end diff --git a/tests/mpllib/compat/mlton.mlb b/tests/mpllib/compat/mlton.mlb new file mode 100644 index 000000000..e0e996a1c --- /dev/null +++ b/tests/mpllib/compat/mlton.mlb @@ -0,0 +1,25 @@ +local + $(SML_LIB)/basis/basis.mlb + $(SML_LIB)/basis/mlton.mlb + $(SML_LIB)/basis/unsafe.mlb + local + $(SML_LIB)/basis/build/sources.mlb + in + structure ArrayExtra = Array + structure VectorExtra = Vector + structure Word8ArrayExtra = Word8Array + end + + PosixReadFile.sml + PosixWriteFile.sml + mlton.sml +in + structure ForkJoin + structure Concurrency + structure ReadFile + structure WriteFile + structure GCStats + structure MLton + structure VectorExtra + structure RuntimeStats +end diff --git a/tests/mpllib/compat/mlton.sml b/tests/mpllib/compat/mlton.sml new file mode 100644 index 000000000..b5a3b243c --- /dev/null +++ b/tests/mpllib/compat/mlton.sml @@ -0,0 +1,68 @@ +structure ForkJoin: +sig + val par: (unit -> 'a) * (unit -> 'b) -> 'a * 'b + val parfor: int -> int * int -> (int -> unit) -> unit + val alloc: int -> 'a array +end = +struct + fun par (f, g) = (f (), g ()) + fun parfor (g: int) (lo, hi) (f: int -> unit) = + if lo >= hi then () else (f lo; parfor g (lo + 1, hi) f) + fun alloc n = ArrayExtra.alloc n +end + +structure VectorExtra: +sig + val unsafeFromArray: 'a array -> 'a vector +end = +struct open VectorExtra end + +structure Concurrency = +struct + val numberOfProcessors = 1 + + fun cas r (x, y) = + let val current = !r + in if MLton.eq (x, current) then r := y else (); current + end + + fun casArray (a, i) (x, y) = + let val current = Array.sub (a, i) + in if MLton.eq (x, current) then Array.update (a, i, y) else (); current + end +end + +structure ReadFile = PosixReadFile + +structure WriteFile = PosixWriteFile + +structure GCStats: +sig + val report: unit -> unit +end = +struct + + fun p name thing = + print (name ^ ": " ^ thing () ^ "\n") + + fun report () = + let in print ("======== GC Stats ========\n"); print "none yet...\n" + end + +end + +structure RuntimeStats: +sig + type t + val get: unit -> t + val benchReport: {before: t, after: t} -> unit +end = +struct + type t = unit + fun get () = () + fun benchReport _ = + ( print ("======== Runtime Stats ========\n") + ; print ("none yet...\n") + ; print ("====== End Runtime Stats ======\n") + ) +end diff --git a/tests/mpllib/compat/mpl-old.mlb b/tests/mpllib/compat/mpl-old.mlb new file mode 100644 index 000000000..acf9bcf69 --- /dev/null +++ b/tests/mpllib/compat/mpl-old.mlb @@ -0,0 +1,25 @@ +local + $(SML_LIB)/basis/basis.mlb + $(SML_LIB)/basis/mlton.mlb + $(SML_LIB)/basis/unsafe.mlb + $(SML_LIB)/basis/fork-join.mlb + + local + $(SML_LIB)/basis/build/sources.mlb + in + structure ArrayExtra = Array + structure VectorExtra = Vector + structure Word8ArrayExtra = Word8Array + end + + PosixReadFile.sml + mpl-old.sml +in + structure ForkJoin + structure Concurrency + structure ReadFile + structure GCStats + structure MLton + structure VectorExtra + structure RuntimeStats +end diff --git a/tests/mpllib/compat/mpl-old.sml b/tests/mpllib/compat/mpl-old.sml new file mode 100644 index 000000000..1b5124c9c --- /dev/null +++ b/tests/mpllib/compat/mpl-old.sml @@ -0,0 +1,49 @@ +(* already provided by the compiler *) +structure ForkJoin = ForkJoin + +structure Concurrency = +struct + val numberOfProcessors = MLton.Parallel.numberOfProcessors + val cas = MLton.Parallel.compareAndSwap + val casArray = MLton.Parallel.arrayCompareAndSwap +end + +structure VectorExtra: +sig + val unsafeFromArray: 'a array -> 'a vector +end = +struct open VectorExtra end + +structure ReadFile = PosixReadFile + +structure GCStats: +sig + val report: unit -> unit +end = +struct + + fun p name thing = + print (name ^ ": " ^ thing () ^ "\n") + + fun report () = + let in print ("======== GC Stats ========\n"); print "none yet...\n" + end + +end + + +structure RuntimeStats: +sig + type t + val get: unit -> t + val benchReport: {before: t, after: t} -> unit +end = +struct + type t = unit + fun get () = () + fun benchReport _ = + ( print ("======== Runtime Stats ========\n") + ; print ("none yet...\n") + ; print ("====== End Runtime Stats ======\n") + ) +end diff --git a/tests/mpllib/compat/mpl.mlb b/tests/mpllib/compat/mpl.mlb new file mode 100644 index 000000000..9e56c0ad6 --- /dev/null +++ b/tests/mpllib/compat/mpl.mlb @@ -0,0 +1,23 @@ +local + $(SML_LIB)/basis/basis.mlb + $(SML_LIB)/basis/mlton.mlb + $(SML_LIB)/basis/mpl.mlb + $(SML_LIB)/basis/fork-join.mlb + + local + $(SML_LIB)/basis/build/sources.mlb + in + structure VectorExtra = Vector + end + PosixWriteFile.sml + mpl.sml +in + structure ForkJoin + structure Concurrency + structure ReadFile + structure WriteFile + structure GCStats + structure RuntimeStats + structure MLton + structure VectorExtra +end diff --git a/tests/mpllib/compat/mpl.sml b/tests/mpllib/compat/mpl.sml new file mode 100644 index 000000000..6d3183c59 --- /dev/null +++ b/tests/mpllib/compat/mpl.sml @@ -0,0 +1,236 @@ +(* already provided by the compiler *) +structure ForkJoin = ForkJoin + +structure Concurrency = +struct + val numberOfProcessors = MLton.Parallel.numberOfProcessors + val cas = MLton.Parallel.compareAndSwap + val casArray = MLton.Parallel.arrayCompareAndSwap +end + +structure VectorExtra: +sig + val unsafeFromArray: 'a array -> 'a vector +end = +struct open VectorExtra end + +structure ReadFile = +struct + + fun contentsSeq' reader filename = + let + val file = MPL.File.openFile filename + val n = MPL.File.size file + val arr = ForkJoin.alloc n + val k = 10000 + val m = 1 + (n - 1) div k + in + ForkJoin.parfor 1 (0, m) (fn i => + let + val lo = i * k + val hi = Int.min ((i + 1) * k, n) + in + reader file lo (ArraySlice.slice (arr, lo, SOME (hi - lo))) + end); + MPL.File.closeFile file; + ArraySlice.full arr + end + + fun contentsSeq filename = contentsSeq' MPL.File.readChars filename + + fun contentsBinSeq filename = contentsSeq' MPL.File.readWord8s filename + + fun contents filename = + let + val chars = contentsSeq filename + in + CharVector.tabulate (ArraySlice.length chars, fn i => + ArraySlice.sub (chars, i)) + end + +end + +structure WriteFile = PosixWriteFile + +structure GCStats: +sig + val report: unit -> unit +end = +struct + + fun p name thing = + print (name ^ ": " ^ thing () ^ "\n") + + fun report () = + let in + print ("======== GC Stats ========\n"); + p "local reclaimed" (LargeInt.toString o MPL.GC.localBytesReclaimed); + p "num local" (LargeInt.toString o MPL.GC.numLocalGCs); + p "local gc time" + (LargeInt.toString o Time.toMilliseconds o MPL.GC.localGCTime); + p "promo time" + (LargeInt.toString o Time.toMilliseconds o MPL.GC.promoTime); + p "internal reclaimed" (LargeInt.toString o MPL.GC.internalBytesReclaimed) + end + +end + + +structure RuntimeStats: +sig + type t + val get: unit -> t + val benchReport: {before: t, after: t} -> unit +end = +struct + + type stats = + { lgcCount: int + , lgcBytesReclaimed: int + , lgcBytesInScope: int + , lgcTracingTime: Time.time + , lgcPromoTime: Time.time + , cgcCount: int + , cgcBytesReclaimed: int + , cgcBytesInScope: int + , cgcTime: Time.time + , schedWorkTime: Time.time + , schedIdleTime: Time.time + , susMarks: int + , deChecks: int + , entanglements: int + , bytesPinnedEntangled: int + , bytesPinnedEntangledWatermark: int + , numSpawns: int + , numEagerSpawns: int + , numHeartbeats: int + , numSkippedHeartbeats: int + , numSteals: int + , maxHeartbeatStackWalk: int + , maxHeartbeatStackSize: int + } + + datatype t = Stats of stats + + fun get () = + Stats + { lgcCount = LargeInt.toInt (MPL.GC.numLocalGCs ()) + , lgcBytesReclaimed = LargeInt.toInt (MPL.GC.localBytesReclaimed ()) + , lgcBytesInScope = LargeInt.toInt (MPL.GC.bytesInScopeForLocal ()) + , lgcTracingTime = MPL.GC.localGCTime () + , lgcPromoTime = MPL.GC.promoTime () + , cgcCount = LargeInt.toInt (MPL.GC.numCCs ()) + , cgcBytesReclaimed = LargeInt.toInt (MPL.GC.ccBytesReclaimed ()) + , cgcBytesInScope = LargeInt.toInt (MPL.GC.bytesInScopeForCC ()) + , cgcTime = MPL.GC.ccTime () + , schedWorkTime = ForkJoin.workTimeSoFar () + , schedIdleTime = ForkJoin.idleTimeSoFar () + , susMarks = LargeInt.toInt (MPL.GC.numberSuspectsMarked ()) + , deChecks = LargeInt.toInt (MPL.GC.numberDisentanglementChecks ()) + , entanglements = LargeInt.toInt (MPL.GC.numberEntanglements ()) + , bytesPinnedEntangled = LargeInt.toInt (MPL.GC.bytesPinnedEntangled ()) + , bytesPinnedEntangledWatermark = LargeInt.toInt + (MPL.GC.bytesPinnedEntangledWatermark ()) + , numSpawns = ForkJoin.numSpawnsSoFar () + , numEagerSpawns = ForkJoin.numEagerSpawnsSoFar () + , numHeartbeats = ForkJoin.numHeartbeatsSoFar () + , numSkippedHeartbeats = ForkJoin.numSkippedHeartbeatsSoFar () + , numSteals = ForkJoin.numStealsSoFar () + , maxHeartbeatStackSize = IntInf.toInt + (MPL.GC.maxStackSizeForHeartbeat ()) + , maxHeartbeatStackWalk = IntInf.toInt + (MPL.GC.maxStackFramesWalkedForHeartbeat ()) + } + + fun pct a b = + Real.round (100.0 * (Real.fromInt a / Real.fromInt b)) + handle _ => 0 + + val itos = Int.toString + val rtos = Real.fmt (StringCvt.FIX (SOME 2)) + + fun benchReport {before = Stats b, after = Stats a} = + let + val numSpawns = #numSpawns a - #numSpawns b + val numEagerSpawns = #numEagerSpawns a - #numEagerSpawns b + val numHeartbeatSpawns = numSpawns - numEagerSpawns + val numHeartbeats = #numHeartbeats a - #numHeartbeats b + val numSkippedHeartbeats = + #numSkippedHeartbeats a - #numSkippedHeartbeats b + val numSteals = #numSteals a - #numSteals b + + val eagerp = pct numEagerSpawns numSpawns + val hbp = pct numHeartbeatSpawns numSpawns + val skipp = pct numSkippedHeartbeats numHeartbeats + + val spawnsPerHb = Real.fromInt numSpawns / Real.fromInt numHeartbeats + handle _ => 0.0 + + val eagerSpawnsPerHb = + Real.fromInt numEagerSpawns / Real.fromInt numHeartbeats + handle _ => 0.0 + + val hbSpawnsPerHb = + Real.fromInt numHeartbeatSpawns / Real.fromInt numHeartbeats + handle _ => 0.0 + + fun p name (selector: stats -> 'a) (differ: 'a * 'a -> 'a) + (stringer: 'a -> string) : unit = + print (name ^ " " ^ stringer (differ (selector a, selector b)) ^ "\n") + in + print ("======== Runtime Stats ========\n"); + print ("num spawns " ^ itos numSpawns ^ "\n"); + print + (" eager " ^ itos numEagerSpawns ^ " (" ^ itos eagerp + ^ "%)\n"); + print + (" at heartbeat " ^ itos numHeartbeatSpawns ^ " (" ^ itos hbp + ^ "%)\n"); + print "\n"; + print ("num heartbeats " ^ itos numHeartbeats ^ "\n"); + print + (" skipped " ^ itos numSkippedHeartbeats ^ " (" ^ itos skipp + ^ "%)\n"); + print "\n"; + print ("spawns / hb " ^ rtos spawnsPerHb ^ "\n"); + print (" eager " ^ rtos eagerSpawnsPerHb ^ "\n"); + print (" at heartbeat " ^ rtos hbSpawnsPerHb ^ "\n"); + print "\n"; + print ("num steals " ^ itos numSteals ^ "\n"); + print "\n"; + print ("max hb stack walk " ^ itos (#maxHeartbeatStackWalk a) ^ "\n"); + print ("max hb stack size " ^ itos (#maxHeartbeatStackSize a) ^ "\n"); + print "\n"; + + p "sus marks" #susMarks op- Int.toString; + p "de checks" #deChecks op- Int.toString; + p "entanglements" #entanglements op- Int.toString; + p "bytes pinned entangled" #bytesPinnedEntangled op- Int.toString; + p "bytes pinned entangled watermark" #bytesPinnedEntangledWatermark #1 + Int.toString; + print "\n"; + p "lgc count" #lgcCount op- Int.toString; + p "lgc bytes reclaimed" #lgcBytesReclaimed op- Int.toString; + p "lgc bytes in scope " #lgcBytesInScope op- Int.toString; + p "lgc trace time(ms) " #lgcTracingTime Time.- + (LargeInt.toString o Time.toMilliseconds); + p "lgc promo time(ms) " #lgcPromoTime Time.- + (LargeInt.toString o Time.toMilliseconds); + p "lgc total time(ms) " + (fn x => Time.+ (#lgcTracingTime x, #lgcPromoTime x)) Time.- + (LargeInt.toString o Time.toMilliseconds); + print "\n"; + p "cgc count" #cgcCount op- Int.toString; + p "cgc bytes reclaimed" #cgcBytesReclaimed op- Int.toString; + p "cgc bytes in scope " #cgcBytesInScope op- Int.toString; + p "cgc time(ms)" #cgcTime Time.- (LargeInt.toString o Time.toMilliseconds); + print "\n"; + p "work time(ms)" #schedWorkTime Time.- + (LargeInt.toString o Time.toMilliseconds); + p "idle time(ms)" #schedIdleTime Time.- + (LargeInt.toString o Time.toMilliseconds); + print ("====== End Runtime Stats ======\n"); + () + end + +end diff --git a/tests/mpllib/sources.mlton.mlb b/tests/mpllib/sources.mlton.mlb new file mode 100644 index 000000000..67ebb64e1 --- /dev/null +++ b/tests/mpllib/sources.mlton.mlb @@ -0,0 +1,85 @@ +$(SML_LIB)/basis/basis.mlb +compat/mlton.mlb + +CommandLineArgs.sml +Util.sml +SeqBasis.sml + +SEQUENCE.sml +local + ArraySequence.sml +in + structure Seq = ArraySequence + structure ArraySequence +end + +Seqifier.sml +TFlatten.sml + +OffsetSearch.sml +STREAM.sml +DelayedStream.sml +RecursiveStream.sml +DelayedSeq.sml +OldDelayedSeq.sml + +FuncSequence.sml +BinarySearch.sml +Merge.sml +SeqifiedMerge.sml +FlattenMerge.sml +StableMerge.sml +DoubleBinarySearch.sml +StableMergeLowSpan.sml +StableSort.sml +Quicksort.sml +Mergesort.sml +TreeMatrix.sml +AugMap.sml + +PureSeq.sml + +SampleSort.sml +CountingSort.sml +RadixSort.sml +Shuffle.sml + +(* ReadFile.sml *) +Tokenize.sml +FindFirst.sml +Parse.sml +ParseFile.sml +(* TabFilterTree.sml *) +AdjacencyGraph.sml +AdjacencyInt.sml + +MatCOO.sml + +CheckSort.sml + +MkComplex.sml +Rat.sml +Geometry3D.sml +Geometry2D.sml +Topology2D.sml + +NearestNeighbors.sml + +Color.sml +PPM.sml +ExtraBinIO.sml +GIF.sml +NewWaveIO.sml +Signal.sml + +MeshToImage.sml + +MkGrep.sml + +Hashset.sml +Hashtable.sml + +ParFuncArray.sml +ChunkedTreap.sml + +Benchmark.sml diff --git a/tests/mpllib/sources.mpl.mlb b/tests/mpllib/sources.mpl.mlb new file mode 100644 index 000000000..a1da67fc5 --- /dev/null +++ b/tests/mpllib/sources.mpl.mlb @@ -0,0 +1,85 @@ +$(SML_LIB)/basis/basis.mlb +compat/mpl.mlb + +CommandLineArgs.sml +Util.sml +SeqBasis.sml + +SEQUENCE.sml +local + ArraySequence.sml +in + structure Seq = ArraySequence + structure ArraySequence +end + +Seqifier.sml +TFlatten.sml + +OffsetSearch.sml +STREAM.sml +DelayedStream.sml +RecursiveStream.sml +DelayedSeq.sml +OldDelayedSeq.sml + +FuncSequence.sml +BinarySearch.sml +Merge.sml +SeqifiedMerge.sml +FlattenMerge.sml +StableMerge.sml +DoubleBinarySearch.sml +StableMergeLowSpan.sml +StableSort.sml +Quicksort.sml +Mergesort.sml +TreeMatrix.sml +AugMap.sml + +PureSeq.sml + +SampleSort.sml +CountingSort.sml +RadixSort.sml +Shuffle.sml + +(* ReadFile.sml *) +Tokenize.sml +FindFirst.sml +Parse.sml +ParseFile.sml +(* TabFilterTree.sml *) +AdjacencyGraph.sml +AdjacencyInt.sml + +MatCOO.sml + +CheckSort.sml + +MkComplex.sml +Rat.sml +Geometry3D.sml +Geometry2D.sml +Topology2D.sml + +NearestNeighbors.sml + +Color.sml +PPM.sml +ExtraBinIO.sml +GIF.sml +NewWaveIO.sml +Signal.sml + +MeshToImage.sml + +MkGrep.sml + +Hashset.sml +Hashtable.sml + +ParFuncArray.sml +ChunkedTreap.sml + +Benchmark.sml From bb1e92275c6432049e07ca164b3bb2f8a524f7e4 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 13:57:48 -0400 Subject: [PATCH 03/18] Add benchmarking scripts from github.com/mpllang/parallel-ml-bench Copies over existing benchmarking scripts with appropriate modifications to their respective .mlb files. --- tests/bfs-delayed/MkBFS.sml | 47 ++ tests/bfs-delayed/SerialBFS.sml | 39 ++ tests/bfs-delayed/bfs-delayed.mlb | 4 + tests/bfs-delayed/main.sml | 72 +++ tests/bfs-det-dedup/Dedup.sml | 159 +++++++ tests/bfs-det-dedup/DedupBFS.sml | 147 ++++++ tests/bfs-det-dedup/OffsetSearch.sml | 54 +++ tests/bfs-det-dedup/SerialBFS.sml | 38 ++ tests/bfs-det-dedup/bfs-det-dedup.mlb | 6 + tests/bfs-det-dedup/main.sml | 77 ++++ tests/bfs-det-priority/OffsetSearch.sml | 54 +++ tests/bfs-det-priority/PriorityBFS.sml | 168 +++++++ tests/bfs-det-priority/SerialBFS.sml | 38 ++ tests/bfs-det-priority/bfs-det-priority.mlb | 5 + tests/bfs-det-priority/main.sml | 77 ++++ tests/bfs-tree-entangled-fixed/NondetBFS.sml | 206 +++++++++ .../bfs-tree-entangled-fixed/OffsetSearch.sml | 54 +++ tests/bfs-tree-entangled-fixed/SerialBFS.sml | 42 ++ .../bfs-tree-entangled-fixed.mlb | 5 + tests/bfs-tree-entangled-fixed/main.sml | 66 +++ tests/bfs-tree-entangled/NondetBFS.sml | 212 +++++++++ tests/bfs-tree-entangled/OffsetSearch.sml | 54 +++ tests/bfs-tree-entangled/SerialBFS.sml | 42 ++ .../bfs-tree-entangled/bfs-tree-entangled.mlb | 5 + tests/bfs-tree-entangled/main.sml | 66 +++ tests/bfs/NondetBFS.sml | 177 ++++++++ tests/bfs/OffsetSearch.sml | 54 +++ tests/bfs/SerialBFS.sml | 38 ++ tests/bfs/bfs.mlb | 5 + tests/bfs/main.sml | 77 ++++ tests/bignum-add-opt/Add.sml | 78 ++++ tests/bignum-add-opt/bignum-add-opt.mlb | 5 + tests/bignum-add-opt/main.sml | 36 ++ tests/bignum-add/Bignum.sml | 57 +++ tests/bignum-add/MkAdd.sml | 39 ++ tests/bignum-add/SequentialAdd.sml | 76 ++++ tests/bignum-add/bignum-add.mlb | 5 + tests/bignum-add/main.sml | 36 ++ tests/centrality/BC.sml | 333 ++++++++++++++ tests/centrality/OffsetSearch.sml | 54 +++ tests/centrality/centrality.mlb | 4 + tests/centrality/main.sml | 57 +++ tests/collect/CollectHash.sml | 30 ++ tests/collect/CollectSort.sml | 45 ++ tests/collect/HashTable.sml | 101 +++++ tests/collect/KEY.sml | 8 + tests/collect/VALUE.sml | 6 + tests/collect/collect.mlb | 7 + tests/collect/main.sml | 49 ++ tests/connectivity/Connectivity.sml | 28 ++ tests/connectivity/connectivity.mlb | 4 + tests/connectivity/main.sml | 73 +++ tests/dedup-entangled-fixed/NondetDedup.sml | 143 ++++++ .../dedup-entangled-fixed.mlb | 3 + tests/dedup-entangled/NondetDedup.sml | 141 ++++++ tests/dedup-entangled/dedup-entangled.mlb | 3 + tests/dedup/dedup.mlb | 2 + tests/dedup/dedup.sml | 216 +++++++++ .../DelaunayTriangulation.sml | 334 ++++++++++++++ tests/delaunay-animation/Split.sml | 73 +++ .../delaunay-animation/delaunay-animation.mlb | 4 + tests/delaunay-animation/main.sml | 222 +++++++++ tests/delaunay-animation/test.sml | 125 +++++ .../DelaunayTriangulation.sml | 191 ++++++++ .../delaunay-mostly-pure.mlb | 3 + tests/delaunay-mostly-pure/main.sml | 126 +++++ tests/delaunay-mostly-pure/test.sml | 125 +++++ .../DelaunayTriangulationTopDown.sml | 191 ++++++++ tests/delaunay-top-down/HashTable.sml | 190 ++++++++ tests/delaunay-top-down/README.md | 28 ++ tests/delaunay-top-down/delaunay-top-down.mlb | 4 + tests/delaunay-top-down/main.sml | 231 ++++++++++ tests/delaunay/DelaunayTriangulation.sml | 368 +++++++++++++++ tests/delaunay/Split.sml | 73 +++ tests/delaunay/delaunay.mlb | 4 + tests/delaunay/main.sml | 207 +++++++++ tests/delaunay/test.sml | 125 +++++ tests/dense-matmul/dense-matmul.mlb | 2 + tests/dense-matmul/main.sml | 29 ++ tests/fib/fib.mlb | 2 + tests/fib/fib.sml | 41 ++ tests/flatten/AllBSFlatten.sml | 19 + tests/flatten/BinarySearch.sml | 96 ++++ tests/flatten/BlockedAllBSFlatten.sml | 71 +++ tests/flatten/ExpandFlatten.sml | 102 +++++ tests/flatten/FullExpandPow2Flatten.sml | 132 ++++++ tests/flatten/MultiBlockedBSFlatten.sml | 105 +++++ tests/flatten/SimpleBlockedFlatten.sml | 66 +++ tests/flatten/SimpleExpandFlatten.sml | 58 +++ tests/flatten/flatten.mlb | 10 + tests/flatten/flatten.sml | 43 ++ tests/gif-encode/main.sml | 55 +++ tests/graphio/graphio.mlb | 2 + tests/graphio/main.sml | 21 + tests/grep-old/Grep.sml | 129 ++++++ tests/grep-old/grep-old.mlb | 3 + tests/grep-old/main.sml | 32 ++ tests/grep/grep.mlb | 2 + tests/grep/main.sml | 53 +++ tests/high-frag/high-frag.mlb | 2 + tests/high-frag/main.sml | 51 +++ tests/integrate-opt/Integrate.sml | 15 + tests/integrate-opt/integrate-opt.mlb | 3 + tests/integrate-opt/main.sml | 26 ++ tests/integrate/MkIntegrate.sml | 13 + tests/integrate/integrate.mlb | 3 + tests/integrate/main.sml | 39 ++ tests/interval-tree/IntervalTree.sml | 47 ++ tests/interval-tree/interval-tree.mlb | 3 + tests/interval-tree/main.sml | 87 ++++ tests/interval-tree/new-main.sml | 65 +++ tests/linearrec-opt/LinearRec.sml | 66 +++ tests/linearrec-opt/linearrec-opt.mlb | 3 + tests/linearrec-opt/main.sml | 21 + tests/linearrec/MkLinearRec.sml | 12 + tests/linearrec/linearrec.mlb | 3 + tests/linearrec/main.sml | 21 + tests/linefit-opt/LineFit.sml | 25 + tests/linefit-opt/linefit-opt.mlb | 3 + tests/linefit-opt/main.sml | 32 ++ tests/linefit/MkLineFit.sml | 21 + tests/linefit/linefit.mlb | 3 + tests/linefit/main.sml | 34 ++ tests/low-d-decomp/LDD.sml | 172 +++++++ tests/low-d-decomp/ldd-alt.sml | 132 ++++++ tests/low-d-decomp/low-d-decomp.mlb | 4 + tests/low-d-decomp/main.sml | 80 ++++ tests/max-indep-set/MIS.sml | 105 +++++ tests/max-indep-set/faa.mlton.sml | 11 + tests/max-indep-set/faa.mpl.sml | 5 + tests/max-indep-set/main.sml | 86 ++++ tests/max-indep-set/max-indep-set.mlb | 5 + tests/mcss-opt/MCSS.sml | 30 ++ tests/mcss-opt/main.sml | 20 + tests/mcss-opt/mcss-opt.mlb | 3 + tests/mcss/MkMapReduceMCSS.sml | 29 ++ tests/mcss/MkScanMCSS.sml | 24 + tests/mcss/main.sml | 18 + tests/mcss/mcss.mlb | 3 + tests/msort-int32/msort-int32.mlb | 2 + tests/msort-int32/msort.sml | 16 + tests/msort-strings/msort-strings.mlb | 2 + tests/msort-strings/msort.sml | 34 ++ tests/msort/msort.mlb | 2 + tests/msort/msort.sml | 17 + tests/nearest-nbrs/ParseFile.sml | 190 ++++++++ tests/nearest-nbrs/main.sml | 160 +++++++ tests/nearest-nbrs/nearest-nbrs.mlb | 3 + tests/nqueens-simple/main.sml | 104 +++++ tests/nqueens-simple/nqueens-simple.mlb | 2 + tests/nqueens/nqueens.mlb | 2 + tests/nqueens/nqueens.sml | 39 ++ tests/ocaml-binarytrees5/main.sml | 90 ++++ .../ocaml-binarytrees5/ocaml-binarytrees5.mlb | 2 + tests/ocaml-binarytrees5/ocaml-source.ml | 74 +++ tests/ocaml-game-of-life-pure/main.sml | 114 +++++ .../ocaml-game-of-life-pure.mlb | 2 + tests/ocaml-game-of-life/main.sml | 231 ++++++++++ .../ocaml-game-of-life/ocaml-game-of-life.mlb | 2 + tests/ocaml-game-of-life/ocaml-source.ml | 75 +++ tests/ocaml-lu-decomp/main.sml | 212 +++++++++ tests/ocaml-lu-decomp/ocaml-lu-decomp.mlb | 7 + tests/ocaml-lu-decomp/ocaml-source.ml | 62 +++ tests/ocaml-mandelbrot/main.sml | 187 ++++++++ tests/ocaml-mandelbrot/ocaml-mandelbrot.mlb | 2 + tests/ocaml-mandelbrot/ocaml-source.ml | 80 ++++ tests/ocaml-nbody-imm/README | 3 + tests/ocaml-nbody-imm/main.sml | 148 ++++++ tests/ocaml-nbody-imm/ocaml-nbody-imm.mlb | 7 + tests/ocaml-nbody-packed/README | 5 + tests/ocaml-nbody-packed/main.sml | 156 +++++++ .../ocaml-nbody-packed/ocaml-nbody-packed.mlb | 7 + tests/ocaml-nbody/main.sml | 128 ++++++ tests/ocaml-nbody/ocaml-nbody.mlb | 7 + tests/ocaml-nbody/ocaml-source.ml | 101 +++++ tests/palindrome/Pal.sml | 107 +++++ tests/palindrome/main.sml | 26 ++ tests/palindrome/palindrome.mlb | 3 + tests/parens/MkParens.sml | 31 ++ tests/parens/main.sml | 33 ++ tests/parens/parens.mlb | 3 + tests/primes-blocked/primes-blocked.mlb | 2 + tests/primes-blocked/primes-blocked.sml | 61 +++ tests/primes-segmented/SegmentedPrimes.sml | 104 +++++ tests/primes-segmented/TreeSeq.sml | 83 ++++ tests/primes-segmented/main.sml | 64 +++ tests/primes-segmented/primes-segmented.mlb | 4 + tests/primes/check-stack-dir/check-stack | Bin 0 -> 1296216 bytes tests/primes/check-stack-dir/primes.mlb | 5 + tests/primes/check-stack-dir/primes.sml | 37 ++ tests/primes/primes.mlb | 2 + tests/primes/primes.sml | 46 ++ tests/primes/safe/primes.mlb | 2 + tests/primes/safe/primes.sml | 47 ++ tests/pure-msort-int32/msort.sml | 37 ++ tests/pure-msort-int32/pure-msort-int32.mlb | 2 + tests/pure-msort-strings/msort.sml | 55 +++ .../pure-msort-strings/pure-msort-strings.mlb | 2 + tests/pure-msort/msort.sml | 35 ++ tests/pure-msort/pure-msort.mlb | 2 + tests/pure-nn/nn.sml | 420 +++++++++++++++++ tests/pure-nn/pure-nn.mlb | 2 + tests/pure-quickhull/Quickhull.sml | 120 +++++ tests/pure-quickhull/Split.sml | 126 +++++ tests/pure-quickhull/TreeSeq.sml | 54 +++ tests/pure-quickhull/main.sml | 58 +++ tests/pure-quickhull/pure-quickhull.mlb | 5 + tests/pure-skyline/CityGen.sml | 77 ++++ tests/pure-skyline/FastHashRand.sml | 66 +++ tests/pure-skyline/Skyline.sml | 56 +++ tests/pure-skyline/main.sml | 102 +++++ tests/pure-skyline/pure-skyline.mlb | 6 + tests/quickhull/MkOptSplit.sml | 123 +++++ tests/quickhull/MkPurishSplit.sml | 46 ++ tests/quickhull/MkQuickhull.sml | 109 +++++ tests/quickhull/ParseFile.sml | 190 ++++++++ tests/quickhull/Quickhull.sml | 119 +++++ tests/quickhull/Split.sml | 122 +++++ tests/quickhull/TreeSeq.sml | 54 +++ tests/quickhull/main.sml | 86 ++++ tests/quickhull/quickhull.mlb | 18 + tests/random/random.mlb | 2 + tests/random/random.sml | 33 ++ tests/range-tree/RangeTree.sml | 59 +++ tests/range-tree/main.sml | 103 +++++ tests/range-tree/range-tree.mlb | 3 + tests/raytracer/main.sml | 429 ++++++++++++++++++ tests/raytracer/raytracer.mlb | 2 + tests/reverb/main.sml | 26 ++ tests/reverb/reverb.mlb | 2 + tests/samplesort/main.sml | 16 + tests/samplesort/samplesort.mlb | 2 + tests/seam-carve-index/README | 7 + tests/seam-carve-index/SCI.sml | 192 ++++++++ .../seam-carve-index/VerticalSeamIndexMap.sml | 80 ++++ tests/seam-carve-index/main.sml | 122 +++++ tests/seam-carve-index/seam-carve-index.mlb | 4 + tests/seam-carve/SC.sml | 287 ++++++++++++ tests/seam-carve/main.sml | 29 ++ tests/seam-carve/seam-carve.mlb | 3 + tests/shuf/main.sml | 47 ++ tests/shuf/shuf.mlb | 2 + tests/skyline/CityGen.sml | 77 ++++ tests/skyline/FastHashRand.sml | 66 +++ tests/skyline/Skyline.sml | 60 +++ tests/skyline/main.sml | 102 +++++ tests/skyline/skyline.mlb | 6 + tests/spanner/Spanner.sml | 41 ++ tests/spanner/main.sml | 73 +++ tests/spanner/spanner.mlb | 4 + tests/sparse-mxv-opt/SparseMxV.sml | 14 + tests/sparse-mxv-opt/main.sml | 37 ++ tests/sparse-mxv-opt/sparse-mxv-opt.mlb | 3 + tests/sparse-mxv/MkMXV.sml | 16 + tests/sparse-mxv/main.sml | 37 ++ tests/sparse-mxv/sparse-mxv.mlb | 3 + tests/subset-sum/SubsetSumTiled.sml | 100 ++++ tests/subset-sum/main.sml | 36 ++ tests/subset-sum/subset-sum.mlb | 3 + tests/suffix-array/AS.sml | 8 + tests/suffix-array/BruteForce.sml | 29 ++ tests/suffix-array/PrefixDoubling.sml | 256 +++++++++++ tests/suffix-array/main.sml | 104 +++++ tests/suffix-array/suffix-array.mlb | 5 + tests/tape-delay/main.sml | 34 ++ tests/tape-delay/tape-delay.mlb | 2 + tests/tinykaboom/TinyKaboom.sml | 142 ++++++ tests/tinykaboom/f32.sml | 7 + tests/tinykaboom/main.sml | 102 +++++ tests/tinykaboom/tinykaboom.mlb | 5 + tests/tinykaboom/vec3.sml | 20 + tests/to-gif/main.sml | 39 ++ tests/to-gif/to-gif.mlb | 2 + tests/tokens/tokens.mlb | 2 + tests/tokens/tokens.sml | 48 ++ tests/triangle-count/TriangleCount.sml | 125 +++++ tests/triangle-count/main.sml | 71 +++ tests/triangle-count/triangle-count.mlb | 3 + tests/wc-opt/WC.sml | 51 +++ tests/wc-opt/main.sml | 31 ++ tests/wc-opt/wc-opt.mlb | 3 + tests/wc/MkWC.sml | 45 ++ tests/wc/main.sml | 33 ++ tests/wc/wc.mlb | 3 + 284 files changed, 17074 insertions(+) create mode 100644 tests/bfs-delayed/MkBFS.sml create mode 100644 tests/bfs-delayed/SerialBFS.sml create mode 100644 tests/bfs-delayed/bfs-delayed.mlb create mode 100644 tests/bfs-delayed/main.sml create mode 100644 tests/bfs-det-dedup/Dedup.sml create mode 100644 tests/bfs-det-dedup/DedupBFS.sml create mode 100644 tests/bfs-det-dedup/OffsetSearch.sml create mode 100644 tests/bfs-det-dedup/SerialBFS.sml create mode 100644 tests/bfs-det-dedup/bfs-det-dedup.mlb create mode 100644 tests/bfs-det-dedup/main.sml create mode 100644 tests/bfs-det-priority/OffsetSearch.sml create mode 100644 tests/bfs-det-priority/PriorityBFS.sml create mode 100644 tests/bfs-det-priority/SerialBFS.sml create mode 100644 tests/bfs-det-priority/bfs-det-priority.mlb create mode 100644 tests/bfs-det-priority/main.sml create mode 100644 tests/bfs-tree-entangled-fixed/NondetBFS.sml create mode 100644 tests/bfs-tree-entangled-fixed/OffsetSearch.sml create mode 100644 tests/bfs-tree-entangled-fixed/SerialBFS.sml create mode 100644 tests/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb create mode 100644 tests/bfs-tree-entangled-fixed/main.sml create mode 100644 tests/bfs-tree-entangled/NondetBFS.sml create mode 100644 tests/bfs-tree-entangled/OffsetSearch.sml create mode 100644 tests/bfs-tree-entangled/SerialBFS.sml create mode 100644 tests/bfs-tree-entangled/bfs-tree-entangled.mlb create mode 100644 tests/bfs-tree-entangled/main.sml create mode 100644 tests/bfs/NondetBFS.sml create mode 100644 tests/bfs/OffsetSearch.sml create mode 100644 tests/bfs/SerialBFS.sml create mode 100644 tests/bfs/bfs.mlb create mode 100644 tests/bfs/main.sml create mode 100644 tests/bignum-add-opt/Add.sml create mode 100644 tests/bignum-add-opt/bignum-add-opt.mlb create mode 100644 tests/bignum-add-opt/main.sml create mode 100644 tests/bignum-add/Bignum.sml create mode 100644 tests/bignum-add/MkAdd.sml create mode 100644 tests/bignum-add/SequentialAdd.sml create mode 100644 tests/bignum-add/bignum-add.mlb create mode 100644 tests/bignum-add/main.sml create mode 100644 tests/centrality/BC.sml create mode 100644 tests/centrality/OffsetSearch.sml create mode 100644 tests/centrality/centrality.mlb create mode 100644 tests/centrality/main.sml create mode 100644 tests/collect/CollectHash.sml create mode 100644 tests/collect/CollectSort.sml create mode 100644 tests/collect/HashTable.sml create mode 100644 tests/collect/KEY.sml create mode 100644 tests/collect/VALUE.sml create mode 100644 tests/collect/collect.mlb create mode 100644 tests/collect/main.sml create mode 100644 tests/connectivity/Connectivity.sml create mode 100644 tests/connectivity/connectivity.mlb create mode 100644 tests/connectivity/main.sml create mode 100644 tests/dedup-entangled-fixed/NondetDedup.sml create mode 100644 tests/dedup-entangled-fixed/dedup-entangled-fixed.mlb create mode 100644 tests/dedup-entangled/NondetDedup.sml create mode 100644 tests/dedup-entangled/dedup-entangled.mlb create mode 100644 tests/dedup/dedup.mlb create mode 100644 tests/dedup/dedup.sml create mode 100644 tests/delaunay-animation/DelaunayTriangulation.sml create mode 100644 tests/delaunay-animation/Split.sml create mode 100644 tests/delaunay-animation/delaunay-animation.mlb create mode 100644 tests/delaunay-animation/main.sml create mode 100644 tests/delaunay-animation/test.sml create mode 100644 tests/delaunay-mostly-pure/DelaunayTriangulation.sml create mode 100644 tests/delaunay-mostly-pure/delaunay-mostly-pure.mlb create mode 100644 tests/delaunay-mostly-pure/main.sml create mode 100644 tests/delaunay-mostly-pure/test.sml create mode 100644 tests/delaunay-top-down/DelaunayTriangulationTopDown.sml create mode 100644 tests/delaunay-top-down/HashTable.sml create mode 100644 tests/delaunay-top-down/README.md create mode 100644 tests/delaunay-top-down/delaunay-top-down.mlb create mode 100644 tests/delaunay-top-down/main.sml create mode 100644 tests/delaunay/DelaunayTriangulation.sml create mode 100644 tests/delaunay/Split.sml create mode 100644 tests/delaunay/delaunay.mlb create mode 100644 tests/delaunay/main.sml create mode 100644 tests/delaunay/test.sml create mode 100644 tests/dense-matmul/dense-matmul.mlb create mode 100644 tests/dense-matmul/main.sml create mode 100644 tests/fib/fib.mlb create mode 100644 tests/fib/fib.sml create mode 100644 tests/flatten/AllBSFlatten.sml create mode 100644 tests/flatten/BinarySearch.sml create mode 100644 tests/flatten/BlockedAllBSFlatten.sml create mode 100644 tests/flatten/ExpandFlatten.sml create mode 100644 tests/flatten/FullExpandPow2Flatten.sml create mode 100644 tests/flatten/MultiBlockedBSFlatten.sml create mode 100644 tests/flatten/SimpleBlockedFlatten.sml create mode 100644 tests/flatten/SimpleExpandFlatten.sml create mode 100644 tests/flatten/flatten.mlb create mode 100644 tests/flatten/flatten.sml create mode 100644 tests/gif-encode/main.sml create mode 100644 tests/graphio/graphio.mlb create mode 100644 tests/graphio/main.sml create mode 100644 tests/grep-old/Grep.sml create mode 100644 tests/grep-old/grep-old.mlb create mode 100644 tests/grep-old/main.sml create mode 100644 tests/grep/grep.mlb create mode 100644 tests/grep/main.sml create mode 100644 tests/high-frag/high-frag.mlb create mode 100644 tests/high-frag/main.sml create mode 100644 tests/integrate-opt/Integrate.sml create mode 100644 tests/integrate-opt/integrate-opt.mlb create mode 100644 tests/integrate-opt/main.sml create mode 100644 tests/integrate/MkIntegrate.sml create mode 100644 tests/integrate/integrate.mlb create mode 100644 tests/integrate/main.sml create mode 100644 tests/interval-tree/IntervalTree.sml create mode 100644 tests/interval-tree/interval-tree.mlb create mode 100644 tests/interval-tree/main.sml create mode 100644 tests/interval-tree/new-main.sml create mode 100644 tests/linearrec-opt/LinearRec.sml create mode 100644 tests/linearrec-opt/linearrec-opt.mlb create mode 100644 tests/linearrec-opt/main.sml create mode 100644 tests/linearrec/MkLinearRec.sml create mode 100644 tests/linearrec/linearrec.mlb create mode 100644 tests/linearrec/main.sml create mode 100644 tests/linefit-opt/LineFit.sml create mode 100644 tests/linefit-opt/linefit-opt.mlb create mode 100644 tests/linefit-opt/main.sml create mode 100644 tests/linefit/MkLineFit.sml create mode 100644 tests/linefit/linefit.mlb create mode 100644 tests/linefit/main.sml create mode 100644 tests/low-d-decomp/LDD.sml create mode 100644 tests/low-d-decomp/ldd-alt.sml create mode 100644 tests/low-d-decomp/low-d-decomp.mlb create mode 100644 tests/low-d-decomp/main.sml create mode 100644 tests/max-indep-set/MIS.sml create mode 100644 tests/max-indep-set/faa.mlton.sml create mode 100644 tests/max-indep-set/faa.mpl.sml create mode 100644 tests/max-indep-set/main.sml create mode 100644 tests/max-indep-set/max-indep-set.mlb create mode 100644 tests/mcss-opt/MCSS.sml create mode 100644 tests/mcss-opt/main.sml create mode 100644 tests/mcss-opt/mcss-opt.mlb create mode 100644 tests/mcss/MkMapReduceMCSS.sml create mode 100644 tests/mcss/MkScanMCSS.sml create mode 100644 tests/mcss/main.sml create mode 100644 tests/mcss/mcss.mlb create mode 100644 tests/msort-int32/msort-int32.mlb create mode 100644 tests/msort-int32/msort.sml create mode 100644 tests/msort-strings/msort-strings.mlb create mode 100644 tests/msort-strings/msort.sml create mode 100644 tests/msort/msort.mlb create mode 100644 tests/msort/msort.sml create mode 100644 tests/nearest-nbrs/ParseFile.sml create mode 100644 tests/nearest-nbrs/main.sml create mode 100644 tests/nearest-nbrs/nearest-nbrs.mlb create mode 100644 tests/nqueens-simple/main.sml create mode 100644 tests/nqueens-simple/nqueens-simple.mlb create mode 100644 tests/nqueens/nqueens.mlb create mode 100644 tests/nqueens/nqueens.sml create mode 100644 tests/ocaml-binarytrees5/main.sml create mode 100644 tests/ocaml-binarytrees5/ocaml-binarytrees5.mlb create mode 100644 tests/ocaml-binarytrees5/ocaml-source.ml create mode 100644 tests/ocaml-game-of-life-pure/main.sml create mode 100644 tests/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb create mode 100644 tests/ocaml-game-of-life/main.sml create mode 100644 tests/ocaml-game-of-life/ocaml-game-of-life.mlb create mode 100644 tests/ocaml-game-of-life/ocaml-source.ml create mode 100644 tests/ocaml-lu-decomp/main.sml create mode 100644 tests/ocaml-lu-decomp/ocaml-lu-decomp.mlb create mode 100644 tests/ocaml-lu-decomp/ocaml-source.ml create mode 100644 tests/ocaml-mandelbrot/main.sml create mode 100644 tests/ocaml-mandelbrot/ocaml-mandelbrot.mlb create mode 100644 tests/ocaml-mandelbrot/ocaml-source.ml create mode 100644 tests/ocaml-nbody-imm/README create mode 100644 tests/ocaml-nbody-imm/main.sml create mode 100644 tests/ocaml-nbody-imm/ocaml-nbody-imm.mlb create mode 100644 tests/ocaml-nbody-packed/README create mode 100644 tests/ocaml-nbody-packed/main.sml create mode 100644 tests/ocaml-nbody-packed/ocaml-nbody-packed.mlb create mode 100644 tests/ocaml-nbody/main.sml create mode 100644 tests/ocaml-nbody/ocaml-nbody.mlb create mode 100644 tests/ocaml-nbody/ocaml-source.ml create mode 100644 tests/palindrome/Pal.sml create mode 100644 tests/palindrome/main.sml create mode 100644 tests/palindrome/palindrome.mlb create mode 100644 tests/parens/MkParens.sml create mode 100644 tests/parens/main.sml create mode 100644 tests/parens/parens.mlb create mode 100644 tests/primes-blocked/primes-blocked.mlb create mode 100644 tests/primes-blocked/primes-blocked.sml create mode 100644 tests/primes-segmented/SegmentedPrimes.sml create mode 100644 tests/primes-segmented/TreeSeq.sml create mode 100644 tests/primes-segmented/main.sml create mode 100644 tests/primes-segmented/primes-segmented.mlb create mode 100755 tests/primes/check-stack-dir/check-stack create mode 100644 tests/primes/check-stack-dir/primes.mlb create mode 100644 tests/primes/check-stack-dir/primes.sml create mode 100644 tests/primes/primes.mlb create mode 100644 tests/primes/primes.sml create mode 100644 tests/primes/safe/primes.mlb create mode 100644 tests/primes/safe/primes.sml create mode 100644 tests/pure-msort-int32/msort.sml create mode 100644 tests/pure-msort-int32/pure-msort-int32.mlb create mode 100644 tests/pure-msort-strings/msort.sml create mode 100644 tests/pure-msort-strings/pure-msort-strings.mlb create mode 100644 tests/pure-msort/msort.sml create mode 100644 tests/pure-msort/pure-msort.mlb create mode 100644 tests/pure-nn/nn.sml create mode 100644 tests/pure-nn/pure-nn.mlb create mode 100644 tests/pure-quickhull/Quickhull.sml create mode 100644 tests/pure-quickhull/Split.sml create mode 100644 tests/pure-quickhull/TreeSeq.sml create mode 100644 tests/pure-quickhull/main.sml create mode 100644 tests/pure-quickhull/pure-quickhull.mlb create mode 100644 tests/pure-skyline/CityGen.sml create mode 100644 tests/pure-skyline/FastHashRand.sml create mode 100644 tests/pure-skyline/Skyline.sml create mode 100644 tests/pure-skyline/main.sml create mode 100644 tests/pure-skyline/pure-skyline.mlb create mode 100644 tests/quickhull/MkOptSplit.sml create mode 100644 tests/quickhull/MkPurishSplit.sml create mode 100644 tests/quickhull/MkQuickhull.sml create mode 100644 tests/quickhull/ParseFile.sml create mode 100644 tests/quickhull/Quickhull.sml create mode 100644 tests/quickhull/Split.sml create mode 100644 tests/quickhull/TreeSeq.sml create mode 100644 tests/quickhull/main.sml create mode 100644 tests/quickhull/quickhull.mlb create mode 100644 tests/random/random.mlb create mode 100644 tests/random/random.sml create mode 100644 tests/range-tree/RangeTree.sml create mode 100644 tests/range-tree/main.sml create mode 100644 tests/range-tree/range-tree.mlb create mode 100644 tests/raytracer/main.sml create mode 100644 tests/raytracer/raytracer.mlb create mode 100644 tests/reverb/main.sml create mode 100644 tests/reverb/reverb.mlb create mode 100644 tests/samplesort/main.sml create mode 100644 tests/samplesort/samplesort.mlb create mode 100644 tests/seam-carve-index/README create mode 100644 tests/seam-carve-index/SCI.sml create mode 100644 tests/seam-carve-index/VerticalSeamIndexMap.sml create mode 100644 tests/seam-carve-index/main.sml create mode 100644 tests/seam-carve-index/seam-carve-index.mlb create mode 100644 tests/seam-carve/SC.sml create mode 100644 tests/seam-carve/main.sml create mode 100644 tests/seam-carve/seam-carve.mlb create mode 100644 tests/shuf/main.sml create mode 100644 tests/shuf/shuf.mlb create mode 100644 tests/skyline/CityGen.sml create mode 100644 tests/skyline/FastHashRand.sml create mode 100644 tests/skyline/Skyline.sml create mode 100644 tests/skyline/main.sml create mode 100644 tests/skyline/skyline.mlb create mode 100644 tests/spanner/Spanner.sml create mode 100644 tests/spanner/main.sml create mode 100644 tests/spanner/spanner.mlb create mode 100644 tests/sparse-mxv-opt/SparseMxV.sml create mode 100644 tests/sparse-mxv-opt/main.sml create mode 100644 tests/sparse-mxv-opt/sparse-mxv-opt.mlb create mode 100644 tests/sparse-mxv/MkMXV.sml create mode 100644 tests/sparse-mxv/main.sml create mode 100644 tests/sparse-mxv/sparse-mxv.mlb create mode 100644 tests/subset-sum/SubsetSumTiled.sml create mode 100644 tests/subset-sum/main.sml create mode 100644 tests/subset-sum/subset-sum.mlb create mode 100644 tests/suffix-array/AS.sml create mode 100644 tests/suffix-array/BruteForce.sml create mode 100644 tests/suffix-array/PrefixDoubling.sml create mode 100644 tests/suffix-array/main.sml create mode 100644 tests/suffix-array/suffix-array.mlb create mode 100644 tests/tape-delay/main.sml create mode 100644 tests/tape-delay/tape-delay.mlb create mode 100644 tests/tinykaboom/TinyKaboom.sml create mode 100644 tests/tinykaboom/f32.sml create mode 100644 tests/tinykaboom/main.sml create mode 100644 tests/tinykaboom/tinykaboom.mlb create mode 100644 tests/tinykaboom/vec3.sml create mode 100644 tests/to-gif/main.sml create mode 100644 tests/to-gif/to-gif.mlb create mode 100644 tests/tokens/tokens.mlb create mode 100644 tests/tokens/tokens.sml create mode 100644 tests/triangle-count/TriangleCount.sml create mode 100644 tests/triangle-count/main.sml create mode 100644 tests/triangle-count/triangle-count.mlb create mode 100644 tests/wc-opt/WC.sml create mode 100644 tests/wc-opt/main.sml create mode 100644 tests/wc-opt/wc-opt.mlb create mode 100644 tests/wc/MkWC.sml create mode 100644 tests/wc/main.sml create mode 100644 tests/wc/wc.mlb diff --git a/tests/bfs-delayed/MkBFS.sml b/tests/bfs-delayed/MkBFS.sml new file mode 100644 index 000000000..e564bafd3 --- /dev/null +++ b/tests/bfs-delayed/MkBFS.sml @@ -0,0 +1,47 @@ +functor MkBFS (Seq: SEQUENCE) = +struct + + structure G = AdjacencyGraph(Int) + + fun bfs graph source = + let + val N = G.numVertices graph + val M = G.numEdges graph + + fun outEdges u = + Seq.map (fn v => (u, v)) (Seq.fromArraySeq (G.neighbors graph u)) + + val parents = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => + Array.update (parents, i, ~1)) + + fun isVisited v = + Array.sub (parents, v) <> ~1 + + fun visit (u, v) = + if not (isVisited v) andalso + (~1 = Concurrency.casArray (parents, v) (~1, u)) + then + SOME v + else + NONE + + fun loop frontier totalVisited = + if Seq.length frontier = 0 then + totalVisited + else + let + val allNeighbors = Seq.flatten (Seq.map outEdges frontier) + val nextFrontier = Seq.mapOption visit allNeighbors + in + loop nextFrontier (totalVisited + Seq.length nextFrontier) + end + + val _ = Array.update (parents, source, source) + val initFrontier = Seq.singleton source + val numVisited = loop initFrontier 1 + in + ArraySlice.full parents + end + +end diff --git a/tests/bfs-delayed/SerialBFS.sml b/tests/bfs-delayed/SerialBFS.sml new file mode 100644 index 000000000..347559a3e --- /dev/null +++ b/tests/bfs-delayed/SerialBFS.sml @@ -0,0 +1,39 @@ +structure SerialBFS = +struct + + structure Seq = ArraySequence + structure G = AdjacencyGraph(Int) + + fun bfs g s = + let + fun neighbors v = G.neighbors g v + fun degree v = G.degree g v + + val n = G.numVertices g + val m = G.numEdges g + + val queue = ForkJoin.alloc (m+1) + val parents = Array.array (n, ~1) + + fun search (lo, hi) = + if lo >= hi then lo else + let + val v = Array.sub (queue, lo) + fun visit (hi', u) = + if Array.sub (parents, u) >= 0 then hi' + else ( Array.update (parents, u, v) + ; Array.update (queue, hi', u) + ; hi'+1 + ) + in + search (lo+1, Seq.iterate visit hi (neighbors v)) + end + + val _ = Array.update (parents, s, s) + val _ = Array.update (queue, 0, s) + val numVisited = search (0, 1) + in + ArraySlice.full parents + end + +end diff --git a/tests/bfs-delayed/bfs-delayed.mlb b/tests/bfs-delayed/bfs-delayed.mlb new file mode 100644 index 000000000..80dfe0fe6 --- /dev/null +++ b/tests/bfs-delayed/bfs-delayed.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +SerialBFS.sml +MkBFS.sml +main.sml diff --git a/tests/bfs-delayed/main.sml b/tests/bfs-delayed/main.sml new file mode 100644 index 000000000..ef63bf1b8 --- /dev/null +++ b/tests/bfs-delayed/main.sml @@ -0,0 +1,72 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence +structure G = AdjacencyGraph(Int) + +(* Set by subdirectory *) +structure BFS = MkBFS(OldDelayedSeq) + +(* Generate an input + * If -infile is given, then will load file. + * Otherwise, uses -n -d to generate a random graph. *) +val filename = CLA.parseString "infile" "" +val t0 = Time.now () +val (graphspec, input) = + if filename <> "" then + (filename, G.parseFile filename) + else + let + val n = CLA.parseInt "n" 1000000 + val d = CLA.parseInt "d" 10 + in + ("random(" ^ Int.toString n ^ "," ^ Int.toString d ^ ")", + G.randSymmGraph n d) + end +val t1 = Time.now () +val _ = print ("loaded graph in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val n = G.numVertices input +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +val _ = print ("graph " ^ graphspec ^ "\n") +val _ = print ("num-verts " ^ Int.toString n ^ "\n") +val _ = print ("num-edges " ^ Int.toString (G.numEdges input) ^ "\n") +val _ = print ("source " ^ Int.toString source ^ "\n") +val _ = print ("check " ^ (if doCheck then "true" else "false") ^ "\n") + +fun task () = + BFS.bfs input source + +val P = Benchmark.run "running bfs" task + +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices input) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs input source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () diff --git a/tests/bfs-det-dedup/Dedup.sml b/tests/bfs-det-dedup/Dedup.sml new file mode 100644 index 000000000..2c6375ebd --- /dev/null +++ b/tests/bfs-det-dedup/Dedup.sml @@ -0,0 +1,159 @@ +structure Dedup: +sig + val dedup: ('k * 'k -> bool) (* equality check *) + -> ('k -> Word64.word) (* first hash function *) + -> ('k -> Word64.word) (* second hash function *) + -> 'k Seq.t (* input (with duplicates) *) + -> 'k Seq.t (* deduplicated (not sorted!) *) +end = +struct + + structure A = Array + structure AS = ArraySlice + val update = Array.update + val sub = Array.sub + + fun chunkedfor chunkSize (flo, fhi) f = + let + val n = fhi - flo + val numChunks = (n-1) div chunkSize + 1 + in + Util.for (0, numChunks) (fn i => + let + val clo = flo + i*chunkSize + val chi = if i = numChunks - 1 then fhi else flo + (i+1)*chunkSize + in + Util.for (clo, chi) f + end) + end + + fun chunkedloop chunkSize (flo, fhi) init f = + let + val n = fhi - flo + val numChunks = (n-1) div chunkSize + 1 + in + Util.loop (0, numChunks) init (fn (b, i) => + let + val clo = flo + i*chunkSize + val chi = if i = numChunks - 1 then fhi else flo + (i+1)*chunkSize + val b' = Util.loop (clo, chi) b f + in + b' + end) + end + + datatype 'a bucketTree = + Leaf of 'a array + | Node of int * 'a bucketTree * 'a bucketTree + + fun count t = + case t of + Leaf a => A.length a + | Node (c, _, _) => c + + fun bucketTree n (f : int -> 'a array) = + let + fun tree (lo, hi) = + case hi - lo of + 0 => Leaf (ForkJoin.alloc 0) + | 1 => Leaf (f lo) + | n => let val mid = lo + n div 2 + val (l, r) = ForkJoin.par (fn _ => tree (lo, mid), fn _ => tree (mid, hi)) + in Node (count l + count r, l, r) + end + in + tree (0, n) + end + + fun indexApp chunkSize (f : (int * 'a) -> unit) (t : 'a bucketTree) = + let + fun app offset t = + case t of + Leaf a => chunkedfor chunkSize (0, A.length a) (fn i => f (offset+i, sub (a, i))) + | Node (_, l, r) => + (ForkJoin.par (fn _ => app offset l, fn _ => app (offset + count l) r); + ()) + in + app 0 t + end + + fun compactFilter chunkSize (s : 'a option array) count = + let + val t = ForkJoin.alloc count + val _ = chunkedloop chunkSize (0, A.length s) 0 (fn (ti, si) => + case sub (s, si) of + NONE => ti + | SOME x => (update (t, ti, x); ti+1)) + in + t + end + + fun serialHistogram eq hash s = + let + val n = AS.length s + val tn = Util.boundPow2 n + val tmask = Word64.fromInt (tn - 1) + val t = Array.array (tn, NONE) + + fun insert k = + let + fun probe i = + case sub (t, i) of + NONE => (update (t, i, SOME k); true) + | SOME k' => + if eq (k', k) then + false + else if i+1 = tn then + probe 0 + else + probe (i+1) + val h = Word64.toInt (Word64.andb (hash k, tmask)) + in + probe h + end + + val (sa, slo, sn) = AS.base s + val shi = slo+sn + val count = chunkedloop 1024 (slo, shi) 0 (fn (c, i) => + if insert (sub (sa, i)) + then c+1 + else c) + in + compactFilter 1024 t count + end + + + (* val dedup : ('k * 'k -> bool) equality check + -> ('k -> Word64.word) first hash function + -> ('k -> Word64.word) second hash function + -> 'k seq input (with duplicates) + -> 'k seq deduplicated (not sorted!) + *) + fun dedup eq hash hash' keys = + if AS.length keys = 0 then Seq.empty () else + let + val n = AS.length keys + val bucketBits = + if n < Util.pow2 27 + then (Util.log2 n - 7) div 2 + else Util.log2 n - 17 + val numBuckets = Util.pow2 (bucketBits + 1) + val bucketMask = Word64.fromInt (numBuckets - 1) + fun getBucket k = Word64.toInt (Word64.andb (hash k, bucketMask)) + fun ithKeyBucket i = getBucket (Seq.nth keys i) + val (bucketed, offsets) = CountingSort.sort keys ithKeyBucket numBuckets + fun offset i = Seq.nth offsets i + val tree = bucketTree numBuckets (fn i => + let + val bucketks = Seq.subseq bucketed (offset i, offset (i+1) - offset i) + in + serialHistogram eq hash' bucketks + end) + + val result = ForkJoin.alloc (count tree) + val _ = indexApp 1024 (fn (i, x) => update (result, i, x)) tree + in + AS.full result + end + +end \ No newline at end of file diff --git a/tests/bfs-det-dedup/DedupBFS.sml b/tests/bfs-det-dedup/DedupBFS.sml new file mode 100644 index 000000000..eb91438df --- /dev/null +++ b/tests/bfs-det-dedup/DedupBFS.sml @@ -0,0 +1,147 @@ +structure DedupBFS = +struct + type 'a seq = 'a Seq.t + + (* structure DS = DelayedSeq *) + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + + type vertex = G.vertex + + val sub = Array.sub + val upd = Array.update + + val vtoi = V.toInt + val itov = V.fromInt + + (* fun ASsub s = + let val (a, i, _) = ArraySlice.base s + in sub (a, i+s) + end *) + + val GRAIN = 10000 + + fun strip s = + let val (s', start, _) = ArraySlice.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun bfs {diropt: bool} (g : G.graph) (s : vertex) = + let + val n = G.numVertices g + val parent = strip (Seq.tabulate (fn _ => ~1) n) + + (* Choose method of filtering the frontier: either frontier always + * only consists of valid vertex ids, or it allows invalid vertices and + * pretends that these vertices are isolated. *) + fun degree v = G.degree g v + fun filterFrontier s = Seq.filter (fn x => x <> itov (~1)) s + (* + fun degree v = if v < 0 then 0 else Graph.degree g v + fun filterFrontier s = s + *) + + val denseThreshold = G.numEdges g div 20 + + fun sumOfOutDegrees frontier = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length frontier) (degree o Seq.nth frontier) + (* DS.reduce op+ 0 (DS.map degree (DS.fromArraySeq frontier)) *) + + fun shouldProcessDense frontier = + diropt andalso + let + val n = Seq.length frontier + val m = sumOfOutDegrees frontier + in + n + m > denseThreshold + end + + fun bottomUp frontier = + raise Fail "DedupBFS: direction optimization not implemented yet" + + fun topDown frontier = + let + val nf = Seq.length frontier + val offsets = SeqBasis.scan GRAIN op+ 0 (0, nf) (degree o Seq.nth frontier) + val mf = sub (offsets, nf) + val outNbrs: (vertex * vertex) array = ForkJoin.alloc mf + + fun visitNeighbors offset v nghs = + Util.for (0, Seq.length nghs) (fn i => + let val u = Seq.nth nghs i + in upd (outNbrs, offset+i, (v, u)) + end) + + fun visitMany offlo lo hi = + if lo = hi then () else + let + val v = Seq.nth frontier offlo + val voffset = sub (offsets, offlo) + val k = Int.min (hi - lo, sub (offsets, offlo+1) - lo) + in + if k = 0 then visitMany (offlo+1) lo hi + else ( visitNeighbors lo v (Seq.subseq (G.neighbors g v) (lo - voffset, k)) + ; visitMany (offlo+1) (lo+k) hi + ) + end + + fun parVisitMany (offlo, offhi) (lo, hi) = + if hi - lo <= GRAIN then + visitMany offlo lo hi + else + let + val mid = lo + (hi - lo) div 2 + val (i, j) = OffsetSearch.search mid offsets (offlo, offhi) + val _ = ForkJoin.par + ( fn _ => parVisitMany (offlo, i) (lo, mid) + , fn _ => parVisitMany (j-1, offhi) (mid, hi) + ) + in + () + end + + val vtow = Word64.fromInt o vtoi + fun h1 w = Word64.>> (Util.hash64_2 w, 0w32) + fun h2 w = Util.hash64_2 w + + (* populates outNbrs *) + val _ = parVisitMany (0, nf + 1) (0, mf) + val outNbrs = ArraySlice.full outNbrs + val unvisited = Seq.filter (fn (_, u) => sub (parent, u) = ~1) outNbrs + val deduped = Dedup.dedup + (fn ((_, u1), (_, u2)) => u1 = u2) + (fn (_, u) => h1 (vtow u)) + (fn (_, u) => h2 (vtow u)) + unvisited + + val nextFrontier = + Seq.map (fn (v, u) => (upd (parent, vtoi u, v); u)) deduped + in + nextFrontier + end + + fun search frontier = + if Seq.length frontier = 0 then + () + else if shouldProcessDense frontier then + let + val (nextFrontier, tm) = Util.getTime (fn _ => bottomUp frontier) + in + print ("dense " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + else + let + val (nextFrontier, tm) = Util.getTime (fn _ => topDown frontier) + in + print ("sparse " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + + val _ = upd (parent, vtoi s, s) + val _ = search (Seq.fromList [s]) + in + ArraySlice.full parent + end + +end diff --git a/tests/bfs-det-dedup/OffsetSearch.sml b/tests/bfs-det-dedup/OffsetSearch.sml new file mode 100644 index 000000000..7e17febb8 --- /dev/null +++ b/tests/bfs-det-dedup/OffsetSearch.sml @@ -0,0 +1,54 @@ +structure OffsetSearch :> +sig + (* `search x xs (lo, hi)` searches the sorted array `xs` between indices `lo` + * and `hi`, returning `(i, j)` where `i-lo` is the number of elements that + * are strictly less than `x`, and `j-i` is the number of elements which are + * equal to `x`. *) + val search : int -> int array -> int * int -> int * int +end = +struct + + val sub = Array.sub + val upd = Array.update + + fun lowSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => lowSearch x xs (lo, mid) + | GREATER => lowSearch x xs (mid + 1, hi) + | EQUAL => lowSearchEq x xs (lo, mid) + end + + and lowSearchEq x xs (lo, mid) = + if (mid = 0) orelse (x > sub (xs, mid-1)) + then mid + else lowSearch x xs (lo, mid) + + and highSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => highSearch x xs (lo, mid) + | GREATER => highSearch x xs (mid + 1, hi) + | EQUAL => highSearchEq x xs (mid, hi) + end + + and highSearchEq x xs (mid, hi) = + if (mid = Array.length xs - 1) orelse (x < sub (xs, mid + 1)) + then mid + 1 + else highSearch x xs (mid + 1, hi) + + and search (x : int) (xs : int array) (lo, hi) : int * int = + case hi - lo of + 0 => (lo, lo) + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => search x xs (lo, mid) + | GREATER => search x xs (mid + 1, hi) + | EQUAL => (lowSearchEq x xs (lo, mid), highSearchEq x xs (mid, hi)) + end + +end diff --git a/tests/bfs-det-dedup/SerialBFS.sml b/tests/bfs-det-dedup/SerialBFS.sml new file mode 100644 index 000000000..60a010386 --- /dev/null +++ b/tests/bfs-det-dedup/SerialBFS.sml @@ -0,0 +1,38 @@ +structure SerialBFS = +struct + + structure G = AdjacencyGraph(Int) + + fun bfs g s = + let + fun neighbors v = G.neighbors g v + fun degree v = G.degree g v + + val n = G.numVertices g + val m = G.numEdges g + + val queue = ForkJoin.alloc (m+1) + val parents = Array.array (n, ~1) + + fun search (lo, hi) = + if lo >= hi then lo else + let + val v = Array.sub (queue, lo) + fun visit (hi', u) = + if Array.sub (parents, u) >= 0 then hi' + else ( Array.update (parents, u, v) + ; Array.update (queue, hi', u) + ; hi'+1 + ) + in + search (lo+1, Seq.iterate visit hi (neighbors v)) + end + + val _ = Array.update (parents, s, s) + val _ = Array.update (queue, 0, s) + val numVisited = search (0, 1) + in + ArraySlice.full parents + end + +end diff --git a/tests/bfs-det-dedup/bfs-det-dedup.mlb b/tests/bfs-det-dedup/bfs-det-dedup.mlb new file mode 100644 index 000000000..95aaeaa70 --- /dev/null +++ b/tests/bfs-det-dedup/bfs-det-dedup.mlb @@ -0,0 +1,6 @@ +../mpllib/sources.$(COMPAT).mlb +Dedup.sml +SerialBFS.sml +OffsetSearch.sml +DedupBFS.sml +main.sml diff --git a/tests/bfs-det-dedup/main.sml b/tests/bfs-det-dedup/main.sml new file mode 100644 index 000000000..e137dd218 --- /dev/null +++ b/tests/bfs-det-dedup/main.sml @@ -0,0 +1,77 @@ +structure CLA = CommandLineArgs +structure BFS = DedupBFS +structure G = BFS.G + +val dontDirOpt = CLA.parseFlag "no-dir-opt" +val diropt = not dontDirOpt + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val _ = + if not diropt then () else + let + val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) + val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + in + () + end + +val P = Benchmark.run "running bfs" + (fn _ => BFS.bfs {diropt = diropt} graph source) + +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + diff --git a/tests/bfs-det-priority/OffsetSearch.sml b/tests/bfs-det-priority/OffsetSearch.sml new file mode 100644 index 000000000..7e17febb8 --- /dev/null +++ b/tests/bfs-det-priority/OffsetSearch.sml @@ -0,0 +1,54 @@ +structure OffsetSearch :> +sig + (* `search x xs (lo, hi)` searches the sorted array `xs` between indices `lo` + * and `hi`, returning `(i, j)` where `i-lo` is the number of elements that + * are strictly less than `x`, and `j-i` is the number of elements which are + * equal to `x`. *) + val search : int -> int array -> int * int -> int * int +end = +struct + + val sub = Array.sub + val upd = Array.update + + fun lowSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => lowSearch x xs (lo, mid) + | GREATER => lowSearch x xs (mid + 1, hi) + | EQUAL => lowSearchEq x xs (lo, mid) + end + + and lowSearchEq x xs (lo, mid) = + if (mid = 0) orelse (x > sub (xs, mid-1)) + then mid + else lowSearch x xs (lo, mid) + + and highSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => highSearch x xs (lo, mid) + | GREATER => highSearch x xs (mid + 1, hi) + | EQUAL => highSearchEq x xs (mid, hi) + end + + and highSearchEq x xs (mid, hi) = + if (mid = Array.length xs - 1) orelse (x < sub (xs, mid + 1)) + then mid + 1 + else highSearch x xs (mid + 1, hi) + + and search (x : int) (xs : int array) (lo, hi) : int * int = + case hi - lo of + 0 => (lo, lo) + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => search x xs (lo, mid) + | GREATER => search x xs (mid + 1, hi) + | EQUAL => (lowSearchEq x xs (lo, mid), highSearchEq x xs (mid, hi)) + end + +end diff --git a/tests/bfs-det-priority/PriorityBFS.sml b/tests/bfs-det-priority/PriorityBFS.sml new file mode 100644 index 000000000..6fdc79801 --- /dev/null +++ b/tests/bfs-det-priority/PriorityBFS.sml @@ -0,0 +1,168 @@ +structure PriorityBFS = +struct + type 'a seq = 'a Seq.t + + (* structure DS = DelayedSeq *) + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + + type vertex = G.vertex + + val sub = Array.sub + val upd = Array.update + + val vtoi = V.toInt + val itov = V.fromInt + + (* fun ASsub s = + let val (a, i, _) = ArraySlice.base s + in sub (a, i+s) + end *) + + val GRAIN = 10000 + + fun strip s = + let val (s', start, _) = ArraySlice.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun bfs {diropt: bool} (g : G.graph) (s : vertex) = + let + val n = G.numVertices g + val parent = strip (Seq.tabulate (fn _ => ~1) n) + val visited = strip (Seq.tabulate (fn _ => false) n) + + (* Choose method of filtering the frontier: either frontier always + * only consists of valid vertex ids, or it allows invalid vertices and + * pretends that these vertices are isolated. *) + fun degree v = G.degree g v + fun filterFrontier s = Seq.filter (fn x => x <> itov (~1)) s + (* + fun degree v = if v < 0 then 0 else Graph.degree g v + fun filterFrontier s = s + *) + + val denseThreshold = G.numEdges g div 20 + + fun sumOfOutDegrees frontier = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length frontier) (degree o Seq.nth frontier) + (* DS.reduce op+ 0 (DS.map degree (DS.fromArraySeq frontier)) *) + + fun shouldProcessDense frontier = + diropt andalso + let + val n = Seq.length frontier + val m = sumOfOutDegrees frontier + in + n + m > denseThreshold + end + + fun bottomUp frontier = + raise Fail "PriorityBFS: bottom up not implemented yet" + + fun topDown frontier = + let + val nf = Seq.length frontier + val offsets = SeqBasis.scan GRAIN op+ 0 (0, nf) (degree o Seq.nth frontier) + val mf = sub (offsets, nf) + val outNbrs = ForkJoin.alloc mf + + (* Priority update, attempt set v as parent of u. Returns true only + * if it is the first visit, to ensure that u appears at most once + * in next frontier. *) + fun tryVisit (u, v) = + if sub (visited, u) then false else + let + val old = sub (parent, u) + val isFirstVisit = (old = ~1) + in + if v <= old then + false + else if old = Concurrency.casArray (parent, u) (old, v) then + isFirstVisit + else + tryVisit (u, v) + end + + fun visitNeighbors offset v nghs = + Util.for (0, Seq.length nghs) (fn i => + let val u = Seq.nth nghs i + in if not (tryVisit (vtoi u, vtoi v)) + then upd (outNbrs, offset + i, itov (~1)) + else upd (outNbrs, offset + i, u) + end) + + fun visitMany offlo lo hi = + if lo = hi then () else + let + val v = Seq.nth frontier offlo + val voffset = sub (offsets, offlo) + val k = Int.min (hi - lo, sub (offsets, offlo+1) - lo) + in + if k = 0 then visitMany (offlo+1) lo hi + else ( visitNeighbors lo v (Seq.subseq (G.neighbors g v) (lo - voffset, k)) + ; visitMany (offlo+1) (lo+k) hi + ) + end + + fun parVisitMany (offlo, offhi) (lo, hi) = + if hi - lo <= GRAIN then + visitMany offlo lo hi + else + let + val mid = lo + (hi - lo) div 2 + val (i, j) = OffsetSearch.search mid offsets (offlo, offhi) + val _ = ForkJoin.par + ( fn _ => parVisitMany (offlo, i) (lo, mid) + , fn _ => parVisitMany (j-1, offhi) (mid, hi) + ) + in + () + end + + (* Either one of the following is correct, but the second one has + * significantly better granularity control for graphs that have a + * small number of vertices with huge degree. *) + + (* val _ = ParUtil.parfor 100 (0, nf) (fn i => + visitMany i (sub (offsets, i)) (sub (offsets, i+1))) *) + + val _ = parVisitMany (0, nf + 1) (0, mf) + val nextFrontier = filterFrontier (ArraySlice.full outNbrs) + in + ForkJoin.parfor 5000 (0, Seq.length nextFrontier) (fn i => + let + val v = Seq.nth nextFrontier i + in + upd (visited, v, true) + end); + + nextFrontier + end + + fun search frontier = + if Seq.length frontier = 0 then + () + else if shouldProcessDense frontier then + let + val (nextFrontier, tm) = Util.getTime (fn _ => bottomUp frontier) + in + print ("dense " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + else + let + val (nextFrontier, tm) = Util.getTime (fn _ => topDown frontier) + in + print ("sparse " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + + val _ = upd (parent, vtoi s, s) + val _ = upd (visited, vtoi s, true) + val _ = search (Seq.fromList [s]) + in + ArraySlice.full parent + end + +end diff --git a/tests/bfs-det-priority/SerialBFS.sml b/tests/bfs-det-priority/SerialBFS.sml new file mode 100644 index 000000000..60a010386 --- /dev/null +++ b/tests/bfs-det-priority/SerialBFS.sml @@ -0,0 +1,38 @@ +structure SerialBFS = +struct + + structure G = AdjacencyGraph(Int) + + fun bfs g s = + let + fun neighbors v = G.neighbors g v + fun degree v = G.degree g v + + val n = G.numVertices g + val m = G.numEdges g + + val queue = ForkJoin.alloc (m+1) + val parents = Array.array (n, ~1) + + fun search (lo, hi) = + if lo >= hi then lo else + let + val v = Array.sub (queue, lo) + fun visit (hi', u) = + if Array.sub (parents, u) >= 0 then hi' + else ( Array.update (parents, u, v) + ; Array.update (queue, hi', u) + ; hi'+1 + ) + in + search (lo+1, Seq.iterate visit hi (neighbors v)) + end + + val _ = Array.update (parents, s, s) + val _ = Array.update (queue, 0, s) + val numVisited = search (0, 1) + in + ArraySlice.full parents + end + +end diff --git a/tests/bfs-det-priority/bfs-det-priority.mlb b/tests/bfs-det-priority/bfs-det-priority.mlb new file mode 100644 index 000000000..e43ea8518 --- /dev/null +++ b/tests/bfs-det-priority/bfs-det-priority.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +SerialBFS.sml +OffsetSearch.sml +PriorityBFS.sml +main.sml diff --git a/tests/bfs-det-priority/main.sml b/tests/bfs-det-priority/main.sml new file mode 100644 index 000000000..8f59fb7fa --- /dev/null +++ b/tests/bfs-det-priority/main.sml @@ -0,0 +1,77 @@ +structure CLA = CommandLineArgs +structure BFS = PriorityBFS +structure G = BFS.G + +val dontDirOpt = CLA.parseFlag "no-dir-opt" +val diropt = not dontDirOpt + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val _ = + if not diropt then () else + let + val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) + val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + in + () + end + +val P = Benchmark.run "running bfs" + (fn _ => BFS.bfs {diropt = diropt} graph source) + +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + diff --git a/tests/bfs-tree-entangled-fixed/NondetBFS.sml b/tests/bfs-tree-entangled-fixed/NondetBFS.sml new file mode 100644 index 000000000..2c2effc2b --- /dev/null +++ b/tests/bfs-tree-entangled-fixed/NondetBFS.sml @@ -0,0 +1,206 @@ +(* nondeterministic direction-optimized BFS, using CAS on outneighbors to + * construct next frontier. *) +structure NondetBFS = +struct + type 'a seq = 'a Seq.t + + (* structure DS = DelayedSeq *) + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + + type vertex = G.vertex + + val sub = Array.sub + val upd = Array.update + + val vtoi = V.toInt + val itov = V.fromInt + + (* fun ASsub s = + let val (a, i, _) = ArraySlice.base s + in sub (a, i+s) + end *) + + val GRAIN = 10000 + + fun strip s = + let val (s', start, _) = ArraySlice.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun tryUpdateSome (xs: 'a option array, i: int, old: 'a option, new: 'a option) = + let + val result = Concurrency.casArray (xs, i) (old, new) + in + if MLton.eq (old, result) then + true + else if Option.isSome result then + false + else + tryUpdateSome (xs, i, result, new) + end + + fun bfs (g : G.graph) (s : vertex) = + let + val n = G.numVertices g + val isVisited = strip (Seq.tabulate (fn _ => 0w0: Word8.word) n) + val parent = strip (Seq.tabulate (fn _ => NONE) n) + + (* Choose method of filtering the frontier: either frontier always + * only consists of valid vertex ids, or it allows invalid vertices and + * pretends that these vertices are isolated. *) + fun degree v = G.degree g v + fun filterFrontier s = Seq.filter (fn x => x <> itov (~1)) s + (* + fun degree v = if v < 0 then 0 else Graph.degree g v + fun filterFrontier s = s + *) + + val denseThreshold = G.numEdges g div 20 + + fun sumOfOutDegrees frontier = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length frontier) (degree o Seq.nth frontier) + (* DS.reduce op+ 0 (DS.map degree (DS.fromArraySeq frontier)) *) + + fun shouldProcessDense frontier = false + (* let + val n = Seq.length frontier + val m = sumOfOutDegrees frontier + in + n + m > denseThreshold + end *) + + fun bottomUp frontier = + let + val flags = Seq.tabulate (fn _ => false) n + val _ = Seq.foreach frontier (fn (_, v) => + ArraySlice.update (flags, v, true)) + fun inFrontier v = Seq.nth flags (vtoi v) + + fun processVertex v = + case sub (parent, v) of + SOME _ => NONE + | NONE => + let + val nbrs = G.neighbors g (itov v) + val deg = ArraySlice.length nbrs + fun loop i = + if i >= deg then + NONE + else + let + val u = Seq.nth nbrs i + in + if inFrontier u then + let + val parentList = Option.valOf (sub (parent, u)) + in + upd (isVisited, v, 0w1); + upd (parent, v, SOME (u :: parentList)); + SOME v + end + else + loop (i+1) + end + in + loop 0 + end + in + ArraySlice.full (SeqBasis.tabFilter 1000 (0, n) processVertex) + end + + fun topDown frontier = + let + val nf = Seq.length frontier + val offsets = SeqBasis.scan GRAIN op+ 0 (0, nf) (degree o Seq.nth frontier) + val mf = sub (offsets, nf) + val outNbrs = ForkJoin.alloc mf + + fun claim u = + sub (isVisited, u) = 0w0 + andalso + 0w0 = Concurrency.casArray (isVisited, u) (0w0, 0w1) + + fun visitNeighbors offset v nghs = + Util.for (0, Seq.length nghs) (fn i => + let + val u = Seq.nth nghs i + in + if not (claim (vtoi u)) then + upd (outNbrs, offset + i, itov (~1)) + else + let + val parentList = Option.valOf (sub (parent, vtoi v)) + val parentList' = SOME (v :: parentList) + in + upd (parent, vtoi u, parentList'); + upd (outNbrs, offset + i, u) + end + end) + + fun visitMany offlo lo hi = + if lo = hi then () else + let + val v = Seq.nth frontier offlo + val voffset = sub (offsets, offlo) + val k = Int.min (hi - lo, sub (offsets, offlo+1) - lo) + in + if k = 0 then visitMany (offlo+1) lo hi + else ( visitNeighbors lo v (Seq.subseq (G.neighbors g v) (lo - voffset, k)) + ; visitMany (offlo+1) (lo+k) hi + ) + end + + fun parVisitMany (offlo, offhi) (lo, hi) = + if hi - lo <= GRAIN then + visitMany offlo lo hi + else + let + val mid = lo + (hi - lo) div 2 + val (i, j) = OffsetSearch.search mid offsets (offlo, offhi) + val _ = ForkJoin.par + ( fn _ => parVisitMany (offlo, i) (lo, mid) + , fn _ => parVisitMany (j-1, offhi) (mid, hi) + ) + in + () + end + + (* Either one of the following is correct, but the second one has + * significantly better granularity control for graphs that have a + * small number of vertices with huge degree. *) + + (* val _ = ParUtil.parfor 100 (0, nf) (fn i => + visitMany i (sub (offsets, i)) (sub (offsets, i+1))) *) + + val _ = parVisitMany (0, nf + 1) (0, mf) + in + filterFrontier (ArraySlice.full outNbrs) + end + + fun search frontier = + if Seq.length frontier = 0 then + () + else if shouldProcessDense frontier then + let + val (nextFrontier, tm) = Util.getTime (fn _ => bottomUp frontier) + in + print ("dense " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + else + let + val (nextFrontier, tm) = Util.getTime (fn _ => topDown frontier) + in + print ("sparse " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + + val _ = upd (parent, vtoi s, SOME []) + val _ = upd (isVisited, vtoi s, 0w1) + val _ = search (Seq.fromList [s]) + in + ArraySlice.full parent + end + +end diff --git a/tests/bfs-tree-entangled-fixed/OffsetSearch.sml b/tests/bfs-tree-entangled-fixed/OffsetSearch.sml new file mode 100644 index 000000000..7e17febb8 --- /dev/null +++ b/tests/bfs-tree-entangled-fixed/OffsetSearch.sml @@ -0,0 +1,54 @@ +structure OffsetSearch :> +sig + (* `search x xs (lo, hi)` searches the sorted array `xs` between indices `lo` + * and `hi`, returning `(i, j)` where `i-lo` is the number of elements that + * are strictly less than `x`, and `j-i` is the number of elements which are + * equal to `x`. *) + val search : int -> int array -> int * int -> int * int +end = +struct + + val sub = Array.sub + val upd = Array.update + + fun lowSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => lowSearch x xs (lo, mid) + | GREATER => lowSearch x xs (mid + 1, hi) + | EQUAL => lowSearchEq x xs (lo, mid) + end + + and lowSearchEq x xs (lo, mid) = + if (mid = 0) orelse (x > sub (xs, mid-1)) + then mid + else lowSearch x xs (lo, mid) + + and highSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => highSearch x xs (lo, mid) + | GREATER => highSearch x xs (mid + 1, hi) + | EQUAL => highSearchEq x xs (mid, hi) + end + + and highSearchEq x xs (mid, hi) = + if (mid = Array.length xs - 1) orelse (x < sub (xs, mid + 1)) + then mid + 1 + else highSearch x xs (mid + 1, hi) + + and search (x : int) (xs : int array) (lo, hi) : int * int = + case hi - lo of + 0 => (lo, lo) + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => search x xs (lo, mid) + | GREATER => search x xs (mid + 1, hi) + | EQUAL => (lowSearchEq x xs (lo, mid), highSearchEq x xs (mid, hi)) + end + +end diff --git a/tests/bfs-tree-entangled-fixed/SerialBFS.sml b/tests/bfs-tree-entangled-fixed/SerialBFS.sml new file mode 100644 index 000000000..fb06b8ba9 --- /dev/null +++ b/tests/bfs-tree-entangled-fixed/SerialBFS.sml @@ -0,0 +1,42 @@ +structure SerialBFS = +struct + + structure G = AdjacencyGraph(Int) + + fun bfs g s = + let + fun neighbors v = G.neighbors g v + fun degree v = G.degree g v + + val n = G.numVertices g + val m = G.numEdges g + + val queue = ForkJoin.alloc (m+1) + val parents = Array.array (n, NONE) + + fun search (lo, hi) = + if lo >= hi then lo else + let + val v = Array.sub (queue, lo) + val parentList = Option.valOf (Array.sub (parents, v)) + + fun visit (hi', u) = + case Array.sub (parents, u) of + SOME _ => hi' + | NONE => + ( Array.update (parents, u, SOME (v :: parentList)) + ; Array.update (queue, hi', u) + ; hi'+1 + ) + in + search (lo+1, Seq.iterate visit hi (neighbors v)) + end + + val _ = Array.update (parents, s, SOME []) + val _ = Array.update (queue, 0, s) + val numVisited = search (0, 1) + in + ArraySlice.full parents + end + +end diff --git a/tests/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb b/tests/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb new file mode 100644 index 000000000..320aea3a4 --- /dev/null +++ b/tests/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +SerialBFS.sml +OffsetSearch.sml +NondetBFS.sml +main.sml diff --git a/tests/bfs-tree-entangled-fixed/main.sml b/tests/bfs-tree-entangled-fixed/main.sml new file mode 100644 index 000000000..5e19b9261 --- /dev/null +++ b/tests/bfs-tree-entangled-fixed/main.sml @@ -0,0 +1,66 @@ +structure CLA = CommandLineArgs +structure BFS = NondetBFS +structure G = BFS.G + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +(* val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") *) + +val P = Benchmark.run "running bfs" (fn _ => BFS.bfs graph source) + +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Option.isSome (Seq.nth P i) then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P v = + case Seq.nth P v of + NONE => ~1 + | SOME parents => List.length parents + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P i = numHops P' i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + diff --git a/tests/bfs-tree-entangled/NondetBFS.sml b/tests/bfs-tree-entangled/NondetBFS.sml new file mode 100644 index 000000000..993ab05b7 --- /dev/null +++ b/tests/bfs-tree-entangled/NondetBFS.sml @@ -0,0 +1,212 @@ +(* nondeterministic direction-optimized BFS, using CAS on outneighbors to + * construct next frontier. *) +structure NondetBFS = +struct + type 'a seq = 'a Seq.t + + (* structure DS = DelayedSeq *) + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + + type vertex = G.vertex + + val sub = Array.sub + val upd = Array.update + + val vtoi = V.toInt + val itov = V.fromInt + + (* fun ASsub s = + let val (a, i, _) = ArraySlice.base s + in sub (a, i+s) + end *) + + val GRAIN = 10000 + + fun strip s = + let val (s', start, _) = ArraySlice.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun tryUpdateSome (xs: 'a option array, i: int, old: 'a option, new: 'a option) = + let + val result = Concurrency.casArray (xs, i) (old, new) + in + if MLton.eq (old, result) then + true + else if Option.isSome result then + false + else + tryUpdateSome (xs, i, result, new) + end + + fun bfs (g : G.graph) (s : vertex) = + let + val n = G.numVertices g + val parent = strip (Seq.tabulate (fn _ => NONE) n) + + (* Choose method of filtering the frontier: either frontier always + * only consists of valid vertex ids, or it allows invalid vertices and + * pretends that these vertices are isolated. *) + fun degree v = G.degree g v + fun filterFrontier s = Seq.filter (fn x => x <> itov (~1)) s + (* + fun degree v = if v < 0 then 0 else Graph.degree g v + fun filterFrontier s = s + *) + + val denseThreshold = G.numEdges g div 20 + + fun sumOfOutDegrees frontier = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length frontier) (degree o Seq.nth frontier) + (* DS.reduce op+ 0 (DS.map degree (DS.fromArraySeq frontier)) *) + + fun shouldProcessDense frontier = false + (* let + val n = Seq.length frontier + val m = sumOfOutDegrees frontier + in + n + m > denseThreshold + end *) + + fun bottomUp frontier = + let + val flags = Seq.tabulate (fn _ => false) n + val _ = Seq.foreach frontier (fn (_, v) => + ArraySlice.update (flags, v, true)) + fun inFrontier v = Seq.nth flags (vtoi v) + + fun processVertex v = + case sub (parent, v) of + SOME _ => NONE + | NONE => + let + val nbrs = G.neighbors g (itov v) + val deg = ArraySlice.length nbrs + fun loop i = + if i >= deg then + NONE + else + let + val u = Seq.nth nbrs i + in + if inFrontier u then + let + val parentList = Option.valOf (sub (parent, u)) + in + upd (parent, v, SOME (u :: parentList)); + SOME v + end + else + loop (i+1) + end + in + loop 0 + end + in + ArraySlice.full (SeqBasis.tabFilter 1000 (0, n) processVertex) + end + + fun topDown frontier = + let + val nf = Seq.length frontier + val offsets = SeqBasis.scan GRAIN op+ 0 (0, nf) (degree o Seq.nth frontier) + val mf = sub (offsets, nf) + val outNbrs = ForkJoin.alloc mf + + (* attempt to claim parent of u as v *) + (* fun claim (u, v) = + sub (parent, u) = ~1 + andalso + ~1 = Concurrency.casArray (parent, u) (~1, v) *) + + fun visitNeighbors offset v nghs = + Util.for (0, Seq.length nghs) (fn i => + let + val u = Seq.nth nghs i + in + case sub (parent, vtoi u) of + SOME _ => upd (outNbrs, offset + i, itov (~1)) + | old as NONE => + let + val parentList = Option.valOf (sub (parent, vtoi v)) + val parentList' = SOME (v :: parentList) + in + if tryUpdateSome (parent, vtoi u, old, parentList') then + upd (outNbrs, offset + i, u) + else + upd (outNbrs, offset + i, itov (~1)) + end + end) + + (* let val u = Seq.nth nghs i + in if not (claim (vtoi u, vtoi v)) + then upd (outNbrs, offset + i, itov (~1)) + else upd (outNbrs, offset + i, u) + end) *) + + fun visitMany offlo lo hi = + if lo = hi then () else + let + val v = Seq.nth frontier offlo + val voffset = sub (offsets, offlo) + val k = Int.min (hi - lo, sub (offsets, offlo+1) - lo) + in + if k = 0 then visitMany (offlo+1) lo hi + else ( visitNeighbors lo v (Seq.subseq (G.neighbors g v) (lo - voffset, k)) + ; visitMany (offlo+1) (lo+k) hi + ) + end + + fun parVisitMany (offlo, offhi) (lo, hi) = + if hi - lo <= GRAIN then + visitMany offlo lo hi + else + let + val mid = lo + (hi - lo) div 2 + val (i, j) = OffsetSearch.search mid offsets (offlo, offhi) + val _ = ForkJoin.par + ( fn _ => parVisitMany (offlo, i) (lo, mid) + , fn _ => parVisitMany (j-1, offhi) (mid, hi) + ) + in + () + end + + (* Either one of the following is correct, but the second one has + * significantly better granularity control for graphs that have a + * small number of vertices with huge degree. *) + + (* val _ = ParUtil.parfor 100 (0, nf) (fn i => + visitMany i (sub (offsets, i)) (sub (offsets, i+1))) *) + + val _ = parVisitMany (0, nf + 1) (0, mf) + in + filterFrontier (ArraySlice.full outNbrs) + end + + fun search frontier = + if Seq.length frontier = 0 then + () + else if shouldProcessDense frontier then + let + val (nextFrontier, tm) = Util.getTime (fn _ => bottomUp frontier) + in + print ("dense " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + else + let + val (nextFrontier, tm) = Util.getTime (fn _ => topDown frontier) + in + print ("sparse " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + + val _ = upd (parent, vtoi s, SOME []) + val _ = search (Seq.fromList [s]) + in + ArraySlice.full parent + end + +end diff --git a/tests/bfs-tree-entangled/OffsetSearch.sml b/tests/bfs-tree-entangled/OffsetSearch.sml new file mode 100644 index 000000000..7e17febb8 --- /dev/null +++ b/tests/bfs-tree-entangled/OffsetSearch.sml @@ -0,0 +1,54 @@ +structure OffsetSearch :> +sig + (* `search x xs (lo, hi)` searches the sorted array `xs` between indices `lo` + * and `hi`, returning `(i, j)` where `i-lo` is the number of elements that + * are strictly less than `x`, and `j-i` is the number of elements which are + * equal to `x`. *) + val search : int -> int array -> int * int -> int * int +end = +struct + + val sub = Array.sub + val upd = Array.update + + fun lowSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => lowSearch x xs (lo, mid) + | GREATER => lowSearch x xs (mid + 1, hi) + | EQUAL => lowSearchEq x xs (lo, mid) + end + + and lowSearchEq x xs (lo, mid) = + if (mid = 0) orelse (x > sub (xs, mid-1)) + then mid + else lowSearch x xs (lo, mid) + + and highSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => highSearch x xs (lo, mid) + | GREATER => highSearch x xs (mid + 1, hi) + | EQUAL => highSearchEq x xs (mid, hi) + end + + and highSearchEq x xs (mid, hi) = + if (mid = Array.length xs - 1) orelse (x < sub (xs, mid + 1)) + then mid + 1 + else highSearch x xs (mid + 1, hi) + + and search (x : int) (xs : int array) (lo, hi) : int * int = + case hi - lo of + 0 => (lo, lo) + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => search x xs (lo, mid) + | GREATER => search x xs (mid + 1, hi) + | EQUAL => (lowSearchEq x xs (lo, mid), highSearchEq x xs (mid, hi)) + end + +end diff --git a/tests/bfs-tree-entangled/SerialBFS.sml b/tests/bfs-tree-entangled/SerialBFS.sml new file mode 100644 index 000000000..fb06b8ba9 --- /dev/null +++ b/tests/bfs-tree-entangled/SerialBFS.sml @@ -0,0 +1,42 @@ +structure SerialBFS = +struct + + structure G = AdjacencyGraph(Int) + + fun bfs g s = + let + fun neighbors v = G.neighbors g v + fun degree v = G.degree g v + + val n = G.numVertices g + val m = G.numEdges g + + val queue = ForkJoin.alloc (m+1) + val parents = Array.array (n, NONE) + + fun search (lo, hi) = + if lo >= hi then lo else + let + val v = Array.sub (queue, lo) + val parentList = Option.valOf (Array.sub (parents, v)) + + fun visit (hi', u) = + case Array.sub (parents, u) of + SOME _ => hi' + | NONE => + ( Array.update (parents, u, SOME (v :: parentList)) + ; Array.update (queue, hi', u) + ; hi'+1 + ) + in + search (lo+1, Seq.iterate visit hi (neighbors v)) + end + + val _ = Array.update (parents, s, SOME []) + val _ = Array.update (queue, 0, s) + val numVisited = search (0, 1) + in + ArraySlice.full parents + end + +end diff --git a/tests/bfs-tree-entangled/bfs-tree-entangled.mlb b/tests/bfs-tree-entangled/bfs-tree-entangled.mlb new file mode 100644 index 000000000..320aea3a4 --- /dev/null +++ b/tests/bfs-tree-entangled/bfs-tree-entangled.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +SerialBFS.sml +OffsetSearch.sml +NondetBFS.sml +main.sml diff --git a/tests/bfs-tree-entangled/main.sml b/tests/bfs-tree-entangled/main.sml new file mode 100644 index 000000000..5e19b9261 --- /dev/null +++ b/tests/bfs-tree-entangled/main.sml @@ -0,0 +1,66 @@ +structure CLA = CommandLineArgs +structure BFS = NondetBFS +structure G = BFS.G + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +(* val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") *) + +val P = Benchmark.run "running bfs" (fn _ => BFS.bfs graph source) + +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Option.isSome (Seq.nth P i) then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P v = + case Seq.nth P v of + NONE => ~1 + | SOME parents => List.length parents + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P i = numHops P' i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + diff --git a/tests/bfs/NondetBFS.sml b/tests/bfs/NondetBFS.sml new file mode 100644 index 000000000..0522dd85b --- /dev/null +++ b/tests/bfs/NondetBFS.sml @@ -0,0 +1,177 @@ +(* nondeterministic direction-optimized BFS, using CAS on outneighbors to + * construct next frontier. *) +structure NondetBFS = +struct + type 'a seq = 'a Seq.t + + (* structure DS = DelayedSeq *) + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + + type vertex = G.vertex + + val sub = Array.sub + val upd = Array.update + + val vtoi = V.toInt + val itov = V.fromInt + + (* fun ASsub s = + let val (a, i, _) = ArraySlice.base s + in sub (a, i+s) + end *) + + val GRAIN = 10000 + + fun strip s = + let val (s', start, _) = ArraySlice.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun bfs {diropt: bool} (g : G.graph) (s : vertex) = + let + val n = G.numVertices g + val parent = strip (Seq.tabulate (fn _ => ~1) n) + + (* Choose method of filtering the frontier: either frontier always + * only consists of valid vertex ids, or it allows invalid vertices and + * pretends that these vertices are isolated. *) + fun degree v = G.degree g v + fun filterFrontier s = Seq.filter (fn x => x <> itov (~1)) s + (* + fun degree v = if v < 0 then 0 else Graph.degree g v + fun filterFrontier s = s + *) + + val denseThreshold = G.numEdges g div 20 + + fun sumOfOutDegrees frontier = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length frontier) (degree o Seq.nth frontier) + (* DS.reduce op+ 0 (DS.map degree (DS.fromArraySeq frontier)) *) + + fun shouldProcessDense frontier = + diropt andalso + let + val n = Seq.length frontier + val m = sumOfOutDegrees frontier + in + n + m > denseThreshold + end + + fun bottomUp frontier = + let + val flags = Seq.tabulate (fn _ => false) n + val _ = Seq.foreach frontier (fn (_, v) => + ArraySlice.update (flags, v, true)) + fun inFrontier v = Seq.nth flags (vtoi v) + + fun processVertex v = + if sub (parent, v) <> ~1 then NONE else + let + val nbrs = G.neighbors g (itov v) + val deg = ArraySlice.length nbrs + fun loop i = + if i >= deg then + NONE + else + let + val u = Seq.nth nbrs i + in + if inFrontier u then + (upd (parent, v, u); SOME v) + else + loop (i+1) + end + in + loop 0 + end + in + ArraySlice.full (SeqBasis.tabFilter 1000 (0, n) processVertex) + end + + fun topDown frontier = + let + val nf = Seq.length frontier + val offsets = SeqBasis.scan GRAIN op+ 0 (0, nf) (degree o Seq.nth frontier) + val mf = sub (offsets, nf) + val outNbrs = ForkJoin.alloc mf + + (* attempt to claim parent of u as v *) + fun claim (u, v) = + sub (parent, u) = ~1 + andalso + ~1 = Concurrency.casArray (parent, u) (~1, v) + + fun visitNeighbors offset v nghs = + Util.for (0, Seq.length nghs) (fn i => + let val u = Seq.nth nghs i + in if not (claim (vtoi u, vtoi v)) + then upd (outNbrs, offset + i, itov (~1)) + else upd (outNbrs, offset + i, u) + end) + + fun visitMany offlo lo hi = + if lo = hi then () else + let + val v = Seq.nth frontier offlo + val voffset = sub (offsets, offlo) + val k = Int.min (hi - lo, sub (offsets, offlo+1) - lo) + in + if k = 0 then visitMany (offlo+1) lo hi + else ( visitNeighbors lo v (Seq.subseq (G.neighbors g v) (lo - voffset, k)) + ; visitMany (offlo+1) (lo+k) hi + ) + end + + fun parVisitMany (offlo, offhi) (lo, hi) = + if hi - lo <= GRAIN then + visitMany offlo lo hi + else + let + val mid = lo + (hi - lo) div 2 + val (i, j) = OffsetSearch.search mid offsets (offlo, offhi) + val _ = ForkJoin.par + ( fn _ => parVisitMany (offlo, i) (lo, mid) + , fn _ => parVisitMany (j-1, offhi) (mid, hi) + ) + in + () + end + + (* Either one of the following is correct, but the second one has + * significantly better granularity control for graphs that have a + * small number of vertices with huge degree. *) + + (* val _ = ParUtil.parfor 100 (0, nf) (fn i => + visitMany i (sub (offsets, i)) (sub (offsets, i+1))) *) + + val _ = parVisitMany (0, nf + 1) (0, mf) + in + filterFrontier (ArraySlice.full outNbrs) + end + + fun search frontier = + if Seq.length frontier = 0 then + () + else if shouldProcessDense frontier then + let + val (nextFrontier, tm) = Util.getTime (fn _ => bottomUp frontier) + in + print ("dense " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + else + let + val (nextFrontier, tm) = Util.getTime (fn _ => topDown frontier) + in + print ("sparse " ^ Time.fmt 4 tm ^ "\n"); + search nextFrontier + end + + val _ = upd (parent, vtoi s, s) + val _ = search (Seq.fromList [s]) + in + ArraySlice.full parent + end + +end diff --git a/tests/bfs/OffsetSearch.sml b/tests/bfs/OffsetSearch.sml new file mode 100644 index 000000000..7e17febb8 --- /dev/null +++ b/tests/bfs/OffsetSearch.sml @@ -0,0 +1,54 @@ +structure OffsetSearch :> +sig + (* `search x xs (lo, hi)` searches the sorted array `xs` between indices `lo` + * and `hi`, returning `(i, j)` where `i-lo` is the number of elements that + * are strictly less than `x`, and `j-i` is the number of elements which are + * equal to `x`. *) + val search : int -> int array -> int * int -> int * int +end = +struct + + val sub = Array.sub + val upd = Array.update + + fun lowSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => lowSearch x xs (lo, mid) + | GREATER => lowSearch x xs (mid + 1, hi) + | EQUAL => lowSearchEq x xs (lo, mid) + end + + and lowSearchEq x xs (lo, mid) = + if (mid = 0) orelse (x > sub (xs, mid-1)) + then mid + else lowSearch x xs (lo, mid) + + and highSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => highSearch x xs (lo, mid) + | GREATER => highSearch x xs (mid + 1, hi) + | EQUAL => highSearchEq x xs (mid, hi) + end + + and highSearchEq x xs (mid, hi) = + if (mid = Array.length xs - 1) orelse (x < sub (xs, mid + 1)) + then mid + 1 + else highSearch x xs (mid + 1, hi) + + and search (x : int) (xs : int array) (lo, hi) : int * int = + case hi - lo of + 0 => (lo, lo) + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => search x xs (lo, mid) + | GREATER => search x xs (mid + 1, hi) + | EQUAL => (lowSearchEq x xs (lo, mid), highSearchEq x xs (mid, hi)) + end + +end diff --git a/tests/bfs/SerialBFS.sml b/tests/bfs/SerialBFS.sml new file mode 100644 index 000000000..60a010386 --- /dev/null +++ b/tests/bfs/SerialBFS.sml @@ -0,0 +1,38 @@ +structure SerialBFS = +struct + + structure G = AdjacencyGraph(Int) + + fun bfs g s = + let + fun neighbors v = G.neighbors g v + fun degree v = G.degree g v + + val n = G.numVertices g + val m = G.numEdges g + + val queue = ForkJoin.alloc (m+1) + val parents = Array.array (n, ~1) + + fun search (lo, hi) = + if lo >= hi then lo else + let + val v = Array.sub (queue, lo) + fun visit (hi', u) = + if Array.sub (parents, u) >= 0 then hi' + else ( Array.update (parents, u, v) + ; Array.update (queue, hi', u) + ; hi'+1 + ) + in + search (lo+1, Seq.iterate visit hi (neighbors v)) + end + + val _ = Array.update (parents, s, s) + val _ = Array.update (queue, 0, s) + val numVisited = search (0, 1) + in + ArraySlice.full parents + end + +end diff --git a/tests/bfs/bfs.mlb b/tests/bfs/bfs.mlb new file mode 100644 index 000000000..320aea3a4 --- /dev/null +++ b/tests/bfs/bfs.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +SerialBFS.sml +OffsetSearch.sml +NondetBFS.sml +main.sml diff --git a/tests/bfs/main.sml b/tests/bfs/main.sml new file mode 100644 index 000000000..4778c2ae9 --- /dev/null +++ b/tests/bfs/main.sml @@ -0,0 +1,77 @@ +structure CLA = CommandLineArgs +structure BFS = NondetBFS +structure G = BFS.G + +val dontDirOpt = CLA.parseFlag "no-dir-opt" +val diropt = not dontDirOpt + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val _ = + if not diropt then () else + let + val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) + val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + in + () + end + +val P = Benchmark.run "running bfs" + (fn _ => BFS.bfs {diropt = diropt} graph source) + +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + diff --git a/tests/bignum-add-opt/Add.sml b/tests/bignum-add-opt/Add.sml new file mode 100644 index 000000000..c6e40794f --- /dev/null +++ b/tests/bignum-add-opt/Add.sml @@ -0,0 +1,78 @@ +structure Add = +struct + structure Seq = ArraySequence + + type byte = Word8.word + type bignum = byte Seq.t + + fun init (b1, b2) = + Word8.+ (b1, b2) + + fun copy (a, b) = + if b = 0w127 then a else b + + + fun add (x, y) = + let + val nx = Seq.length x + val ny = Seq.length y + val n = Int.max (nx, ny) + + fun nthx i = if i < nx then Seq.nth x i else 0w0 + fun nthy i = if i < ny then Seq.nth y i else 0w0 + + val blockSize = 10000 + val numBlocks = 1 + ((n-1) div blockSize) + + val blockCarries = + SeqBasis.tabulate 1 (0, numBlocks) (fn blockIdx => + let + val lo = blockIdx * blockSize + val hi = Int.min (lo + blockSize, n) + fun loop acc i = + if i >= hi then + acc + else + loop (copy (acc, init (nthx i, nthy i))) (i+1) + in + loop 0w0 lo + end) + + val blockPartials = + SeqBasis.scan 5000 copy 0w0 (0, numBlocks) + (fn i => Array.sub (blockCarries, i)) + + val lastCarry = Array.sub (blockPartials, numBlocks) + + val result = ForkJoin.alloc (n+1) + + val _ = + ForkJoin.parfor 1 (0, numBlocks) (fn blockIdx => + let + val lo = blockIdx * blockSize + val hi = Int.min (lo + blockSize, n) + + fun loop acc i = + if i >= hi then + () + else + let + val sum = init (nthx i, nthy i) + val acc' = copy (acc, sum) + val thisByte = + Word8.andb (Word8.+ (Word8.>> (acc, 0w7), sum), 0wx7F) + in + Array.update (result, i, thisByte); + loop acc' (i+1) + end + in + loop (Array.sub (blockPartials, blockIdx)) lo + end) + + in + if lastCarry > 0w127 then + (Array.update (result, n, 0w1); ArraySlice.full result) + else + (ArraySlice.slice (result, 0, SOME n)) + end +end diff --git a/tests/bignum-add-opt/bignum-add-opt.mlb b/tests/bignum-add-opt/bignum-add-opt.mlb new file mode 100644 index 000000000..4e38d2345 --- /dev/null +++ b/tests/bignum-add-opt/bignum-add-opt.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +../bignum-add/Bignum.sml +Add.sml +../bignum-add/SequentialAdd.sml +main.sml diff --git a/tests/bignum-add-opt/main.sml b/tests/bignum-add-opt/main.sml new file mode 100644 index 000000000..0e29ab546 --- /dev/null +++ b/tests/bignum-add-opt/main.sml @@ -0,0 +1,36 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure Add = Add + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val seed = CLA.parseInt "seed" 15210 +val doCheck = CLA.parseFlag "check" + +val _ = print ("n " ^ Int.toString n ^ "\n") + +val input1 = Bignum.generate n seed +val input2 = Bignum.generate n (seed + n) + +fun task () = + Add.add (input1, input2) + +fun check result = + if not doCheck then () else + let + val (correctResult, tm) = + Util.getTime (fn _ => SequentialAdd.add (input1, input2)) + val _ = print ("sequential " ^ Time.fmt 4 tm ^ "s\n") + val correct = + Seq.equal op= (result, correctResult) + in + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + end + +val result = Benchmark.run "bignum add" task +val _ = check result + +(* val _ = print ("result " ^ IntInf.toString (Bignum.toIntInf result) ^ "\n") *) diff --git a/tests/bignum-add/Bignum.sml b/tests/bignum-add/Bignum.sml new file mode 100644 index 000000000..b7ed3e688 --- /dev/null +++ b/tests/bignum-add/Bignum.sml @@ -0,0 +1,57 @@ +structure Bignum = +struct + + structure Seq = ArraySequence + + type byte = Word8.word + + (* radix 128 representation *) + type t = byte Seq.t + + type bignum = t + + fun properlyFormatted x = + Seq.length x = 0 orelse Seq.nth x (Seq.length x - 1) <> 0w0 + + fun fromIntInf (x: IntInf.int): bignum = + if x < 0 then + raise Fail "bignums can't be negative" + else + let + fun toList (x: IntInf.int) : byte list = + if x = 0 then [] + else Word8.fromInt (IntInf.toInt (x mod 128)) :: toList (x div 128) + in + Seq.fromList (toList x) + end + + fun toIntInf (n: bignum): IntInf.int = + if not (properlyFormatted n) then + raise Fail "invalid bignum" + else + let + val n' = Seq.map (IntInf.fromInt o Word8.toInt) n + in + Seq.iterate (fn (x, d) => 128 * x + d) (0: IntInf.int) (Seq.rev n') + end + + fun generate n seed = + let + fun hash seed = Util.hash32_2 (Word32.fromInt seed) + fun w32to8 w = Word8.fromInt (Word32.toInt (Word32.andb (w, 0wx7F))) + fun genByte seed = w32to8 (hash seed) + fun genNonZeroByte seed = + w32to8 (Word32.+ (0w1, Word32.mod (hash seed, 0w127))) + in + Seq.tabulate (fn i => + if i < n-1 then + genByte (seed+i) + else + genNonZeroByte (seed+i)) + n + end + + fun toString x = + Seq.toString Word8.toString x + +end diff --git a/tests/bignum-add/MkAdd.sml b/tests/bignum-add/MkAdd.sml new file mode 100644 index 000000000..d55f8a885 --- /dev/null +++ b/tests/bignum-add/MkAdd.sml @@ -0,0 +1,39 @@ +functor MkAdd (Seq: SEQUENCE) = +struct + + structure ASeq = ArraySequence + + type byte = Word8.word + type bignum = byte ASeq.t + + fun nth' s i = + if i < Seq.length s then Seq.nth s i else (0w0: Word8.word) + + fun add (x, y) = + let + val x = Seq.fromArraySeq x + val y = Seq.fromArraySeq y + + val maxlen = Int.max (Seq.length x, Seq.length y) + val sums = Seq.tabulate (fn i => Word8.+ (nth' x i, nth' y i)) (maxlen+1) + + fun propagate (a, b) = + if b = 0w127 then a else b + val (carries, _) = Seq.scan propagate 0w0 sums + + fun f (carry, sum) = + Word8.andb (Word8.+ (Word8.>> (carry, 0w7), sum), 0wx7F) + + val result = + Seq.force (Seq.zipWith f (carries, sums)) + + val r = Seq.toArraySeq result + in + (* [r] might have a trailing 0. Cut it off. *) + if ASeq.length r = 0 orelse (ASeq.nth r (ASeq.length r - 1) > 0w0) then + r + else + ASeq.take r (ASeq.length r - 1) + end + +end diff --git a/tests/bignum-add/SequentialAdd.sml b/tests/bignum-add/SequentialAdd.sml new file mode 100644 index 000000000..f5a127c53 --- /dev/null +++ b/tests/bignum-add/SequentialAdd.sml @@ -0,0 +1,76 @@ +structure SequentialAdd = +struct + structure A = Array + structure AS = ArraySlice + structure Seq = ArraySequence + + (* radix 128 *) + type byte = Word8.word + type bignum = byte Seq.t + + fun addWithCarry3 (c, b1, b2) = + let + val x = Word8.+ (b1, Word8.+ (b2, c)) + in + {result = Word8.andb (x, 0wx7F), carry = Word8.>> (x, 0w7)} + end + + fun addWithCarry2 (b1, b2) = + addWithCarry3 (0w0, b1, b2) + + fun add (s1, s2) = + let + val n1 = Seq.length s1 + val n2 = Seq.length s2 + val n = Int.max (n1, n2) + + val r = ForkJoin.alloc (1 + n) + + fun finish1 i carry = + if i = n1 then + (A.update (r, i, carry); carry) + else + let + val {result, carry=carry'} = addWithCarry2 (Seq.nth s1 i, carry) + in + A.update (r, i, result); + finish1 (i+1) carry' + end + + fun finish2 i carry = + if i = n2 then + (A.update (r, i, carry); carry) + else + let + val {result, carry=carry'} = addWithCarry2 (Seq.nth s2 i, carry) + in + A.update (r, i, result); + finish2 (i+1) carry' + end + + fun loop i carry = + if i = n1 then + finish2 i carry + else if i = n2 then + finish1 i carry + else + let + val {result, carry=carry'} = + addWithCarry3 (Seq.nth s1 i, Seq.nth s2 i, carry) + in + A.update (r, i, result); + loop (i+1) carry' + end + in + (** Run the loop, and inspect the last carry value. + * If it is 1, then the output is well-formed. + * If it is 0, we need to trim. + *) + case loop 0 0w0 of + 0w0 => + AS.slice (r, 0, SOME n) + | _ => + AS.full r + end + +end diff --git a/tests/bignum-add/bignum-add.mlb b/tests/bignum-add/bignum-add.mlb new file mode 100644 index 000000000..905631370 --- /dev/null +++ b/tests/bignum-add/bignum-add.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +Bignum.sml +MkAdd.sml +SequentialAdd.sml +main.sml diff --git a/tests/bignum-add/main.sml b/tests/bignum-add/main.sml new file mode 100644 index 000000000..9e7cebf74 --- /dev/null +++ b/tests/bignum-add/main.sml @@ -0,0 +1,36 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure Add = MkAdd(DelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val seed = CLA.parseInt "seed" 15210 +val doCheck = CLA.parseFlag "check" + +val _ = print ("n " ^ Int.toString n ^ "\n") + +val input1 = Bignum.generate n seed +val input2 = Bignum.generate n (seed + n) + +fun task () = + Add.add (input1, input2) + +fun check result = + if not doCheck then () else + let + val (correctResult, tm) = + Util.getTime (fn _ => SequentialAdd.add (input1, input2)) + val _ = print ("sequential " ^ Time.fmt 4 tm ^ "s\n") + val correct = + Seq.equal op= (result, correctResult) + in + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + end + +val result = Benchmark.run "bignum add" task +val _ = check result + +(* val _ = print ("result " ^ IntInf.toString (Bignum.toIntInf result) ^ "\n") *) diff --git a/tests/centrality/BC.sml b/tests/centrality/BC.sml new file mode 100644 index 000000000..1351e4638 --- /dev/null +++ b/tests/centrality/BC.sml @@ -0,0 +1,333 @@ +structure BC = +struct + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + + type vertex = G.vertex + type graph = G.graph + + val sub = Array.sub + val upd = Array.update + + val vtoi = V.toInt + val itov = V.fromInt + + (* fun ASsub s = + let val (a, i, _) = ArraySlice.base s + in sub (a, i+s) + end *) + + val GRAIN = 10000 + + fun sumOfOutDegrees g frontier = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length frontier) (G.degree g o Seq.nth frontier) + (* DS.reduce op+ 0 (DS.map degree (DS.fromArraySeq frontier)) *) + + fun shouldProcessDense g frontier = + let + val n = Seq.length frontier + val m = sumOfOutDegrees g frontier + in + n + m > (G.numEdges g) div 20 + end + + fun edgeMapDense + { cond : (vertex * 'a) -> bool + , func : (vertex * vertex * 'a * 'a) -> 'a option + , eq : 'a * 'a -> bool + , frontier + , visitedRoundNums + , state + , roundNum + , graph = g + , shouldOutput + } = + let + val N = G.numVertices g + val M = G.numEdges g + + (* val visitedRoundNums = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (visitedRoundNums, i, ~1)) *) + fun visitedRound v = sub (visitedRoundNums, v) + fun isVisited v = visitedRound v <> ~1 + fun setVisited v r = upd (visitedRoundNums, v, r) + + (* val state : 'a array = ForkJoin.alloc N *) + (* val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (state, i, initialState i)) *) + fun getState v = sub (state, v) + fun setState v s = upd (state, v, s) + + val flags = Seq.tabulate (fn _ => false) N + val _ = Seq.foreach frontier (fn (_, v) => + ArraySlice.update (flags, v, true)) + fun inFrontier v = Seq.nth flags (vtoi v) + + fun processVertex v = + if isVisited v then NONE else + let + val nbrs = G.neighbors g (itov v) + val deg = ArraySlice.length nbrs + fun loop visited sv i = + if i >= deg then (visited, sv) else + let + val u = Seq.nth nbrs i + in + if not (inFrontier u) then loop visited sv (i+1) else + case func (u, v, getState u, sv) of + NONE => loop true sv (i+1) + | SOME sv' => if cond (v, sv') then (true, sv') else loop true sv' (i+1) + end + val sv = getState v + val (visited, sv') = loop false sv 0 + in + if eq (sv, sv') then () else setState v sv'; + if not visited then NONE else (setVisited v roundNum; SOME v) + end + in + if shouldOutput then + ArraySlice.full (SeqBasis.tabFilter 1000 (0, N) processVertex) + (* G.tabFilter 1000 (0, N) processVertex *) + else + (ForkJoin.parfor 1000 (0, N) (ignore o processVertex); Seq.empty ()) + end + + fun edgeMapSparse + { cond : (vertex * 'a) -> bool + , func : (vertex * vertex * 'a * 'a) -> 'a option + , eq : 'a * 'a -> bool + , frontier + , visitedRoundNums + , roundNum + , state + , graph = g + , shouldOutput + } = + let + val N = G.numVertices g + val M = G.numEdges g + + fun degree v = G.degree g v + fun filterFrontier s = Seq.filter (fn x => x <> itov (~1)) s + + (* val visitedRoundNums = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (visitedRoundNums, i, ~1)) *) + fun visitedRound v = sub (visitedRoundNums, v) + fun isVisited v = visitedRound v <> ~1 + fun setVisited v r = upd (visitedRoundNums, v, r) + fun claimVisited v r = + ~1 = Concurrency.casArray (visitedRoundNums, v) (~1, r) + + (* val state : 'a array = ForkJoin.alloc N *) + (* val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (state, i, initialState i)) *) + fun getState v = sub (state, v) + fun setState v s = upd (state, v, s) + + (* repeatedly try to update the state of v by CASing the result of + * computing func (u, v, su, sv) *) + fun tryPushUpdateState (u, v, su, sv) = + if cond (v, sv) then () else + case func (u, v, su, sv) of + NONE => () + | SOME desired => + let + val sv' = Concurrency.casArray (state, v) (sv, desired) + in + if eq (sv', sv) + then () + else tryPushUpdateState (u, v, su, sv') + end + + val nf = Seq.length frontier + val offsets = SeqBasis.scan GRAIN op+ 0 (0, nf) (degree o Seq.nth frontier) + val mf = sub (offsets, nf) + val outNbrs = + if shouldOutput + then ForkJoin.alloc mf + else Array.fromList [] + + fun writeOut i x = upd (outNbrs, i, x) + fun checkWriteOut i x = + if shouldOutput then writeOut i x else () + + fun visitNeighbors offset u nghs = + Util.for (0, Seq.length nghs) (fn i => + let + val v = Seq.nth nghs i + val r = visitedRound v + in + if 0 <= r andalso r < roundNum then + (* v was visited on a previous round, so ignore it. *) + checkWriteOut (offset+i) (itov (~1)) + else + ( if shouldOutput then + (if r = ~1 andalso claimVisited v roundNum then + writeOut (offset+i) v + else + writeOut (offset+i) (itov (~1))) + else + (if r = ~1 then setVisited v roundNum else ()) + + (* regardless, we need to check for updating state. *) + ; tryPushUpdateState (u, v, getState u, getState v) + ) + end) + + fun visitMany offlo lo hi = + if lo = hi then () else + let + val u = Seq.nth frontier offlo + val voffset = sub (offsets, offlo) + val k = Int.min (hi - lo, sub (offsets, offlo+1) - lo) + in + if k = 0 then visitMany (offlo+1) lo hi + else ( visitNeighbors lo u (Seq.subseq (G.neighbors g u) (lo - voffset, k)) + ; visitMany (offlo+1) (lo+k) hi + ) + end + + fun parVisitMany (offlo, offhi) (lo, hi) = + if hi - lo <= GRAIN then + visitMany offlo lo hi + else + let + val mid = lo + (hi - lo) div 2 + val (i, j) = OffsetSearch.search mid offsets (offlo, offhi) + val _ = ForkJoin.par + ( fn _ => parVisitMany (offlo, i) (lo, mid) + , fn _ => parVisitMany (j-1, offhi) (mid, hi) + ) + in + () + end + + (* Either one of the following is correct, but the second one has + * significantly better granularity control for graphs that have a + * small number of vertices with huge degree. *) + + (* val _ = ForkJoin.parfor 100 (0, nf) (fn i => + visitMany i (sub (offsets, i)) (sub (offsets, i+1))) *) + + val _ = parVisitMany (0, nf + 1) (0, mf) + in + filterFrontier (ArraySlice.full outNbrs) + end + + fun edgeMap X = + if shouldProcessDense (#graph X) (#frontier X) then + let + val (nextFrontier, tm) = Util.getTime (fn _ => edgeMapDense X) + in + print ("dense " ^ Time.fmt 4 tm ^ "\n"); + nextFrontier + end + else + let + val (nextFrontier, tm) = Util.getTime (fn _ => edgeMapSparse X) + in + print ("sparse " ^ Time.fmt 4 tm ^ "\n"); + nextFrontier + end + + fun bc graph source = + let + val g = graph + + val N = G.numVertices g + val M = G.numEdges g + + fun initialNumPaths v = + if v = source then 1 else 0 + + val visitedRoundNums = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (visitedRoundNums, i, ~1)) + + val numPathsArr = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (numPathsArr, i, initialNumPaths i)) + + (* accumulate number of paths through v *) + fun edgeFunc (u, v, uNumPaths, vNumPaths) = + uNumPaths + vNumPaths + + fun forwardsEdgeMap roundNum frontier = + edgeMap + { cond = (fn _ => false) (* accumulate all edges *) + , func = SOME o edgeFunc + , eq = op= + , frontier = frontier + , visitedRoundNums = visitedRoundNums + , roundNum = roundNum + , state = numPathsArr + , graph = g + , shouldOutput = true + } + + fun forwardsLoop pastFrontiers roundNum frontier = + if Seq.length frontier = 0 then + pastFrontiers + else + let + val nextFrontier = forwardsEdgeMap roundNum frontier + in + forwardsLoop (frontier :: pastFrontiers) (roundNum+1) nextFrontier + end + + val _ = upd (visitedRoundNums, source, 0) + val frontiers = forwardsLoop [] 1 (Seq.fromList [source]) + + val numPaths = ArraySlice.full numPathsArr + + (* val mnp = DS.reduce Int.max 0 (DS.fromArraySeq numPaths) *) + val mnp = SeqBasis.reduce 10000 Int.max 0 (0, N) (Seq.nth numPaths) + val _ = print ("max-num-paths " ^ Int.toString mnp ^ "\n") + + val lastFrontier = List.hd frontiers + + (* ===================================================================== + * second phase: search in reverse. + *) + + val invNumPaths = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => + upd (invNumPaths, i, 1.0 / Real.fromInt (sub (numPathsArr, i)))) + + val visitedRoundNums = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (visitedRoundNums, i, ~1)) + + val deps = ForkJoin.alloc N + val _ = ForkJoin.parfor 10000 (0, N) (fn i => upd (deps, i, 0.0)) + + fun edgeFunc (u, v, uDep, vDep) = + vDep + uDep + sub (invNumPaths, u) + + fun backwardsEdgeMap roundNum frontier = + edgeMap + { cond = (fn _ => false) (* accumulate all edges *) + , func = SOME o edgeFunc + , eq = Real.== + , frontier = frontier + , visitedRoundNums = visitedRoundNums + , roundNum = roundNum + , state = deps + , graph = g + , shouldOutput = false + } + + fun backwardsLoop frontiers roundNum = + case frontiers of + [] => () + | frontier :: frontiers' => + let + val _ = Seq.foreach frontier (fn (_, v) => + upd (visitedRoundNums, v, roundNum)) + + val _ = backwardsEdgeMap (roundNum+1) frontier + in + backwardsLoop frontiers' (roundNum+1) + end + + val _ = backwardsLoop frontiers 0 + in + Seq.tabulate (fn i => sub (deps, i) / sub (invNumPaths, i)) N + end + +end diff --git a/tests/centrality/OffsetSearch.sml b/tests/centrality/OffsetSearch.sml new file mode 100644 index 000000000..7e17febb8 --- /dev/null +++ b/tests/centrality/OffsetSearch.sml @@ -0,0 +1,54 @@ +structure OffsetSearch :> +sig + (* `search x xs (lo, hi)` searches the sorted array `xs` between indices `lo` + * and `hi`, returning `(i, j)` where `i-lo` is the number of elements that + * are strictly less than `x`, and `j-i` is the number of elements which are + * equal to `x`. *) + val search : int -> int array -> int * int -> int * int +end = +struct + + val sub = Array.sub + val upd = Array.update + + fun lowSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => lowSearch x xs (lo, mid) + | GREATER => lowSearch x xs (mid + 1, hi) + | EQUAL => lowSearchEq x xs (lo, mid) + end + + and lowSearchEq x xs (lo, mid) = + if (mid = 0) orelse (x > sub (xs, mid-1)) + then mid + else lowSearch x xs (lo, mid) + + and highSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => highSearch x xs (lo, mid) + | GREATER => highSearch x xs (mid + 1, hi) + | EQUAL => highSearchEq x xs (mid, hi) + end + + and highSearchEq x xs (mid, hi) = + if (mid = Array.length xs - 1) orelse (x < sub (xs, mid + 1)) + then mid + 1 + else highSearch x xs (mid + 1, hi) + + and search (x : int) (xs : int array) (lo, hi) : int * int = + case hi - lo of + 0 => (lo, lo) + | n => let val mid = lo + n div 2 + in case Int.compare (x, sub (xs, mid)) of + LESS => search x xs (lo, mid) + | GREATER => search x xs (mid + 1, hi) + | EQUAL => (lowSearchEq x xs (lo, mid), highSearchEq x xs (mid, hi)) + end + +end diff --git a/tests/centrality/centrality.mlb b/tests/centrality/centrality.mlb new file mode 100644 index 000000000..ddddceb0f --- /dev/null +++ b/tests/centrality/centrality.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +OffsetSearch.sml +BC.sml +main.sml diff --git a/tests/centrality/main.sml b/tests/centrality/main.sml new file mode 100644 index 000000000..d34146707 --- /dev/null +++ b/tests/centrality/main.sml @@ -0,0 +1,57 @@ +structure CLA = CommandLineArgs +structure BC = BC +structure G = BC.G + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val source = CLA.parseInt "source" 0 + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + +val result = Benchmark.run "running centrality" (fn _ => BC.bc graph source) + +val maxDep = + SeqBasis.reduce 10000 Real.max 0.0 (0, Seq.length result) (Seq.nth result) + (* DS.reduce Real.max 0.0 (DS.fromArraySeq result) *) +val _ = print ("maxdep " ^ Real.toString maxDep ^ "\n") + +val totDep = + SeqBasis.reduce 10000 op+ 0.0 (0, Seq.length result) (Seq.nth result) + (* DS.reduce op+ 0.0 (DS.fromArraySeq result) *) +val _ = print ("avgdep " ^ Real.toString (totDep / Real.fromInt (Seq.length result)) ^ "\n") + +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length result) + (fn i => if Seq.nth result i < 0.0 then 0 else 1) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +val outfile = CLA.parseString "outfile" "" +val _ = + if outfile = "" then + print ("use -outfile XXX to see result\n") + else + let + val n = Seq.length result + val file = TextIO.openOut outfile + fun dump i = + if i >= n then () + else (TextIO.output (file, Real.toString (Seq.nth result i)); + TextIO.output (file, "\n"); + dump (i+1)) + in + dump 0; + TextIO.closeOut file + end + diff --git a/tests/collect/CollectHash.sml b/tests/collect/CollectHash.sml new file mode 100644 index 000000000..9309c2ef0 --- /dev/null +++ b/tests/collect/CollectHash.sml @@ -0,0 +1,30 @@ +functor CollectHash (structure K: KEY structure V: VALUE): +sig + val collect: (K.t * V.t) Seq.t -> (K.t * V.t) Seq.t +end = +struct + + structure T = HashTable (structure K = K structure V = V) + + + fun collect kvs = + let + val t = T.make {capacity = Seq.length kvs} (* very rough upper bound *) + + val _ = ForkJoin.parfor 100 (0, Seq.length kvs) (fn i => + T.insert_combine t (Seq.nth kvs i)) + + val contents = T.unsafe_view_contents t + val results = + ArraySlice.full + (SeqBasis.filter 1000 (0, DelayedSeq.length contents) + (fn i => valOf (DelayedSeq.nth contents i)) + (fn i => Option.isSome (DelayedSeq.nth contents i))) + + val sorted = + Mergesort.sort (fn ((k1, v1), (k2, v2)) => K.cmp (k1, k2)) results + in + sorted + end + +end diff --git a/tests/collect/CollectSort.sml b/tests/collect/CollectSort.sml new file mode 100644 index 000000000..276b656f4 --- /dev/null +++ b/tests/collect/CollectSort.sml @@ -0,0 +1,45 @@ +functor CollectSort (structure K: KEY structure V: VALUE): +sig + val collect: (K.t * V.t) Seq.t -> (K.t * V.t) Seq.t +end = +struct + + structure Key = Int + + type key = int + + fun collect kvs = + let + val n = Seq.length kvs + + fun key (k, v) = k + fun value (k, v) = v + + fun key_cmp (kv1, kv2) = + K.cmp (key kv1, key kv2) + + val sorted = Mergesort.sort key_cmp kvs + + val boundaries = + ArraySlice.full + (SeqBasis.filter 1000 (0, Seq.length sorted) (fn i => i) (fn i => + i = 0 + orelse key_cmp (Seq.nth sorted (i - 1), Seq.nth sorted i) <> EQUAL)) + + fun make i = + let + val start = Seq.nth boundaries i + val stop = + if i + 1 = Seq.length boundaries then n + else Seq.nth (boundaries) (i + 1) + val k = key (Seq.nth sorted start) + val v = SeqBasis.reduce 1000 V.combine V.zero (start, stop) (fn j => + value (Seq.nth sorted j)) + in + (k, v) + end + in + Seq.tabulate make (Seq.length boundaries) + end + +end diff --git a/tests/collect/HashTable.sml b/tests/collect/HashTable.sml new file mode 100644 index 000000000..b467268be --- /dev/null +++ b/tests/collect/HashTable.sml @@ -0,0 +1,101 @@ +functor HashTable (structure K: KEY structure V: VALUE) = +struct + + datatype t = T of {keys: K.t array, values: V.t array} + + exception Full + exception DuplicateKey + + type table = t + + + fun make {capacity} = + let + val keys = SeqBasis.tabulate 5000 (0, capacity) (fn _ => K.empty) + val values = SeqBasis.tabulate 5000 (0, capacity) (fn _ => V.zero) + in + T {keys = keys, values = values} + end + + + fun capacity (T {keys, ...}) = Array.length keys + + + fun size (T {keys, ...}) = + SeqBasis.reduce 10000 op+ 0 (0, Array.length keys) (fn i => + if K.equal (Array.sub (keys, i), K.empty) then 0 else 1) + + + fun unsafe_view_contents (tab as T {keys, values}) = + let + val capacity = Array.length keys + + fun elem i = + let + val k = Array.sub (keys, i) + in + if K.equal (k, K.empty) then NONE else SOME (k, Array.sub (values, i)) + end + in + DelayedSeq.tabulate elem (Array.length keys) + end + + + fun bcas (arr, i, old, new) = + MLton.eq (old, Concurrency.casArray (arr, i) (old, new)) + + + fun atomic_combine_with (f: 'a * 'a -> 'a) (arr: 'a array, i) (x: 'a) = + let + fun loop current = + let + val desired = f (current, x) + in + if MLton.eq (desired, current) then + () + else + let + val current' = + MLton.Parallel.arrayCompareAndSwap (arr, i) (current, desired) + in + if MLton.eq (current', current) then () else loop current' + end + end + in + loop (Array.sub (arr, i)) + end + + + fun insert_combine (input as T {keys, values}) (x, v) = + let + val n = Array.length keys + val tolerance = n + + fun claim_slot_at i = bcas (keys, i, K.empty, x) + + fun put_value_at i = + atomic_combine_with V.combine (values, i) v + + fun loop i probes = + if probes >= tolerance then + raise Full + else if i >= n then + loop 0 probes + else + let + val k = Array.sub (keys, i) + in + if K.equal (k, K.empty) then + if claim_slot_at i then put_value_at i else loop i probes + else if K.equal (k, x) then + put_value_at i + else + loop (i + 1) (probes + 1) + end + + val start = (K.hash x) mod (Array.length keys) + in + loop start 0 + end + +end diff --git a/tests/collect/KEY.sml b/tests/collect/KEY.sml new file mode 100644 index 000000000..c27654cfa --- /dev/null +++ b/tests/collect/KEY.sml @@ -0,0 +1,8 @@ +signature KEY = +sig + type t + val equal: t * t -> bool + val cmp: t * t -> order + val empty: t + val hash: t -> int +end diff --git a/tests/collect/VALUE.sml b/tests/collect/VALUE.sml new file mode 100644 index 000000000..2b0b19065 --- /dev/null +++ b/tests/collect/VALUE.sml @@ -0,0 +1,6 @@ +signature VALUE = +sig + type t + val zero: t + val combine: t * t -> t +end diff --git a/tests/collect/collect.mlb b/tests/collect/collect.mlb new file mode 100644 index 000000000..41b672432 --- /dev/null +++ b/tests/collect/collect.mlb @@ -0,0 +1,7 @@ +../mpllib/sources.$(COMPAT).mlb +KEY.sml +VALUE.sml +HashTable.sml +CollectHash.sml +CollectSort.sml +main.sml \ No newline at end of file diff --git a/tests/collect/main.sml b/tests/collect/main.sml new file mode 100644 index 000000000..a45b76a4f --- /dev/null +++ b/tests/collect/main.sml @@ -0,0 +1,49 @@ +structure CLA = CommandLineArgs +val n = CLA.parseInt "n" 10000000 +val k = CLA.parseInt "k" 1000 +val seed = CLA.parseInt "seed" 15210 +val impl = CLA.parseString "impl" "sort" + +val _ = print ("n " ^ Int.toString n ^ "\n") +val _ = print ("k " ^ Int.toString k ^ "\n") +val _ = print ("seed " ^ Int.toString seed ^ "\n") +val _ = print ("impl " ^ impl ^ "\n") + +fun gen_real seed = + Real.fromInt (Util.hash seed mod 100000000) / 100000000.0 + +fun gen_elem i = + (Util.hash (seed + 2 * i) mod k, gen_real (seed + 2 * i + 1)) + +val kvs = Seq.tabulate gen_elem n + +structure K = +struct + type t = int + fun equal (x: t, y: t) = (x = y) + fun cmp (x, y) = Int.compare (x, y) + val empty = ~1 + val hash = Util.hash +end + +structure V = struct type t = real val zero = 0.0 val combine = Real.+ end + + +structure CollectSort = CollectSort (structure K = K structure V = V) +structure CollectHash = CollectHash (structure K = K structure V = V) + +fun bench () = + case impl of + "sort" => CollectSort.collect kvs + | "hash" => CollectHash.collect kvs + | _ => Util.die "unknown impl" + +val result = Benchmark.run "collect" bench + +val _ = print ("num unique keys: " ^ Int.toString (Seq.length result) ^ "\n") +val _ = print + ("result: " + ^ + Util.summarizeArraySlice 10 + (fn (k, v) => "(" ^ Int.toString k ^ "," ^ Real.toString v ^ ")") result + ^ "\n") diff --git a/tests/connectivity/Connectivity.sml b/tests/connectivity/Connectivity.sml new file mode 100644 index 000000000..2327c437c --- /dev/null +++ b/tests/connectivity/Connectivity.sml @@ -0,0 +1,28 @@ +structure Connectivity = +struct + type 'a seq = 'a Seq.t + + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + structure AS = ArraySlice + + type vertex = G.vertex + + (* review this code *) + fun connectivity g b = + let + val n = G.numVertices g + val (clusters, _) = LDD.ldd g b + val (g', center_label) = AdjInt.contract clusters g + in + if (G.numEdges g') = 0 then clusters + else + let + val l' = connectivity g' b + (* val _ = print ("edges = " ^ (Int.toString (G.numEdges g')) ^ " vertices = " ^ (Int.toString (G.numVertices g')) ^ "\n") *) + fun label u = center_label (Seq.nth clusters u) + in + Seq.tabulate label n + end + end +end \ No newline at end of file diff --git a/tests/connectivity/connectivity.mlb b/tests/connectivity/connectivity.mlb new file mode 100644 index 000000000..16148c348 --- /dev/null +++ b/tests/connectivity/connectivity.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +../ldd/LDD.sml +Connectivity.sml +main.sml diff --git a/tests/connectivity/main.sml b/tests/connectivity/main.sml new file mode 100644 index 000000000..468a02883 --- /dev/null +++ b/tests/connectivity/main.sml @@ -0,0 +1,73 @@ +structure CLA = CommandLineArgs +structure G = AdjacencyGraph(Int) + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + + +val b = (CommandLineArgs.parseReal "b" 0.3) + +val P = Benchmark.run "running connectivity: " (fn _ => Connectivity.connectivity graph b) +(* val _ = print ("num-triangles = " ^ (Int.toString P) ^ "\n") *) +(* val _ = LDD.check_ldd graph (#1 P) (#2 P) *) +(* val _ = Benchmark.run "running connectivity" (fn _ => LDD.connectivity graph b) *) +(* +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + +val _ = GCStats.report () *) diff --git a/tests/dedup-entangled-fixed/NondetDedup.sml b/tests/dedup-entangled-fixed/NondetDedup.sml new file mode 100644 index 000000000..73a98e925 --- /dev/null +++ b/tests/dedup-entangled-fixed/NondetDedup.sml @@ -0,0 +1,143 @@ +(* Phase-concurrent hash table based deduplication. + * See https://people.csail.mit.edu/jshun/hash.pdf. + * + * This entangled benchmark deduplicates a sequence of 64-bit integers using a + * phase-concurrent hash table implementation. The basic idea is that the hash + * table stores int options, which are heap-allocated by the thread inserting + * into the hash table. Hence, when a thread probes the table during an + * insertion, it may CAS and load an allocation made by a concurrent thread, + * thereby tripping the entanglement checker. + *) + +structure A = Array +structure AS = ArraySlice +val update = Array.update +val sub = Array.sub + +structure Hashtbl = struct + type 'a t = 'a option array * ('a -> int) * (('a * 'a) -> order) + + val gran = 10000 + + fun create hash cmp n = + let + val t = ForkJoin.alloc n + val () = ForkJoin.parfor gran (0, n) (fn i => update (t, i, NONE)) + in + (t, hash, cmp) + end + + fun insert (t, hash, cmp) xx = + let + val x = case xx of SOME x => x | NONE => raise Fail "impossible!" + val n = A.length t + fun nextIndex i = + if i = n - 1 then 0 + else i + 1 + fun hash' x = + let + val y = hash x + in + if y < 0 then ~y mod n + else y mod n + end + fun cmp' (x, y) = + case (x, y) of + (NONE, NONE) => EQUAL + | (NONE, SOME _) => LESS + | (SOME _, NONE) => GREATER + | (SOME x', SOME y') => cmp (x', y') + fun probe (i, x) = + if not (Option.isSome x) then () else + let + val y = sub (t, i) + in + case cmp' (x, y) of + EQUAL => () + | LESS => probe (nextIndex i, x) + | GREATER => + let + val z = Concurrency.casArray (t, i) (y, x) + in + if MLton.eq (y, z) then probe (nextIndex i, y) + else probe (i, x) + end + end + in + probe (hash' x, xx) + end + + fun keys (t, _, _) = + let + val n = A.length t + val t' = SeqBasis.tabFilter gran (0, n) (fn i => sub (t, i)) + in + AS.full t' + end +end + +(* val dedup : ('k -> int) hash function + -> (('k, 'k) -> order) comparison function + -> 'k seq input (with duplicates) + -> 'k seq deduplicated (not sorted!) +*) +fun dedup hash cmp keys = + if AS.length keys = 0 then Seq.empty () else + let + val n = AS.length keys + val tbl = Hashtbl.create hash cmp (4 * n) + val keys' = Seq.map SOME keys + val () = + ForkJoin.parfor 100 (0, n) (fn i => Hashtbl.insert tbl (Seq.nth keys' i)) + in + Hashtbl.keys tbl + end + +(* ========================================================================== + * now the main bit *) + +structure CLA = CommandLineArgs + +fun usage () = + let + val msg = + "usage: dedup [--verbose] [--no-output] [-N]\n" + in + TextIO.output (TextIO.stdErr, msg); + OS.Process.exit OS.Process.failure + end + +val n = CLA.parseInt "N" (100 * 1000 * 1000) + +val beVerbose = CommandLineArgs.parseFlag "verbose" +val noOutput = CommandLineArgs.parseFlag "no-output" +val rep = case (Int.fromString (CLA.parseString "repeat" "1")) of + SOME(a) => a + | NONE => 1 + +fun vprint str = + if not beVerbose then () + else TextIO.output (TextIO.stdErr, str) + +val input = Seq.tabulate Util.hash n + +fun dedupEx () = + dedup Util.hash Int.compare input + +val result = Benchmark.run "running dedup" dedupEx + +fun put c = TextIO.output1 (TextIO.stdOut, c) +val _ = + if noOutput then () + else + let + val (_, tm) = Util.getTime (fn _ => + ArraySlice.app (fn x => (print (Int.toString x); put #"\n")) result) + in + vprint ("output in " ^ Time.fmt 4 tm ^ "s\n") + end + +val _ = + if not beVerbose then () + else GCStats.report () + diff --git a/tests/dedup-entangled-fixed/dedup-entangled-fixed.mlb b/tests/dedup-entangled-fixed/dedup-entangled-fixed.mlb new file mode 100644 index 000000000..f65fd3394 --- /dev/null +++ b/tests/dedup-entangled-fixed/dedup-entangled-fixed.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +$(SML_LIB)/basis/mlton.mlb +NondetDedup.sml diff --git a/tests/dedup-entangled/NondetDedup.sml b/tests/dedup-entangled/NondetDedup.sml new file mode 100644 index 000000000..e883904cc --- /dev/null +++ b/tests/dedup-entangled/NondetDedup.sml @@ -0,0 +1,141 @@ +(* Phase-concurrent hash table based deduplication. + * See https://people.csail.mit.edu/jshun/hash.pdf. + * + * This entangled benchmark deduplicates a sequence of 64-bit integers using a + * phase-concurrent hash table implementation. The basic idea is that the hash + * table stores int options, which are heap-allocated by the thread inserting + * into the hash table. Hence, when a thread probes the table during an + * insertion, it may CAS and load an allocation made by a concurrent thread, + * thereby tripping the entanglement checker. + *) + +structure A = Array +structure AS = ArraySlice +val update = Array.update +val sub = Array.sub + +structure Hashtbl = struct + type 'a t = 'a option array * ('a -> int) * (('a * 'a) -> order) + + val gran = 10000 + + fun create hash cmp n = + let + val t = ForkJoin.alloc n + val () = ForkJoin.parfor gran (0, n) (fn i => update (t, i, NONE)) + in + (t, hash, cmp) + end + + fun insert (t, hash, cmp) x = + let + val n = A.length t + fun nextIndex i = + if i = n - 1 then 0 + else i + 1 + fun hash' x = + let + val y = hash x + in + if y < 0 then ~y mod n + else y mod n + end + fun cmp' (x, y) = + case (x, y) of + (NONE, NONE) => EQUAL + | (NONE, SOME _) => LESS + | (SOME _, NONE) => GREATER + | (SOME x', SOME y') => cmp (x', y') + fun probe (i, x) = + if not (Option.isSome x) then () else + let + val y = sub (t, i) + in + case cmp' (x, y) of + EQUAL => () + | LESS => probe (nextIndex i, x) + | GREATER => + let + val z = Concurrency.casArray (t, i) (y, x) + in + if MLton.eq (y, z) then probe (nextIndex i, y) + else probe (i, x) + end + end + in + probe (hash' x, SOME x) + end + + fun keys (t, _, _) = + let + val n = A.length t + val t' = SeqBasis.tabFilter gran (0, n) (fn i => sub (t, i)) + in + AS.full t' + end +end + +(* val dedup : ('k -> int) hash function + -> (('k, 'k) -> order) comparison function + -> 'k seq input (with duplicates) + -> 'k seq deduplicated (not sorted!) +*) +fun dedup hash cmp keys = + if AS.length keys = 0 then Seq.empty () else + let + val n = AS.length keys + val tbl = Hashtbl.create hash cmp (4 * n) + val () = + ForkJoin.parfor 100 (0, n) (fn i => Hashtbl.insert tbl (Seq.nth keys i)) + in + Hashtbl.keys tbl + end + +(* ========================================================================== + * now the main bit *) + +structure CLA = CommandLineArgs + +fun usage () = + let + val msg = + "usage: dedup [--verbose] [--no-output] [-N]\n" + in + TextIO.output (TextIO.stdErr, msg); + OS.Process.exit OS.Process.failure + end + +val n = CLA.parseInt "N" (100 * 1000 * 1000) + +val beVerbose = CommandLineArgs.parseFlag "verbose" +val noOutput = CommandLineArgs.parseFlag "no-output" +val rep = case (Int.fromString (CLA.parseString "repeat" "1")) of + SOME(a) => a + | NONE => 1 + +fun vprint str = + if not beVerbose then () + else TextIO.output (TextIO.stdErr, str) + +val input = Seq.tabulate Util.hash n + +fun dedupEx () = + dedup Util.hash Int.compare input + +val result = Benchmark.run "running dedup" dedupEx + +fun put c = TextIO.output1 (TextIO.stdOut, c) +val _ = + if noOutput then () + else + let + val (_, tm) = Util.getTime (fn _ => + ArraySlice.app (fn x => (print (Int.toString x); put #"\n")) result) + in + vprint ("output in " ^ Time.fmt 4 tm ^ "s\n") + end + +val _ = + if not beVerbose then () + else GCStats.report () + diff --git a/tests/dedup-entangled/dedup-entangled.mlb b/tests/dedup-entangled/dedup-entangled.mlb new file mode 100644 index 000000000..f65fd3394 --- /dev/null +++ b/tests/dedup-entangled/dedup-entangled.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +$(SML_LIB)/basis/mlton.mlb +NondetDedup.sml diff --git a/tests/dedup/dedup.mlb b/tests/dedup/dedup.mlb new file mode 100644 index 000000000..d70b7e467 --- /dev/null +++ b/tests/dedup/dedup.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +dedup.sml diff --git a/tests/dedup/dedup.sml b/tests/dedup/dedup.sml new file mode 100644 index 000000000..9173dadfb --- /dev/null +++ b/tests/dedup/dedup.sml @@ -0,0 +1,216 @@ +structure A = Array +structure AS = ArraySlice +val update = Array.update +val sub = Array.sub + +fun chunkedfor chunkSize (flo, fhi) f = + let + val n = fhi - flo + val numChunks = (n-1) div chunkSize + 1 + in + Util.for (0, numChunks) (fn i => + let + val clo = flo + i*chunkSize + val chi = if i = numChunks - 1 then fhi else flo + (i+1)*chunkSize + in + Util.for (clo, chi) f + end) + end + +fun chunkedloop chunkSize (flo, fhi) init f = + let + val n = fhi - flo + val numChunks = (n-1) div chunkSize + 1 + in + Util.loop (0, numChunks) init (fn (b, i) => + let + val clo = flo + i*chunkSize + val chi = if i = numChunks - 1 then fhi else flo + (i+1)*chunkSize + val b' = Util.loop (clo, chi) b f + in + b' + end) + end + +datatype 'a bucketTree = + Leaf of 'a array +| Node of int * 'a bucketTree * 'a bucketTree + +fun count t = + case t of + Leaf a => A.length a + | Node (c, _, _) => c + +fun bucketTree n (f : int -> 'a array) = + let + fun tree (lo, hi) = + case hi - lo of + 0 => Leaf (ForkJoin.alloc 0) + | 1 => Leaf (f lo) + | n => let val mid = lo + n div 2 + val (l, r) = ForkJoin.par (fn _ => tree (lo, mid), fn _ => tree (mid, hi)) + in Node (count l + count r, l, r) + end + in + tree (0, n) + end + +fun indexApp chunkSize (f : (int * 'a) -> unit) (t : 'a bucketTree) = + let + fun app offset t = + case t of + Leaf a => chunkedfor chunkSize (0, A.length a) (fn i => f (offset+i, sub (a, i))) + | Node (_, l, r) => + (ForkJoin.par (fn _ => app offset l, fn _ => app (offset + count l) r); + ()) + in + app 0 t + end + +fun compactFilter chunkSize (s : 'a option array) count = + let + val t = ForkJoin.alloc count + val _ = chunkedloop chunkSize (0, A.length s) 0 (fn (ti, si) => + case sub (s, si) of + NONE => ti + | SOME x => (update (t, ti, x); ti+1)) + in + t + end + +fun serialHistogram eq hash s = + let + val n = AS.length s + val tn = Util.boundPow2 n + val tmask = Word64.fromInt (tn - 1) + val t = Array.array (tn, NONE) + + fun insert k = + let + fun probe i = + case sub (t, i) of + NONE => (update (t, i, SOME k); true) + | SOME k' => + if eq (k', k) then + false + else if i+1 = tn then + probe 0 + else + probe (i+1) + val h = Word64.toInt (Word64.andb (hash k, tmask)) + in + probe h + end + + val (sa, slo, sn) = AS.base s + val shi = slo+sn + val count = chunkedloop 1024 (slo, shi) 0 (fn (c, i) => + if insert (sub (sa, i)) + then c+1 + else c) + in + compactFilter 1024 t count + end + + +(* val dedup : ('k * 'k -> bool) equality check + -> ('k -> Word64.word) first hash function + -> ('k -> Word64.word) second hash function + -> 'k seq input (with duplicates) + -> 'k seq deduplicated (not sorted!) +*) +fun dedup eq hash hash' keys = + if AS.length keys = 0 then Seq.empty () else + let + val n = AS.length keys + val bucketBits = + if n < Util.pow2 27 + then (Util.log2 n - 7) div 2 + else Util.log2 n - 17 + val numBuckets = Util.pow2 (bucketBits + 1) + val bucketMask = Word64.fromInt (numBuckets - 1) + fun getBucket k = Word64.toInt (Word64.andb (hash k, bucketMask)) + fun ithKeyBucket i = getBucket (Seq.nth keys i) + val (bucketed, offsets) = CountingSort.sort keys ithKeyBucket numBuckets + fun offset i = Seq.nth offsets i + val tree = bucketTree numBuckets (fn i => + let + val bucketks = Seq.subseq bucketed (offset i, offset (i+1) - offset i) + in + serialHistogram eq hash' bucketks + end) + + val result = ForkJoin.alloc (count tree) + val _ = indexApp 1024 (fn (i, x) => update (result, i, x)) tree + in + AS.full result + end + +(* ========================================================================== + * now the main bit *) + +structure CLA = CommandLineArgs + +fun usage () = + let + val msg = + "usage: dedup [--verbose] [--no-output] FILE\n" + in + TextIO.output (TextIO.stdErr, msg); + OS.Process.exit OS.Process.failure + end + +val filename = + case CLA.positional () of + [x] => x + | _ => usage () + +val beVerbose = CommandLineArgs.parseFlag "verbose" +val noOutput = CommandLineArgs.parseFlag "no-output" +val rep = case (Int.fromString (CLA.parseString "repeat" "1")) of + SOME(a) => a + | NONE => 1 + +fun toWord str = + let + (* just cap at 32 for long strings *) + val n = Int.min (32, String.size str) + fun c i = Word64.fromInt (Char.ord (String.sub (str, i))) + fun loop h i = + if i >= n then h + else loop (Word64.+ (Word64.* (h, 0w31), c i)) (i+1) + in + loop 0w7 0 + end + +fun hash1 str = Util.hash64 (toWord str) +fun hash2 str = Util.hash64 (toWord str + 0w1111111) + +fun vprint str = + if not beVerbose then () + else TextIO.output (TextIO.stdErr, str) + +val (contents, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filename) +val _ = vprint ("read file in " ^ Time.fmt 4 tm ^ "s\n") +val (tokens, tm) = Util.getTime (fn _ => Tokenize.tokens Char.isSpace contents) +val _ = vprint ("tokenized in " ^ Time.fmt 4 tm ^ "s\n") + +fun dedupEx() = + dedup op= hash1 hash2 tokens + +val result = Benchmark.run "running dedup" dedupEx + +fun put c = TextIO.output1 (TextIO.stdOut, c) +val _ = + if noOutput then () + else + let + val (_, tm) = Util.getTime (fn _ => + ArraySlice.app (fn token => (print token; put #"\n")) result) + in + vprint ("output in " ^ Time.fmt 4 tm ^ "s\n") + end + +val _ = + if not beVerbose then () + else GCStats.report () diff --git a/tests/delaunay-animation/DelaunayTriangulation.sml b/tests/delaunay-animation/DelaunayTriangulation.sml new file mode 100644 index 000000000..bdfd5543e --- /dev/null +++ b/tests/delaunay-animation/DelaunayTriangulation.sml @@ -0,0 +1,334 @@ +structure DelaunayTriangulation : +sig + type step = + { mesh: Topology2D.mesh + , updates: (Geometry2D.point * Topology2D.cavity) Seq.t + } + + val triangulate: Geometry2D.point Seq.t -> step Seq.t * Topology2D.mesh +end = +struct + + structure CLA = CommandLineArgs + + val showDelaunayRoundStats = CLA.parseFlag "show-delaunay-round-stats" + val maxBatchDiv = CLA.parseInt "max-batch-divisor" 10 + val reserveGrain = CLA.parseInt "reserve-grain" 20 + val ripAndTentGrain = CLA.parseInt "rip-and-tent-grain" 20 + val initialThreshold = CLA.parseInt "init-threshold" 10000 + val nnRebuildFactor = CLA.parseReal "nn-rebuild-factor" 10.0 + val batchSizeFrac = CLA.parseReal "batch-frac" 0.035 + + val reportTimes = false + + structure G = Geometry2D + structure T = Topology2D + structure NN = NearestNeighbors + structure A = Array + structure AS = ArraySlice + structure DSeq = DelayedSeq + + type vertex = T.vertex + type simplex = T.simplex + + type step = {mesh: T.mesh, updates: (G.point * T.cavity) Seq.t} + + val BOUNDARY_SIZE = 10 + + fun generateBoundary pts = + let + val p0 = Seq.nth pts 0 + val minCorner = Seq.reduce G.Point.minCoords p0 pts + val maxCorner = Seq.reduce G.Point.maxCoords p0 pts + val diagonal = G.Point.sub (maxCorner, minCorner) + val size = G.Vector.length diagonal + val stretch = 10.0 + val radius = stretch*size + val center = G.Vector.add (minCorner, G.Vector.scaleBy 0.5 diagonal) + + val vertexInfo = + { numVertices = Seq.length pts + BOUNDARY_SIZE + , numBoundaryVertices = BOUNDARY_SIZE + } + + val circleInfo = + {center=center, radius=radius} + in + T.initialMeshWithBoundaryCircle vertexInfo circleInfo + end + + + (* fun initialMesh pts = + let + val mesh = generateBoundary pts + + val totalNumVertices = T.numVertices boundaryMesh + Seq.length pts + val totalNumTriangles = T.numTriangles boundaryMesh + 2 * (Seq.length pts) + + val mesh = + T.new {numVertices = totalNumVertices, numTriangles = totalNumTriangles} + in + T.copyData {src = boundaryMesh, dst = mesh}; + + (mesh, T.numVertices boundaryMesh, T.numTriangles boundaryMesh) + end *) + + + fun writeMax a i x = + let + fun loop old = + if x <= old then () else + let + val old' = Concurrency.casArray (a, i) (old, x) + in + if old' = old then () + else loop old' + end + in + loop (A.sub (a, i)) + end + + + fun dsAppend (s, t) = + DSeq.tabulate (fn i => + if i < Seq.length s then + Seq.nth s i + else + Seq.nth t (i - Seq.length s)) + (Seq.length s + Seq.length t) + + + type nn = (Geometry2D.point -> vertex) + + + fun triangulate inputPts = + let + val t0 = Time.now () + + val maxBatch = Util.ceilDiv (Seq.length inputPts) maxBatchDiv + val mesh = generateBoundary inputPts + val totalNumVertices = T.numVertices mesh + + val reserved = + SeqBasis.tabulate 10000 (0, totalNumVertices) (fn _ => ~1) + + val allVertices = Seq.tabulate (fn i => i) (Seq.length inputPts) + + fun nearestSimplex nn pt = + (T.triangleOfVertex mesh (nn pt), 0) + + fun singleInsert start (id, pt) = + let + val center = #1 (T.findPoint mesh pt start) + in + T.ripAndTentCavity mesh center (id, pt) (2*id, 2*id+1) + end + + fun singleInsertLookupStart nn id = + let + val pt = Seq.nth inputPts id + in + singleInsert (nearestSimplex nn pt) (id, pt) + end + + fun batchInsert (nn: nn) (vertsToInsert: vertex DSeq.t) = + let + val m = DSeq.length vertsToInsert + + val centers = + AS.full (SeqBasis.tabulate reserveGrain (0, m) (fn i => + let + val id = DSeq.nth vertsToInsert i + val pt = Seq.nth inputPts id + val center = + #1 (T.findPoint mesh pt (nearestSimplex nn pt)) + val _ = + T.loopPerimeter mesh center pt () + (fn (_, v) => writeMax reserved v id) + in + center + end)) + + val winnerFlags = + AS.full (SeqBasis.tabulate ripAndTentGrain (0, m) (fn i => + let + val id = DSeq.nth vertsToInsert i + val pt = Seq.nth inputPts id + val center = Seq.nth centers i + val isWinner = + T.loopPerimeter mesh center pt true + (fn (allMine, v) => + if A.sub (reserved, v) = id then + (A.update (reserved, v, ~1); allMine) + else + false) + in + isWinner + end)) + + val winnerCavities = + AS.full (SeqBasis.tabFilter ripAndTentGrain (0, m) (fn i => + if not (Seq.nth winnerFlags i) then NONE else + let + val id = DSeq.nth vertsToInsert i + val pt = Seq.nth inputPts id + val center = Seq.nth centers i + in + SOME (T.findCavity mesh center pt) + end)) + + val () = + ForkJoin.parfor ripAndTentGrain (0, m) (fn i => + let + val id = DSeq.nth vertsToInsert i + val pt = Seq.nth inputPts id + val center = Seq.nth centers i + val isWinner = Seq.nth winnerFlags i + in + if not isWinner then () else + (** rip-and-tent needs to create 1 new vertex and 2 new + * triangles. The new vertex is `id`, and the new triangles + * are respectively `2*id` and `2*id+1`. This ensures unique + * names. + *) + T.ripAndTentCavity mesh center (id, pt) (2*id, 2*id+1) + end) + + val {true=winners, false=losers} = + Split.split vertsToInsert (DSeq.fromArraySeq winnerFlags) + in + (winners, losers, winnerCavities) + end + + fun shouldRebuild numNextRebuild numDone = + let + val n = Seq.length inputPts + in + numDone >= numNextRebuild + andalso + numDone <= Real.floor (Real.fromInt n / nnRebuildFactor) + end + + fun buildNN (done: vertex Seq.t) = + let + val pts = Seq.map (Seq.nth inputPts) done + val tree = NN.makeTree 16 pts + in + (fn pt => Seq.nth done (NN.nearestNeighbor tree pt)) + end + + fun doRebuildNN numNextRebuild doneVertices = + let + val nn = buildNN doneVertices + val numNextRebuild = + Real.ceil (Real.fromInt numNextRebuild * nnRebuildFactor) + in + if not showDelaunayRoundStats then () else + print ("rebuilt nn; next rebuild at " ^ Int.toString numNextRebuild ^ "\n"); + + (nn, numNextRebuild) + end + + + (** start by inserting points one-by-one until mesh is large enough *) + fun smallLoop numDone (nn, numNextRebuild) remaining = + if numDone >= initialThreshold orelse Seq.length remaining = 0 then + (numDone, nn, numNextRebuild, remaining) + else + let + val (id, remaining) = + (Seq.nth remaining 0, Seq.drop remaining 1) + val _ = singleInsertLookupStart nn id + val numDone = numDone+1 + + val (nn, numNextRebuild) = + if not (shouldRebuild numNextRebuild numDone) then + (nn, numNextRebuild) + else + doRebuildNN numNextRebuild (Seq.take allVertices numDone) + in + smallLoop numDone (nn, numNextRebuild) remaining + end + + + fun loop numRounds steps (done, numDone) (nn, numNextRebuild) losers remaining = + if numDone = Seq.length inputPts then + (numRounds, Seq.fromList (List.rev steps)) + else + let + val startMesh = T.copy mesh + + val numRetry = Seq.length losers + val totalRemaining = numRetry + Seq.length remaining + (* val numDone = Seq.length inputPts - totalRemaining *) + val desiredSize = + Int.min (maxBatch, Int.min (totalRemaining, + 1 + Real.round (Real.fromInt numDone * batchSizeFrac))) + val numAdditional = + Int.max (0, Int.min (desiredSize - numRetry, Seq.length remaining)) + val thisBatchSize = numAdditional + numRetry + + val newcomers = Seq.take remaining numAdditional + val remaining = Seq.drop remaining numAdditional + val (winners, losers, winnerCavities) = + batchInsert nn (dsAppend (losers, newcomers)) + + val thisStep = + { mesh = startMesh + , updates = + Seq.zip (Seq.map (Seq.nth inputPts) winners, winnerCavities) + } + val steps = thisStep :: steps + + val numSucceeded = thisBatchSize - Seq.length losers + val numDone = numDone + numSucceeded + val done = winners :: done + + val rate = Real.fromInt numSucceeded / Real.fromInt thisBatchSize + val pcRate = Real.round (100.0 * rate) + + val _ = + if not showDelaunayRoundStats then () else + print ("round " ^ Int.toString numRounds + ^ "\tdone " ^ Int.toString numDone + ^ "\tremaining " ^ Int.toString totalRemaining + ^ "\tdesired " ^ Int.toString desiredSize + ^ "\tretrying " ^ Int.toString numRetry + ^ "\tfresh " ^ Int.toString numAdditional + ^ "\tsuccess-rate " ^ Int.toString pcRate ^ "%\n") + + val (done, (nn, numNextRebuild)) = + if not (shouldRebuild numNextRebuild numDone) then + (done, (nn, numNextRebuild)) + else + let + val done = Seq.flatten (Seq.fromList done) + in + ([done], doRebuildNN numNextRebuild done) + end + in + loop (numRounds+1) steps (done, numDone) (nn, numNextRebuild) losers remaining + end + + val start: simplex = (2 * Seq.length inputPts, 0) + val _ = singleInsert start (0, Seq.nth inputPts 0) + val done = Seq.singleton 0 + val remaining = Seq.drop allVertices 1 + val numDone = 1 + + val nn = buildNN done + val numNextRebuild = 10 + + val (numDone, nn, numNextRebuild, remaining) = + smallLoop numDone (nn, numNextRebuild) remaining + + val done = [Seq.take allVertices numDone] + + val (numRounds, steps) = + loop 0 [] (done, numDone) (nn, numNextRebuild) (Seq.empty()) remaining + + in + (steps, mesh) + end + +end diff --git a/tests/delaunay-animation/Split.sml b/tests/delaunay-animation/Split.sml new file mode 100644 index 000000000..61f0c0f22 --- /dev/null +++ b/tests/delaunay-animation/Split.sml @@ -0,0 +1,73 @@ +structure Split: +sig + type 'a dseq + type 'a seq + val split: 'a dseq -> bool dseq -> {true: 'a seq, false: 'a seq} +end = +struct + + structure A = Array + structure AS = ArraySlice + + structure DS = DelayedSeq + type 'a dseq = 'a DS.t + type 'a seq = 'a AS.slice + + fun split s flags = + let + val n = DS.length s + val blockSize = 2000 + val numBlocks = 1 + (n-1) div blockSize + + (* the later scan(s) appears to be faster when split into two separate + * scans, rather than doing a single scan on tuples. *) + + (* val counts = Primitives.alloc numBlocks *) + val countl = ForkJoin.alloc numBlocks + val countr = ForkJoin.alloc numBlocks + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + fun loop (cl, cr) i = + if i >= hi then + (* A.update (counts, b, (cl, cr)) *) + (A.update (countl, b, cl); A.update (countr, b, cr)) + else if DS.nth flags i then + loop (cl+1, cr) (i+1) + else + loop (cl, cr+1) (i+1) + in + loop (0, 0) lo + end) + + (* val (offsets, (totl, totr)) = + Seq.scan (fn ((a,b),(c,d)) => (a+c,b+d)) (0,0) (ArraySlice.full counts) *) + val (offsetsl, totl) = Seq.scan op+ 0 (AS.full countl) + val (offsetsr, totr) = Seq.scan op+ 0 (AS.full countr) + + val left = ForkJoin.alloc totl + val right = ForkJoin.alloc totr + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + (* val (offsetl, offsetr) = Seq.nth offsets b *) + val offsetl = Seq.nth offsetsl b + val offsetr = Seq.nth offsetsr b + fun loop (cl, cr) i = + if i >= hi then () + else if DS.nth flags i then + (A.update (left, offsetl+cl, DS.nth s i); loop (cl+1, cr) (i+1)) + else + (A.update (right, offsetr+cr, DS.nth s i); loop (cl, cr+1) (i+1)) + in + loop (0, 0) lo + end) + in + {true = AS.full left, false = AS.full right} + end + +end diff --git a/tests/delaunay-animation/delaunay-animation.mlb b/tests/delaunay-animation/delaunay-animation.mlb new file mode 100644 index 000000000..295de819c --- /dev/null +++ b/tests/delaunay-animation/delaunay-animation.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +Split.sml +DelaunayTriangulation.sml +main.sml diff --git a/tests/delaunay-animation/main.sml b/tests/delaunay-animation/main.sml new file mode 100644 index 000000000..bfbd4d3ab --- /dev/null +++ b/tests/delaunay-animation/main.sml @@ -0,0 +1,222 @@ +structure CLA = CommandLineArgs +structure T = Topology2D +structure DT = DelaunayTriangulation + +val n = CLA.parseInt "n" (1000 * 1000) +val seed = CLA.parseInt "seed" 15210 +val filename = CLA.parseString "input" "" + +fun generateInputPoints () = + let + fun genReal i = + let + val x = Word64.fromInt (seed + i) + in + Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 x, 0w1000000))) + / 1000000.0 + end + + fun genPoint i = (genReal (2*i), genReal (2*i + 1)) + + val (points, tm) = Util.getTime (fn _ => Seq.tabulate genPoint n) + val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + in + points + end + +fun parseInputFile () = + let + val (points, tm) = Util.getTime (fn _ => + ParseFile.readSequencePoint2d filename) + in + print ("parsed input points in " ^ Time.fmt 4 tm ^ "s\n"); + points + end + + +val input = + case filename of + "" => generateInputPoints () + | _ => parseInputFile () + + +val (steps, mesh) = Benchmark.run "delaunay" (fn _ => DT.triangulate input) +val _ = print ("num rounds " ^ Int.toString (Seq.length steps) ^ "\n") + +(* val _ = + print ("\n" ^ T.toString mesh ^ "\n") *) + + +(* ========================================================================== + * output result image + * only works if all input points are in range [0,1) + *) + +val filename = CLA.parseString "output" "" +val _ = + if filename <> "" then () + else ( print ("\nto see output, use -output, -resolution, and -fps arguments\n" ^ + "for example: delaunay -n 1000 -output result.gif -resolution 1000 -fps 10.0\n") + ; OS.Process.exit OS.Process.success + ) + +val t0 = Time.now () + +val resolution = CLA.parseInt "resolution" 1000 +val fadeEdgeMargin = CLA.parseInt "fade-edge" 0 +val borderEdgeMargin = CLA.parseInt "border-edge" 0 +val fps = CLA.parseReal "fps" 10.0 + +val niceBackground: Color.color = + let + val c = Real.fromInt 0xFD / 255.0 + in + {red = c, green = c, blue = c, alpha = 1.0} + end +val niceBackgroundPx: Color.pixel = Color.colorToPixel niceBackground + +(* val bg = #fdfdfd *) + +(* val image = MeshToImage.toImage {mesh = mesh, resolution = resolution} *) + +(* fun makeRed b = + let + val c = Word8.fromInt (Real.ceil (255.0 * (1.0 - b))) + val color = {blue = c, green = c, red = 0w255} + in + color + end + +fun makeGray b = + Color.colorToPixel ({red = 0.5, blue = 0.5, green = 0.5, alpha = b}) *) + +(* val niceGray = Color.hsv {h=0.0, s=0.0, v=0.88} *) +(* val niceRed = Color.colorToPixel + (Color.hsva {h = 0.0, s = 0.55, v = 0.95, a = 0.8}) *) +(* val colors = [Color.white, Color.black, Color.red] +val palette = + GIF.Palette.summarizeBySampling colors 103 + (fn i => + if i < 50 then + (* 50 shades of gray *) + makeGray (Real.fromInt (1 + i mod 50) / 50.0) + else + (* ... and 50 shades of red *) + makeRed (Real.fromInt (1 + i mod 50) / 50.0)) *) + +fun fadeEdges margin (img as {width, height, data}) = + let + fun clampedDistFromZero x = + Real.fromInt (margin - (Int.max (0, margin - x))) / Real.fromInt margin + fun hbrightness col = + Real.min (clampedDistFromZero col, clampedDistFromZero (width - col - 1)) + fun vbrightness row = + Real.min (clampedDistFromZero row, clampedDistFromZero (height - row - 1)) + fun brightness (row, col) = + Real.max (0.0, Real.min (hbrightness col, vbrightness row)) + fun update (row, col) = + if 0 <= row andalso row < height andalso 0 <= col andalso col < width then + let + val px = Seq.nth data (row*width + col) + val {red, green, blue, ...} = Color.pixelToColor px + val alpha = brightness (row, col) + val px' = Color.colorToPixel (Color.overlayColor + { fg = {red=red, green=green, blue=blue, alpha=alpha} + , bg = niceBackground + }) + in + ArraySlice.update (data, row*width + col, px') + end + else () + + fun updateBox {topleft=(row0, col0), botright=(row1, col1)} = + Util.for (row0, row1) (fn row => + Util.for (col0, col1) (fn col => + update (row, col) + )) + in + updateBox {topleft = (0, 0), botright = (margin, width)}; + updateBox {topleft = (margin, 0), botright = (height-margin, margin)}; + updateBox {topleft = (margin, width-margin), botright = (height-margin, width)}; + updateBox {topleft = (height-margin, 0), botright = (height, width)}; + + img + end + +fun drawBorder margin (img as {width, height, data}) = + let + fun update (row, col) = + if 0 <= row andalso row < height andalso 0 <= col andalso col < width then + let + in + ArraySlice.update (data, row*width + col, Color.black) + end + else () + + fun updateBox {topleft=(row0, col0), botright=(row1, col1)} = + Util.for (row0, row1) (fn row => + Util.for (col0, col1) (fn col => + update (row, col) + )) + in + updateBox {topleft = (0, 0), botright = (margin, width)}; + updateBox {topleft = (margin, 0), botright = (height-margin, margin)}; + updateBox {topleft = (margin, width-margin), botright = (height-margin, width)}; + updateBox {topleft = (height-margin, 0), botright = (height, width)}; + + img + end + +(* val numImages = 2 * (Seq.length steps) + 1 *) +(* val numImages = 3 *) +val numImages = Seq.length steps + 1 +val images = SeqBasis.tabulate 1 (0, numImages) (fn i => + let + val j = i (*div 2*) + val mesh = + if j < Seq.length steps then #mesh (Seq.nth steps j) else mesh + val cavs = + if (*i mod 2 = 1 andalso*) j < Seq.length steps then + SOME (#updates (Seq.nth steps j)) + else + NONE + val img = + MeshToImage.toImage {mesh = mesh, resolution = resolution, cavities = cavs, background = niceBackground} + in + if fadeEdgeMargin > 0 then + fadeEdges fadeEdgeMargin img + else if borderEdgeMargin > 0 then + drawBorder borderEdgeMargin img + else + img + end) + +val t1 = Time.now () + +val _ = print ("generated frames in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val _ = print ("writing to " ^ filename ^"...\n") + +val palette = + GIF.Palette.summarizeBySampling [niceBackgroundPx, Color.black, Color.red] 256 + (fn i => + let + val j1 = Util.hash (2*i) mod (Array.length images) + val img = Array.sub (images, j1) + val j2 = Util.hash (2*i + 1) mod (Seq.length (#data img)) + in + Seq.nth (#data img) j2 + end) + +val msBetween = Real.round ((1.0 / fps) * 100.0) +val (_, tm) = Util.getTime (fn _ => + GIF.writeMany filename msBetween palette + { width = resolution + , height = resolution + , numImages = Array.length images + , getImage = fn i => #remap palette (Array.sub (images, i)) + }) +val _ = print ("wrote all frames in " ^ Time.fmt 4 tm ^ "s\n") + +(* val (_, tm) = Util.getTime (fn _ => PPM.write "first.ppm" (Array.sub (images, 1))) +val _ = print ("wrote to " ^ "first.ppm" ^ " in " ^ Time.fmt 4 tm ^ "s\n") *) diff --git a/tests/delaunay-animation/test.sml b/tests/delaunay-animation/test.sml new file mode 100644 index 000000000..a82638d33 --- /dev/null +++ b/tests/delaunay-animation/test.sml @@ -0,0 +1,125 @@ +structure CLA = CommandLineArgs +structure T = Topology2D +structure DT = DelaunayTriangulation + +val (filename, testPtStr) = + case CLA.positional () of + [x, y] => (x, y) + | _ => Util.die "usage: ./foo " + +val testType = CLA.parseString "test" "split" + +val testPoint = + case List.mapPartial Real.fromString (String.tokens (fn c => c = #",") testPtStr) of + [x,y] => (x,y) + | _ => Util.die ("bad test point") + +val (mesh, tm) = Util.getTime (fn _ => T.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (T.numVertices mesh) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (T.numTriangles mesh) ^ "\n") + +val _ = print ("\n" ^ T.toString mesh ^ "\n\n") + +val start: T.simplex = (0, 0) + +fun simpToString (t, i) = + "triangle " ^ Int.toString t ^ " orientation " ^ Int.toString i ^ ": " ^ + let + val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t + in + String.concatWith " " (List.map Int.toString + (if i = 0 then [a,b,c] + else if i = 1 then [b,c,a] + else [c,a,b])) + end + +fun triToString t = + "triangle " ^ Int.toString t ^ ": " ^ + let + val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t + in + String.concatWith " " (List.map Int.toString [a,b,c]) + end + +val _ = Util.for (0, T.numVertices mesh) (fn v => + let + val s = T.find mesh v start + in + print ("found " ^ Int.toString v ^ ": " ^ simpToString s ^ "\n") + end) + + +(* ======================================================================== *) + +fun testSplit () = + let + val _ = print ("===================================\nTESTING SPLIT\n") + + val ((center, tris), verts) = T.findCavityAndPerimeter mesh start testPoint + val _ = + print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") + val _ = + print ("CAVITY MEMBERS ARE:\n" + ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") + val _ = + print ("CAVITY PERIMETER VERTICES ARE:\n " + ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") + + + val mesh' = T.split mesh center testPoint + val _ = + print ("===================================\nAFTER SPLIT:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +fun testFlip () = + let + val _ = print ("===================================\nTESTING FLIP\n") + + val simp = T.findPoint mesh testPoint start + val _ = + print ("SIMPLEX CONTAINING POINT:\n " ^ simpToString simp ^ "\n") + + val mesh' = T.flip mesh simp + val _ = + print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +fun testRipAndTent () = + let + val _ = print ("===================================\nTESTING RIP-AND-TENT\n") + + val (cavity as (center, tris), verts) = + T.findCavityAndPerimeter mesh start testPoint + + val _ = + print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") + val _ = + print ("CAVITY MEMBERS ARE:\n" + ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") + val _ = + print ("CAVITY PERIMETER VERTICES ARE:\n " + ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") + + val mesh' = T.ripAndTentOne (cavity, testPoint) mesh + val _ = + print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +val _ = + case testType of + "split" => testSplit () + | "flip" => testFlip () + | "rip-and-tent" => testRipAndTent () + | _ => Util.die ("unknown test type") diff --git a/tests/delaunay-mostly-pure/DelaunayTriangulation.sml b/tests/delaunay-mostly-pure/DelaunayTriangulation.sml new file mode 100644 index 000000000..8f071b116 --- /dev/null +++ b/tests/delaunay-mostly-pure/DelaunayTriangulation.sml @@ -0,0 +1,191 @@ +structure DelaunayTriangulation : +sig + val triangulate: Geometry2D.point Seq.t -> Topology2D.mesh +end = +struct + + val showDelaunayRoundStats = + CommandLineArgs.parseFlag "show-delaunay-round-stats" + + structure G = Geometry2D + structure T = Topology2D + structure NN = NearestNeighbors + structure A = Array + structure AS = ArraySlice + + type vertex = T.vertex + type simplex = T.simplex + + val BOUNDARY_SIZE = 10 + + fun generateBoundary pts = + let + val p0 = Seq.nth pts 0 + val minCorner = Seq.reduce G.Point.minCoords p0 pts + val maxCorner = Seq.reduce G.Point.maxCoords p0 pts + val diagonal = G.Point.sub (maxCorner, minCorner) + val size = G.Vector.length diagonal + val stretch = 10.0 + val radius = stretch*size + val center = G.Vector.add (minCorner, G.Vector.scaleBy 0.5 diagonal) + in + T.boundaryCircle {center=center, radius=radius, size=BOUNDARY_SIZE} + end + + + fun writeMax a i x = + let + fun loop old = + if x <= old then () else + let + val old' = Concurrency.casArray (a, i) (old, x) + in + if old' = old then () + else loop old' + end + in + loop (A.sub (a, i)) + end + + + fun reserveForInsert reserved mesh start (pt, id) = + let + val (cavity, perimeter) = + T.findCavityAndPerimeter mesh start pt + in + List.app (fn v => writeMax reserved v id) perimeter; + (cavity, perimeter) + end + + + fun resetAndCheckPerimeter reserved perimeter id = + List.foldl + (fn (v, allMine) => + if A.sub (reserved, v) = id then + (A.update (reserved, v, ~1); allMine) + else + false) + true + perimeter + + + fun triangulate inputPts = + let + val maxBatch = Util.ceilDiv (Seq.length inputPts) 100 + val totalNumVertices = BOUNDARY_SIZE + Seq.length inputPts + val reserved = + SeqBasis.tabulate 10000 (0, totalNumVertices) (fn _ => ~1) + + fun batchInsert mesh (nn: NN.t) pointsToInsert = + let + val n = T.numVertices mesh + val m = Seq.length pointsToInsert + + val attempts = + AS.full (SeqBasis.tabulate 100 (0, m) (fn i => + let + val pt = Seq.nth pointsToInsert i + val start: simplex = + (T.triangleOfVertex mesh (NN.nearestNeighbor nn pt), 0) + in + reserveForInsert reserved mesh start + (Seq.nth pointsToInsert i, i) + end)) + + val winnerFlags = + AS.full (SeqBasis.tabulate 1000 (0, m) (fn i => + let + val (_, perimeter) = Seq.nth attempts i + in + resetAndCheckPerimeter reserved perimeter i + end)) + + val winners = + AS.full (SeqBasis.tabFilter 1000 (0, m) (fn i => + if Seq.nth winnerFlags i then + SOME (#1 (Seq.nth attempts i), Seq.nth pointsToInsert i) + else + NONE)) + + val losers = + AS.full (SeqBasis.filter 1000 (0, m) + (Seq.nth pointsToInsert) + (not o Seq.nth winnerFlags)) + + val mesh' = T.ripAndTent winners mesh + in + (mesh', losers) + end + + val nnRebuildMultiplier = 10 + + fun shouldRebuild numNextRebuild totalRemaining = + let + val n = Seq.length inputPts + val numDone = n - totalRemaining + in + numDone >= numNextRebuild + andalso + numDone <= n div nnRebuildMultiplier + end + + + fun loop numRounds mesh (nn: NN.t) numNextRebuild losers remaining = + if Seq.length losers + Seq.length remaining = 0 then + (numRounds, mesh) + else if shouldRebuild numNextRebuild (Seq.length losers + Seq.length remaining) then + let + val nn = NN.makeTree 16 (T.getPoints mesh) + val numNextRebuild = numNextRebuild * nnRebuildMultiplier + in + if not showDelaunayRoundStats then () else + print ("rebuilt nn; next rebuild at " ^ Int.toString numNextRebuild ^ "\n"); + + loop numRounds mesh nn numNextRebuild losers remaining + end + else + let + val numRetry = Seq.length losers + val totalRemaining = numRetry + Seq.length remaining + val numDone = Seq.length inputPts - totalRemaining + val desiredSize = + Int.min (maxBatch, Int.min (1 + numDone div 50, Seq.length inputPts - numDone)) + val numAdditional = + Int.max (0, Int.min (desiredSize - numRetry, Seq.length remaining)) + val thisBatchSize = numAdditional + numRetry + + val newcomers = Seq.take remaining numAdditional + val remaining = Seq.drop remaining numAdditional + val (mesh, losers) = + batchInsert mesh nn (Seq.append (losers, newcomers)) + + val rate = + Real.round (100.0 * (Real.fromInt (thisBatchSize - Seq.length losers) + / Real.fromInt thisBatchSize)) + + val _ = + if not showDelaunayRoundStats then () else + print ("round " ^ Int.toString numRounds + ^ "\tdone " ^ Int.toString numDone + ^ "\tremaining " ^ Int.toString totalRemaining + ^ "\tdesired " ^ Int.toString desiredSize + ^ "\tretrying " ^ Int.toString numRetry + ^ "\tfresh " ^ Int.toString numAdditional + ^ "\tsuccess-rate " ^ Int.toString rate ^ "%\n") + in + loop (numRounds+1) mesh nn numNextRebuild losers remaining + end + + val initialMesh = generateBoundary inputPts + val initialNN = NN.makeTree 16 (T.getPoints initialMesh) + val numNextRebuild = 100 + + val (numRounds, finalMesh) = + loop 0 initialMesh initialNN numNextRebuild (Seq.empty()) inputPts + + val _ = print ("num rounds " ^ Int.toString numRounds ^ "\n") + in + finalMesh + end + +end diff --git a/tests/delaunay-mostly-pure/delaunay-mostly-pure.mlb b/tests/delaunay-mostly-pure/delaunay-mostly-pure.mlb new file mode 100644 index 000000000..dcf033eb3 --- /dev/null +++ b/tests/delaunay-mostly-pure/delaunay-mostly-pure.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +DelaunayTriangulation.sml +main.sml diff --git a/tests/delaunay-mostly-pure/main.sml b/tests/delaunay-mostly-pure/main.sml new file mode 100644 index 000000000..6fdaaef2e --- /dev/null +++ b/tests/delaunay-mostly-pure/main.sml @@ -0,0 +1,126 @@ +structure CLA = CommandLineArgs +structure T = Topology2D +structure DT = DelaunayTriangulation + +val n = CLA.parseInt "n" (1000 * 1000) +val seed = CLA.parseInt "seed" 15210 + +fun genReal i = + let + val x = Word64.fromInt (seed + i) + in + Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 x, 0w1000000))) + / 1000000.0 + end + +fun genPoint i = (genReal (2*i), genReal (2*i + 1)) +val (input, tm) = Util.getTime (fn _ => Seq.tabulate genPoint n) +val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +val mesh = Benchmark.run "delaunay" (fn _ => DT.triangulate input) + +(* val _ = + print ("\n" ^ T.toString mesh ^ "\n") *) + + +(* ========================================================================== + * output result image + * only works if all input points are in range [0,1) + *) + +val filename = CLA.parseString "output" "" +val _ = + if filename <> "" then () + else ( print ("to see output, use -output and -resolution arguments\n" ^ + "for example: delaunay -n 1000 -output result.ppm -resolution 1000\n") + ; OS.Process.exit OS.Process.success + ) + +val t0 = Time.now () + +val resolution = CLA.parseInt "resolution" 1000 +val width = resolution +val height = resolution + +val image = + { width = width + , height = height + , data = Seq.tabulate (fn _ => Color.white) (width*height) + } + +fun set (i, j) x = + if 0 <= i andalso i < height andalso + 0 <= j andalso j < width + then ArraySlice.update (#data image, i*width + j, x) + else () + +val r = Real.fromInt resolution +fun px x = Real.floor (x * r) +fun pos (x, y) = (resolution - px x - 1, px y) + +fun horizontalLine i (j0, j1) = + if j1 < j0 then horizontalLine i (j1, j0) + else Util.for (j0, j1) (fn j => set (i, j) Color.red) + +fun sign xx = + case Int.compare (xx, 0) of LESS => ~1 | EQUAL => 0 | GREATER => 1 + +(* Bresenham's line algorithm *) +fun line (x1, y1) (x2, y2) = + let + val w = x2 - x1 + val h = y2 - y1 + val dx1 = sign w + val dy1 = sign h + val (longest, shortest, dx2, dy2) = + if Int.abs w > Int.abs h then + (Int.abs w, Int.abs h, dx1, 0) + else + (Int.abs h, Int.abs w, 0, dy1) + + fun loop i numerator x y = + if i > longest then () else + let + val numerator = numerator + shortest; + in + set (x, y) Color.red; + if numerator >= longest then + loop (i+1) (numerator-longest) (x+dx1) (y+dy1) + else + loop (i+1) numerator (x+dx2) (y+dy2) + end + in + loop 0 (longest div 2) x1 y1 + end + +(* draw all triangle edges as straight red lines *) +val _ = ForkJoin.parfor 1000 (0, T.numTriangles mesh) (fn i => + let + fun vpos v = pos (T.vdata mesh v) + val T.Tri {vertices=(u,v,w), ...} = T.tdata mesh i + in + line (vpos u) (vpos v); + line (vpos v) (vpos w); + line (vpos w) (vpos u) + end) + +(* mark input points as a pixel *) +val _ = + ForkJoin.parfor 10000 (0, Seq.length input) (fn i => + let + val (x, y) = pos (Seq.nth input i) + fun b spot = set spot Color.black + in + b (x-1, y); + b (x, y-1); + b (x, y); + b (x, y+1); + b (x+1, y) + end) + +val t1 = Time.now () + +val _ = print ("generated image in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val (_, tm) = Util.getTime (fn _ => PPM.write filename image) +val _ = print ("wrote to " ^ filename ^ " in " ^ Time.fmt 4 tm ^ "s\n") diff --git a/tests/delaunay-mostly-pure/test.sml b/tests/delaunay-mostly-pure/test.sml new file mode 100644 index 000000000..a82638d33 --- /dev/null +++ b/tests/delaunay-mostly-pure/test.sml @@ -0,0 +1,125 @@ +structure CLA = CommandLineArgs +structure T = Topology2D +structure DT = DelaunayTriangulation + +val (filename, testPtStr) = + case CLA.positional () of + [x, y] => (x, y) + | _ => Util.die "usage: ./foo " + +val testType = CLA.parseString "test" "split" + +val testPoint = + case List.mapPartial Real.fromString (String.tokens (fn c => c = #",") testPtStr) of + [x,y] => (x,y) + | _ => Util.die ("bad test point") + +val (mesh, tm) = Util.getTime (fn _ => T.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (T.numVertices mesh) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (T.numTriangles mesh) ^ "\n") + +val _ = print ("\n" ^ T.toString mesh ^ "\n\n") + +val start: T.simplex = (0, 0) + +fun simpToString (t, i) = + "triangle " ^ Int.toString t ^ " orientation " ^ Int.toString i ^ ": " ^ + let + val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t + in + String.concatWith " " (List.map Int.toString + (if i = 0 then [a,b,c] + else if i = 1 then [b,c,a] + else [c,a,b])) + end + +fun triToString t = + "triangle " ^ Int.toString t ^ ": " ^ + let + val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t + in + String.concatWith " " (List.map Int.toString [a,b,c]) + end + +val _ = Util.for (0, T.numVertices mesh) (fn v => + let + val s = T.find mesh v start + in + print ("found " ^ Int.toString v ^ ": " ^ simpToString s ^ "\n") + end) + + +(* ======================================================================== *) + +fun testSplit () = + let + val _ = print ("===================================\nTESTING SPLIT\n") + + val ((center, tris), verts) = T.findCavityAndPerimeter mesh start testPoint + val _ = + print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") + val _ = + print ("CAVITY MEMBERS ARE:\n" + ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") + val _ = + print ("CAVITY PERIMETER VERTICES ARE:\n " + ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") + + + val mesh' = T.split mesh center testPoint + val _ = + print ("===================================\nAFTER SPLIT:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +fun testFlip () = + let + val _ = print ("===================================\nTESTING FLIP\n") + + val simp = T.findPoint mesh testPoint start + val _ = + print ("SIMPLEX CONTAINING POINT:\n " ^ simpToString simp ^ "\n") + + val mesh' = T.flip mesh simp + val _ = + print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +fun testRipAndTent () = + let + val _ = print ("===================================\nTESTING RIP-AND-TENT\n") + + val (cavity as (center, tris), verts) = + T.findCavityAndPerimeter mesh start testPoint + + val _ = + print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") + val _ = + print ("CAVITY MEMBERS ARE:\n" + ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") + val _ = + print ("CAVITY PERIMETER VERTICES ARE:\n " + ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") + + val mesh' = T.ripAndTentOne (cavity, testPoint) mesh + val _ = + print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +val _ = + case testType of + "split" => testSplit () + | "flip" => testFlip () + | "rip-and-tent" => testRipAndTent () + | _ => Util.die ("unknown test type") diff --git a/tests/delaunay-top-down/DelaunayTriangulationTopDown.sml b/tests/delaunay-top-down/DelaunayTriangulationTopDown.sml new file mode 100644 index 000000000..5d56035bd --- /dev/null +++ b/tests/delaunay-top-down/DelaunayTriangulationTopDown.sml @@ -0,0 +1,191 @@ +functor DelaunayTriangulationTopDown (structure R: REAL structure I: INTEGER) = +struct + + fun par3 (f1, f2, f3) = + let val (a, (b, c)) = ForkJoin.par (f1, fn _ => ForkJoin.par (f2, f3)) + in (a, b, c) + end + + type id = I.int + + fun r (x: real) : R.real = + R.fromLarge IEEEReal.TO_NEAREST (Real.toLarge x) + + fun ii x = I.fromInt x + + structure Point = + struct + datatype t = T of {id: id, x: R.real, y: R.real} + + fun x (T p) = #x p + fun y (T p) = #y p + fun id (T p) = #id p + fun id_cmp (p1, p2) = + I.compare (id p1, id p2) + fun id_less_than (p1, p2) = + I.< (id p1, id p2) + + type vec = LargeReal.real * LargeReal.real * LargeReal.real + + fun cross ((x1, y1, z1): vec, (x2, y2, z2): vec) = + (y1 * z2 - z1 * y2, z1 * x2 - x1 * z2, x1 * y2 - y1 * x2) + + fun dot ((x1, y1, z1): vec, (x2, y2, z2): vec) = + x1 * x2 + y1 * y2 + z1 * z2 + + fun project (d: t) (p: t) = + let + val px = R.toLarge (R.- (x p, x d)) + val py = R.toLarge (R.- (y p, y d)) + in + (px, py, LargeReal.+ (LargeReal.* (px, px), LargeReal.* (py, py))) + end + + fun in_circle (a: t, b: t, d: t) = + let val cp = cross (project d a, project d b) + in fn c => dot (cp, project d c) > 0.0 + end + end + + type tri = id * id * id + val dummy_tri = (ii ~1, ii ~1, ii ~1) + + type edge = id * id + val dummy_edge = (ii ~1, ii ~1) + + fun itow64 i = + Word64.fromLarge (LargeWord.fromInt (I.toInt i)) + + structure EH = + HashTable + (struct + type t = edge + val equal = op= + val default = dummy_edge + fun hash (i1, i2) = + Word64.toIntX (Word64.xorb + (Util.hash64 (itow64 i1), Util.hash64_2 (itow64 i2))) + end) + + structure TH = + HashTable + (struct + type t = tri + val equal = op= + val default = dummy_tri + fun hash (i1, i2, i3) = + Word64.toIntX + (Word64.xorb + ( Util.hash64 (itow64 i1) + , Word64.xorb (Util.hash64_2 (itow64 i2), Util.hash64_2 + (Util.hash64_2 (itow64 i3))) + )) + end) + + + type triangle = {tri: tri, conflicts: Point.t Seq.t} + + + fun filter_points points (t1: triangle, t2: triangle, t: tri) = + let + val a = Merge.merge Point.id_cmp (#conflicts t1, #conflicts t2) + val an = Seq.length a + + fun lookup_point id = + Seq.nth points (I.toInt id) + + val is_in_circle = Point.in_circle + (lookup_point (#1 t), lookup_point (#2 t), lookup_point (#3 t)) + + fun same_id i j = + Point.id_cmp (Seq.nth a i, Seq.nth a j) = EQUAL + + fun keep i = + (i <> 0) andalso not (same_id i (i - 1)) + andalso + ((i + 1 < an andalso same_id i (i + 1)) + orelse is_in_circle (Seq.nth a i)) + in + ArraySlice.full (SeqBasis.filter 2000 (0, an) (Seq.nth a) keep) + end + + + (* ~2/3 max load with a bit of slop *) + fun sloppy_capacity max_expected_elems = + 100 + (max_expected_elems * 3) div 2 + + + fun triangulate (points: Point.t Seq.t) = + let + val n = Seq.length points + + fun earliest ({tri, conflicts}: triangle) = + if Seq.length conflicts = 0 then ii n + else Point.id (Seq.nth conflicts 0) + + val edges = EH.make + { default = {tri = dummy_tri, conflicts = Seq.fromList []} + , capacity = sloppy_capacity (6 * n) + } + + val mesh = TH.make {default = (), capacity = sloppy_capacity (2 * n)} + + (* enclosing triangle *) + val p0 = Point.T {id = ii n, x = r 0.0, y = r 100.0} + val p1 = Point.T {id = ii (n + 1), x = r 100.0, y = r ~100.0} + val p2 = Point.T {id = ii (n + 2), x = r ~100.0, y = r ~100.0} + val enclosing_t = + {tri = (ii n, ii (n + 1), ii (n + 2)), conflicts = points} + + val all_points = Seq.append (points, Seq.fromList [p0, p1, p2]) + + fun process_edge (t1: triangle, e: edge, t2: triangle) = + if Seq.length (#conflicts t1) = 0 andalso Seq.length (#conflicts t2) = 0 then + (TH.insert mesh (#tri t1, ()); TH.insert mesh (#tri t2, ()); ()) + else if earliest t1 = earliest t2 then + () + else + let + val (t1, e, t2) = + if I.<= (earliest t1, earliest t2) then (t1, e, t2) + else (t2, (#2 e, #1 e), t1) + + val p = earliest t1 + val t = (#1 e, #2 e, p) + val t1 = {tri = t, conflicts = filter_points all_points (t1, t2, t)} + in + par3 + ( fn _ => check_edge ((p, #1 e), t1) + , fn _ => check_edge ((#2 e, p), t1) + , fn _ => process_edge (t1, e, t2) + ); + () + end + + + and check_edge (e: edge, tp: triangle) = + let + val key = if I.< (#1 e, #2 e) then e else (#2 e, #1 e) + in + if EH.insert edges (key, tp) then + () + else + case EH.remove edges key of + NONE => raise Fail "impossible?" + | SOME tt => process_edge (tp, e, tt) + end + + + val t = enclosing_t + val te = {tri = dummy_tri, conflicts = Seq.empty ()} + val _ = + par3 + ( fn _ => process_edge (t, (ii n, ii (n + 1)), te) + , fn _ => process_edge (t, (ii (n + 1), ii (n + 2)), te) + , fn _ => process_edge (t, (ii (n + 2), ii n), te) + ) + in + TH.keys mesh + end + +end diff --git a/tests/delaunay-top-down/HashTable.sml b/tests/delaunay-top-down/HashTable.sml new file mode 100644 index 000000000..1aa884596 --- /dev/null +++ b/tests/delaunay-top-down/HashTable.sml @@ -0,0 +1,190 @@ +functor HashTable + (K: + sig + type t + val equal: t * t -> bool + val default: t + val hash: t -> int + end) = +struct + + structure Status = + struct + type t = Word8.word + val empty: t = 0w0 + val full: t = 0w1 + val locked: t = 0w2 + val tomb: t = 0w3 + end + + + datatype 'a entry = E of {status: Status.t ref, key: K.t ref, value: 'a ref} + + datatype 'a t = T of 'a entry array + type 'a table = 'a t + + exception Full + + fun make {default: 'a, capacity} : 'a table = + let + fun default_entry _ = + E {status = ref Status.empty, key = ref K.default, value = ref default} + val entries = SeqBasis.tabulate 5000 (0, capacity) default_entry + in + T entries + end + + + fun capacity (T entries) = Array.length entries + + fun status (T entries) i = + let val E {status, ...} = Array.sub (entries, i) + in status + end + + + fun size (t as T entries) = + SeqBasis.reduce 5000 op+ 0 (0, Array.length entries) (fn i => + if !(status t i) = Status.full then 0 else 1) + + + fun keys (t as T entries) = + let + fun key_at i = + let val E {key, ...} = Array.sub (entries, i) + in !key + end + + fun keep_at i = + !(status t i) = Status.full + in + ArraySlice.full + (SeqBasis.filter 2000 (0, Array.length entries) key_at keep_at) + end + + + (* fun unsafe_view_contents (tab as T {keys, values}) = + let + val capacity = Array.length keys + + fun elem i = + let + val k = Array.sub (keys, i) + in + if K.equal (k, K.default) then NONE + else SOME (k, Array.sub (values, i)) + end + in + DelayedSeq.tabulate elem (Array.length keys) + end *) + + + fun bcas (r, old, new) = + MLton.eq (old, MLton.Parallel.compareAndSwap r (old, new)) + + + (* fun atomic_combine_with (f: 'a * 'a -> 'a) (arr: 'a array, i) (x: 'a) = + let + fun loop current = + let + val desired = f (current, x) + in + if MLton.eq (desired, current) then + () + else + let + val current' = + MLton.Parallel.arrayCompareAndSwap (arr, i) (current, desired) + in + if MLton.eq (current', current) then () else loop current' + end + end + in + loop (Array.sub (arr, i)) + end *) + + + fun insert (t as T entries) (k, v) = + let + val n = Array.length entries + val tolerance = n + + fun claim_slot_at i expected = + bcas (status t i, expected, Status.locked) + + fun put_kv_at i = + let val E {status, key, value} = Array.sub (entries, i) + in key := k; value := v; bcas (status, Status.locked, Status.full) + end + + fun put_v_at i = + let val E {status, key, value} = Array.sub (entries, i) + in value := v; bcas (status, Status.locked, Status.full) + end + + fun loop i probes = + if probes >= tolerance then + raise Full + else if i >= n then + loop 0 probes + else + let + val e as E {status, key, value} = Array.sub (entries, i) + val s = !status + in + if s = Status.full orelse s = Status.tomb then + if K.equal (!key, k) then + if s = Status.full then false + else if claim_slot_at i s then (put_v_at i; true) + else loop i probes + else + loop (i + 1) (probes + 1) + else if s = Status.empty then + if claim_slot_at i s then (put_kv_at i; true) else loop i probes + else + loop (i + 1) (probes + 1) + end + + val start = (K.hash k) mod (Array.length entries) + in + loop start 0 + end + + + fun remove (t as T entries) k = + let + val n = Array.length entries + val tolerance = n + + fun release_slot_at i = + bcas (status t i, Status.full, Status.tomb) + + fun loop i probes = + if probes >= tolerance then + raise Full + else if i >= n then + loop 0 probes + else + let + val e as E {status, key, value} = Array.sub (entries, i) + val s = !status + in + if s = Status.empty orelse s = Status.locked then + NONE + else if K.equal (!key, k) then + if s = Status.full then + let val v = !value + in release_slot_at i; SOME v + end + else + NONE + else + loop (i + 1) (probes + 1) + end + + val start = (K.hash k) mod (Array.length entries) + in + loop start 0 + end + +end diff --git a/tests/delaunay-top-down/README.md b/tests/delaunay-top-down/README.md new file mode 100644 index 000000000..404e93dcb --- /dev/null +++ b/tests/delaunay-top-down/README.md @@ -0,0 +1,28 @@ +This "top down" Delaunay implementation is a direct translation of this file: +https://github.com/cmuparlay/parlaylib/blob/e1f1dc0ccf930492a2723f7fbef8510d35bf57f5/examples/delaunay.h + +It is interesting algorithmically, but not +especially fast. I would be curious to see how well it performs in comparison +to the original C++ code. + +In comparison to the [MaPLe PBBS `delaunay`](https://github.com/MPLLang/parallel-ml-bench/tree/main/mpl/bench/delaunay), it is significantly slower +and less space efficient. I believe this is partly due to the use of two +(somewhat unoptimized) global hash tables. One stores the mesh of triangles, +and the other stores a set of outstanding edges that may need to be processed. +Note that the use of hash tables in this way results in memory entanglement, +which is another source of overhead. Disentangling the hash table access +patterns would be interesting to look into; it's not immediately clear to me if +this is possible. + +Some basic tests have been run but more testing is required. + +```bash +[parallel-ml-bench/mpl]$ make delaunay-top-down.mpl.bin +[parallel-ml-bench/mpl]$ bin/delaunay-top-down.mpl.bin @mpl procs 4 -- -n 10000 -output result.ppm -resolution 2000 + +# ... see image in result.ppm +``` + +**NOTE (9/2/25)**: Currently, there is an unresolved race condition which +sometimes results in portions of the triangulation being dropped. It may be a +bug in the hash table implementation. \ No newline at end of file diff --git a/tests/delaunay-top-down/delaunay-top-down.mlb b/tests/delaunay-top-down/delaunay-top-down.mlb new file mode 100644 index 000000000..890647c66 --- /dev/null +++ b/tests/delaunay-top-down/delaunay-top-down.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +HashTable.sml +DelaunayTriangulationTopDown.sml +main.sml diff --git a/tests/delaunay-top-down/main.sml b/tests/delaunay-top-down/main.sml new file mode 100644 index 000000000..779d173fd --- /dev/null +++ b/tests/delaunay-top-down/main.sml @@ -0,0 +1,231 @@ +structure CLA = CommandLineArgs +structure T = Topology2D +structure R = Real32 +structure I = Int32 +structure DT = DelaunayTriangulationTopDown (structure R = R structure I = I) + +val n = CLA.parseInt "n" (1000 * 1000) +val seed = CLA.parseInt "seed" 15210 +val filename = CLA.parseString "input" "" + +fun generateInputPoints () = + let + fun genReal i = + let + val x = Word64.fromInt (seed + i) + in + Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 x, 0w1000000))) + / 1000000.0 + end + + fun genPoint i = + (genReal (2 * i), genReal (2 * i + 1)) + + val (points, tm) = Util.getTime (fn _ => Seq.tabulate genPoint n) + val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + in + points + end + +(* This silly thing helps ensure good placement, by + * forcing points to be reallocated more adjacent. + * It's a no-op, but gives us as much as 2x time + * improvement (!) + *) +fun swap pts = + Seq.map (fn (x, y) => (y, x)) pts +fun compactify pts = + swap (swap pts) + +fun parseInputFile () = + let + val (points, tm) = Util.getTime (fn _ => + compactify (ParseFile.readSequencePoint2d filename)) + in + print ("parsed input points in " ^ Time.fmt 4 tm ^ "s\n"); + points + end + + +val points = + case filename of + "" => generateInputPoints () + | _ => parseInputFile () + +val input = + Seq.mapIdx + (fn (i, (x, y)) => DT.Point.T {id = I.fromInt i, x = DT.r x, y = DT.r y}) + points + +val mesh = Benchmark.run "delaunay-top-down" (fn _ => DT.triangulate input) +val _ = print ("num triangles " ^ Int.toString (Seq.length mesh) ^ "\n") + +(* ===================================================================== *) + +val filename = CLA.parseString "output" "" +val _ = + if filename <> "" then + () + else + ( print + ("\nto see output, use -output and -resolution arguments\n" + ^ + "for example: delaunay-top-down -n 1000 -output result.ppm -resolution 1000\n") + ; OS.Process.exit OS.Process.success + ) + +val t0 = Time.now () + +val resolution = CLA.parseInt "resolution" 1000 +val width = resolution +val height = resolution + +val image = + { width = width + , height = height + , data = Seq.tabulate (fn _ => Color.white) (width * height) + } + +fun set (i, j) x = + if 0 <= i andalso i < height andalso 0 <= j andalso j < width then + ArraySlice.update (#data image, i * width + j, x) + else + () + +fun setxy (x, y) z = + set (resolution - y - 1, x) z + +val r = Real.fromInt resolution +fun px x = + Real.floor (x * r + 0.5) + +fun ipart x = Real.floor x +fun fpart x = x - Real.realFloor x +fun rfpart x = 1.0 - fpart x + +(** input points should be in range [0,1] *) +fun aaLine (x0, y0) (x1, y1) = + if x1 < x0 then + aaLine (x1, y1) (x0, y0) + else + let + (** scale to resolution *) + val (x0, y0, x1, y1) = + (r * x0 + 0.5, r * y0 + 0.5, r * x1 + 0.5, r * y1 + 0.5) + + fun plot (x, y, c) = + let + val c = Word8.fromInt (Real.ceil (255.0 * (1.0 - c))) + val color = {blue = c, green = c, red = 0w255} + in + (* print (Int.toString x ^ " " ^ Int.toString y ^ "\n"); *) + setxy (x, y) color + end + + val dx = x1 - x0 + val dy = y1 - y0 + val yxSlope = dy / dx + val xySlope = dx / dy + (* val xhop = Real.fromInt (Real.sign dx) *) + (* val yhop = Real.fromInt (Real.sign dy) *) + + (* fun y x = x0 + (x-x0) * slope *) + + (** (x,y) = current point on the line *) + fun normalLoop (x, y) = + if x > x1 then + () + else + ( plot (ipart x, ipart y, rfpart y) + ; plot (ipart x, ipart y + 1, fpart y) + ; normalLoop (x + 1.0, y + yxSlope) + ) + + fun steepUpLoop (x, y) = + if y > y1 then + () + else + ( plot (ipart x, ipart y, rfpart x) + ; plot (ipart x + 1, ipart y, fpart x) + ; steepUpLoop (x + xySlope, y + 1.0) + ) + + fun steepDownLoop (x, y) = + if y < y1 then + () + else + ( plot (ipart x, ipart y, rfpart x) + ; plot (ipart x + 1, ipart y, fpart x) + ; steepDownLoop (x - xySlope, y - 1.0) + ) + in + if Real.abs dx > Real.abs dy then normalLoop (x0, y0) + else if y1 > y0 then steepUpLoop (x0, y0) + else steepDownLoop (x0, y0) + end + +(* draw all triangle edges as straight red lines *) +val _ = ForkJoin.parfor 1000 (0, Seq.length mesh) (fn i => + let + (* val _ = print ("triangle number " ^ Int.toString i ^ "\n") *) + (** cut off anything that is outside the image (not important other than + * a little faster this way). + *) + fun constrain (x, y) = + (Real.min (1.0, Real.max (0.0, x)), Real.min (1.0, Real.max (0.0, y))) + + (* fun vpos v = constrain (T.vdata mesh v) + fun doLineIf b (u, v) = + if b then aaLine (vpos u) (vpos v) else () *) + + fun doLineIf _ (id1, id2) = + let + val id1 = I.toInt id1 + val id2 = I.toInt id2 + in + if + id1 >= Seq.length points orelse id2 >= Seq.length points + orelse id1 < 0 orelse id2 < 0 + then + () + else + aaLine (constrain (Seq.nth points id1)) (constrain + (Seq.nth points id2)) + end + + val (u, v, w) = Seq.nth mesh i + (* val () = print + ("drawing " ^ I.toString u ^ " " ^ I.toString v ^ " " ^ I.toString w + ^ "\n") *) + in + (** TODO: ensure each line segment is only drawn once? There is overlap + * here with adjacent triangles. + *) + doLineIf true (w, u); + doLineIf true (u, v); + doLineIf true (v, w) + end + handle e => (print ("error at " ^ Int.toString i ^ "\n"); raise e)) + +val _ = print ("drew all triangles\n") + +(* mark input points as a pixel *) +val _ = ForkJoin.parfor 10000 (0, Seq.length points) (fn i => + let + val (x, y) = Seq.nth points i + val (x, y) = (px x, px y) + fun b spot = setxy spot Color.black + in + b (x - 1, y); + b (x, y - 1); + b (x, y); + b (x, y + 1); + b (x + 1, y) + end) + +val t1 = Time.now () + +val _ = print ("generated image in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val (_, tm) = Util.getTime (fn _ => PPM.write filename image) +val _ = print ("wrote to " ^ filename ^ " in " ^ Time.fmt 4 tm ^ "s\n") diff --git a/tests/delaunay/DelaunayTriangulation.sml b/tests/delaunay/DelaunayTriangulation.sml new file mode 100644 index 000000000..e02d6f011 --- /dev/null +++ b/tests/delaunay/DelaunayTriangulation.sml @@ -0,0 +1,368 @@ +structure DelaunayTriangulation : +sig + val triangulate: Geometry2D.point Seq.t -> int * Topology2D.mesh +end = +struct + + structure CLA = CommandLineArgs + + val showDelaunayRoundStats = CLA.parseFlag "show-delaunay-round-stats" + val maxBatchDiv = CLA.parseInt "max-batch-divisor" 10 + val reserveGrain = CLA.parseInt "reserve-grain" 20 + val ripAndTentGrain = CLA.parseInt "rip-and-tent-grain" 20 + val initialThreshold = CLA.parseInt "init-threshold" 10000 + val nnRebuildFactor = CLA.parseReal "nn-rebuild-factor" 10.0 + val batchSizeFrac = CLA.parseReal "batch-frac" 0.035 + + val reportTimes = CLA.parseFlag "report-delaunay-times" + + structure G = Geometry2D + structure T = Topology2D + structure NN = NearestNeighbors + structure A = Array + structure AS = ArraySlice + structure DSeq = DelayedSeq + + type vertex = T.vertex + type simplex = T.simplex + + val BOUNDARY_SIZE = 10 + + fun generateBoundary pts = + let + val p0 = Seq.nth pts 0 + val minCorner = Seq.reduce G.Point.minCoords p0 pts + val maxCorner = Seq.reduce G.Point.maxCoords p0 pts + val diagonal = G.Point.sub (maxCorner, minCorner) + val size = G.Vector.length diagonal + val stretch = 10.0 + val radius = stretch*size + val center = G.Vector.add (minCorner, G.Vector.scaleBy 0.5 diagonal) + + val vertexInfo = + { numVertices = Seq.length pts + BOUNDARY_SIZE + , numBoundaryVertices = BOUNDARY_SIZE + } + + val circleInfo = + {center=center, radius=radius} + in + T.initialMeshWithBoundaryCircle vertexInfo circleInfo + end + + + (* fun initialMesh pts = + let + val mesh = generateBoundary pts + + val totalNumVertices = T.numVertices boundaryMesh + Seq.length pts + val totalNumTriangles = T.numTriangles boundaryMesh + 2 * (Seq.length pts) + + val mesh = + T.new {numVertices = totalNumVertices, numTriangles = totalNumTriangles} + in + T.copyData {src = boundaryMesh, dst = mesh}; + + (mesh, T.numVertices boundaryMesh, T.numTriangles boundaryMesh) + end *) + + + fun writeMax a i x = + let + fun loop old = + if x <= old then () else + let + val old' = Concurrency.casArray (a, i) (old, x) + in + if old' = old then () + else loop old' + end + in + loop (A.sub (a, i)) + end + + + fun dsAppend (s, t) = + DSeq.tabulate (fn i => + if i < Seq.length s then + Seq.nth s i + else + Seq.nth t (i - Seq.length s)) + (Seq.length s + Seq.length t) + + + type timers = (string * real ref) list + val timers = + [ ("reserve", ref 0.0) + , ("rip-and-tent", ref 0.0) + , ("split", ref 0.0) + , ("nn-rebuild", ref 0.0) + ] + + fun clearTimers () = + List.app (fn (_, t) => t := 0.0) timers + + fun updateTimer timers (name, tm) = + case timers of + [] => () + | (s, t) :: rest => + if name = s then + t := (!t + Time.toReal tm) + else + updateTimer rest (name, tm) + val updateTimer = updateTimer timers + + fun timed name f = + if not reportTimes then f () else + let + val (result, tm) = Util.getTime f + in + updateTimer (name, tm); + result + end + + fun getTimer timers name = + case timers of + [] => 0.0 + | (s, t) :: rest => if s = name then !t else getTimer rest name + val getTimer = getTimer timers + + fun timerSum () = + List.foldl (fn ((_, tm), t) => t + !tm) 0.0 timers + + + type nn = (Geometry2D.point -> vertex) + + + fun triangulate inputPts = + let + val t0 = Time.now () + + val maxBatch = Util.ceilDiv (Seq.length inputPts) maxBatchDiv + val mesh = generateBoundary inputPts + val totalNumVertices = T.numVertices mesh + + val reserved = + SeqBasis.tabulate 10000 (0, totalNumVertices) (fn _ => ~1) + + val allVertices = Seq.tabulate (fn i => i) (Seq.length inputPts) + + fun nearestSimplex nn pt = + (T.triangleOfVertex mesh (nn pt), 0) + + fun singleInsert start (id, pt) = + let + val center = #1 (T.findPoint mesh pt start) + in + T.ripAndTentCavity mesh center (id, pt) (2*id, 2*id+1) + end + + fun singleInsertLookupStart nn id = + let + val pt = Seq.nth inputPts id + in + singleInsert (nearestSimplex nn pt) (id, pt) + end + + fun batchInsert (nn: nn) (vertsToInsert: vertex DSeq.t) = + let + val m = DSeq.length vertsToInsert + + val centers = timed "reserve" (fn _ => + AS.full (SeqBasis.tabulate reserveGrain (0, m) (fn i => + let + val id = DSeq.nth vertsToInsert i + val pt = Seq.nth inputPts id + val center = + #1 (T.findPoint mesh pt (nearestSimplex nn pt)) + val _ = + T.loopPerimeter mesh center pt () + (fn (_, v) => writeMax reserved v id) + in + center + end))) + + val winnerFlags = timed "rip-and-tent" (fn _ => + AS.full (SeqBasis.tabulate ripAndTentGrain (0, m) (fn i => + let + val id = DSeq.nth vertsToInsert i + val pt = Seq.nth inputPts id + val center = Seq.nth centers i + val isWinner = + T.loopPerimeter mesh center pt true + (fn (allMine, v) => + if A.sub (reserved, v) = id then + (A.update (reserved, v, ~1); allMine) + else + false) + in + if not isWinner then () else + (** rip-and-tent needs to create 1 new vertex and 2 new + * triangles. The new vertex is `id`, and the new triangles + * are respectively `2*id` and `2*id+1`. This ensures unique + * names. + *) + T.ripAndTentCavity mesh center (id, pt) (2*id, 2*id+1); + + isWinner + end))) + + val {true=winners, false=losers} = timed "split" (fn _ => + Split.split vertsToInsert (DSeq.fromArraySeq winnerFlags)) + in + (winners, losers) + end + + fun shouldRebuild numNextRebuild numDone = + let + val n = Seq.length inputPts + in + numDone >= numNextRebuild + andalso + numDone <= Real.floor (Real.fromInt n / nnRebuildFactor) + end + + fun buildNN (done: vertex Seq.t) = + let + val pts = Seq.map (Seq.nth inputPts) done + val tree = NN.makeTree 16 pts + in + (fn pt => Seq.nth done (NN.nearestNeighbor tree pt)) + end + + fun doRebuildNN numNextRebuild doneVertices = + let + val nn = timed "nn-rebuild" (fn _ => buildNN doneVertices) + val numNextRebuild = + Real.ceil (Real.fromInt numNextRebuild * nnRebuildFactor) + in + if not showDelaunayRoundStats then () else + print ("rebuilt nn; next rebuild at " ^ Int.toString numNextRebuild ^ "\n"); + + (nn, numNextRebuild) + end + + + (** start by inserting points one-by-one until mesh is large enough *) + fun smallLoop numDone (nn, numNextRebuild) remaining = + if numDone >= initialThreshold orelse Seq.length remaining = 0 then + (numDone, nn, numNextRebuild, remaining) + else + let + val (id, remaining) = + (Seq.nth remaining 0, Seq.drop remaining 1) + val _ = singleInsertLookupStart nn id + val numDone = numDone+1 + + val (nn, numNextRebuild) = + if not (shouldRebuild numNextRebuild numDone) then + (nn, numNextRebuild) + else + doRebuildNN numNextRebuild (Seq.take allVertices numDone) + in + smallLoop numDone (nn, numNextRebuild) remaining + end + + + fun loop numRounds (done, numDone) (nn, numNextRebuild) losers remaining = + if numDone = Seq.length inputPts then + numRounds + else + let + val numRetry = Seq.length losers + val totalRemaining = numRetry + Seq.length remaining + (* val numDone = Seq.length inputPts - totalRemaining *) + val desiredSize = + Int.min (maxBatch, Int.min (totalRemaining, + 1 + Real.round (Real.fromInt numDone * batchSizeFrac))) + val numAdditional = + Int.max (0, Int.min (desiredSize - numRetry, Seq.length remaining)) + val thisBatchSize = numAdditional + numRetry + + val newcomers = Seq.take remaining numAdditional + val remaining = Seq.drop remaining numAdditional + val (winners, losers) = + batchInsert nn (dsAppend (losers, newcomers)) + + val numSucceeded = thisBatchSize - Seq.length losers + val numDone = numDone + numSucceeded + val done = winners :: done + + val rate = Real.fromInt numSucceeded / Real.fromInt thisBatchSize + val pcRate = Real.round (100.0 * rate) + + val _ = + if not showDelaunayRoundStats then () else + print ("round " ^ Int.toString numRounds + ^ "\tdone " ^ Int.toString numDone + ^ "\tremaining " ^ Int.toString totalRemaining + ^ "\tdesired " ^ Int.toString desiredSize + ^ "\tretrying " ^ Int.toString numRetry + ^ "\tfresh " ^ Int.toString numAdditional + ^ "\tsuccess-rate " ^ Int.toString pcRate ^ "%\n") + + val (done, (nn, numNextRebuild)) = + if not (shouldRebuild numNextRebuild numDone) then + (done, (nn, numNextRebuild)) + else + let + val done = Seq.flatten (Seq.fromList done) + in + ([done], doRebuildNN numNextRebuild done) + end + in + loop (numRounds+1) (done, numDone) (nn, numNextRebuild) losers remaining + end + + val start: simplex = (2 * Seq.length inputPts, 0) + val _ = singleInsert start (0, Seq.nth inputPts 0) + val done = Seq.singleton 0 + val remaining = Seq.drop allVertices 1 + val numDone = 1 + + (* val _ = print "inserted first\n" *) + + val nn = buildNN done + val numNextRebuild = 10 + + (* val _ = print ("built initial nn\n") *) + + val (numDone, nn, numNextRebuild, remaining) = + smallLoop numDone (nn, numNextRebuild) remaining + + (* val _ = print ("finished small loop\n") *) + + val done = [Seq.take allVertices numDone] + + val numRounds = + loop 0 (done, numDone) (nn, numNextRebuild) (Seq.empty()) remaining + + val t1 = Time.now () + val elapsed = Time.toReal (Time.- (t1, t0)) + + fun percent x = Real.round (100.0 * (x / elapsed)) + fun rtos x = Real.fmt (StringCvt.FIX (SOME 4)) x + fun stuff x = + rtos x ^ "s (" ^ Int.toString (percent x) ^ "%)" + + val _ = + if not reportTimes then () else + let + val _ = print ("----\n") + (* val _ = print ("find-centers " ^ stuff (getTimer "find-centers") ^ "\n") *) + val _ = print ("reserve " ^ stuff (getTimer "reserve") ^ "\n") + val _ = print ("rip-and-tent " ^ stuff (getTimer "rip-and-tent") ^ "\n") + val _ = print ("split " ^ stuff (getTimer "split") ^ "\n") + val _ = print ("nn-rebuild " ^ stuff (getTimer "nn-rebuild") ^ "\n") + val _ = print ("other " ^ stuff (elapsed - timerSum ()) ^ "\n") + + val _ = clearTimers () + in + () + end + + in + (numRounds, mesh) + end + +end diff --git a/tests/delaunay/Split.sml b/tests/delaunay/Split.sml new file mode 100644 index 000000000..61f0c0f22 --- /dev/null +++ b/tests/delaunay/Split.sml @@ -0,0 +1,73 @@ +structure Split: +sig + type 'a dseq + type 'a seq + val split: 'a dseq -> bool dseq -> {true: 'a seq, false: 'a seq} +end = +struct + + structure A = Array + structure AS = ArraySlice + + structure DS = DelayedSeq + type 'a dseq = 'a DS.t + type 'a seq = 'a AS.slice + + fun split s flags = + let + val n = DS.length s + val blockSize = 2000 + val numBlocks = 1 + (n-1) div blockSize + + (* the later scan(s) appears to be faster when split into two separate + * scans, rather than doing a single scan on tuples. *) + + (* val counts = Primitives.alloc numBlocks *) + val countl = ForkJoin.alloc numBlocks + val countr = ForkJoin.alloc numBlocks + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + fun loop (cl, cr) i = + if i >= hi then + (* A.update (counts, b, (cl, cr)) *) + (A.update (countl, b, cl); A.update (countr, b, cr)) + else if DS.nth flags i then + loop (cl+1, cr) (i+1) + else + loop (cl, cr+1) (i+1) + in + loop (0, 0) lo + end) + + (* val (offsets, (totl, totr)) = + Seq.scan (fn ((a,b),(c,d)) => (a+c,b+d)) (0,0) (ArraySlice.full counts) *) + val (offsetsl, totl) = Seq.scan op+ 0 (AS.full countl) + val (offsetsr, totr) = Seq.scan op+ 0 (AS.full countr) + + val left = ForkJoin.alloc totl + val right = ForkJoin.alloc totr + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + (* val (offsetl, offsetr) = Seq.nth offsets b *) + val offsetl = Seq.nth offsetsl b + val offsetr = Seq.nth offsetsr b + fun loop (cl, cr) i = + if i >= hi then () + else if DS.nth flags i then + (A.update (left, offsetl+cl, DS.nth s i); loop (cl+1, cr) (i+1)) + else + (A.update (right, offsetr+cr, DS.nth s i); loop (cl, cr+1) (i+1)) + in + loop (0, 0) lo + end) + in + {true = AS.full left, false = AS.full right} + end + +end diff --git a/tests/delaunay/delaunay.mlb b/tests/delaunay/delaunay.mlb new file mode 100644 index 000000000..295de819c --- /dev/null +++ b/tests/delaunay/delaunay.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +Split.sml +DelaunayTriangulation.sml +main.sml diff --git a/tests/delaunay/main.sml b/tests/delaunay/main.sml new file mode 100644 index 000000000..128684434 --- /dev/null +++ b/tests/delaunay/main.sml @@ -0,0 +1,207 @@ +structure CLA = CommandLineArgs +structure T = Topology2D +structure DT = DelaunayTriangulation + +val n = CLA.parseInt "n" (1000 * 1000) +val seed = CLA.parseInt "seed" 15210 +val filename = CLA.parseString "input" "" + +fun generateInputPoints () = + let + fun genReal i = + let + val x = Word64.fromInt (seed + i) + in + Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 x, 0w1000000))) + / 1000000.0 + end + + fun genPoint i = (genReal (2*i), genReal (2*i + 1)) + + val (points, tm) = Util.getTime (fn _ => Seq.tabulate genPoint n) + val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + in + points + end + +(* This silly thing helps ensure good placement, by + * forcing points to be reallocated more adjacent. + * It's a no-op, but gives us as much as 2x time + * improvement (!) + *) +fun swap pts = Seq.map (fn (x, y) => (y, x)) pts +fun compactify pts = swap (swap pts) + +fun parseInputFile () = + let + val (points, tm) = Util.getTime (fn _ => + compactify (ParseFile.readSequencePoint2d filename)) + in + print ("parsed input points in " ^ Time.fmt 4 tm ^ "s\n"); + points + end + + +val input = + case filename of + "" => generateInputPoints () + | _ => parseInputFile () + + +val (numRounds, mesh) = Benchmark.run "delaunay" (fn _ => DT.triangulate input) +val _ = print ("num rounds " ^ Int.toString numRounds ^ "\n") + + +(* val _ = + print ("\n" ^ T.toString mesh ^ "\n") *) + + +(* ========================================================================== + * output result image + * only works if all input points are in range [0,1) + *) + +val filename = CLA.parseString "output" "" +val _ = + if filename <> "" then () + else ( print ("\nto see output, use -output and -resolution arguments\n" ^ + "for example: delaunay -n 1000 -output result.ppm -resolution 1000\n") + ; OS.Process.exit OS.Process.success + ) + +val t0 = Time.now () + +val resolution = CLA.parseInt "resolution" 1000 +val width = resolution +val height = resolution + +val image = + { width = width + , height = height + , data = Seq.tabulate (fn _ => Color.white) (width*height) + } + +fun set (i, j) x = + if 0 <= i andalso i < height andalso + 0 <= j andalso j < width + then ArraySlice.update (#data image, i*width + j, x) + else () + +fun setxy (x, y) z = + set (resolution - y - 1, x) z + +val r = Real.fromInt resolution +fun px x = Real.floor (x * r + 0.5) + +fun ipart x = Real.floor x +fun fpart x = x - Real.realFloor x +fun rfpart x = 1.0 - fpart x + +(** input points should be in range [0,1] *) +fun aaLine (x0, y0) (x1, y1) = + if x1 < x0 then aaLine (x1, y1) (x0, y0) else + let + (** scale to resolution *) + val (x0, y0, x1, y1) = (r*x0 + 0.5, r*y0 + 0.5, r*x1 + 0.5, r*y1 + 0.5) + + fun plot (x, y, c) = + let + val c = Word8.fromInt (Real.ceil (255.0 * (1.0 - c))) + val color = {blue = c, green = c, red = 0w255} + in + (* print (Int.toString x ^ " " ^ Int.toString y ^ "\n"); *) + setxy (x, y) color + end + + val dx = x1-x0 + val dy = y1-y0 + val yxSlope = dy / dx + val xySlope = dx / dy + (* val xhop = Real.fromInt (Real.sign dx) *) + (* val yhop = Real.fromInt (Real.sign dy) *) + + (* fun y x = x0 + (x-x0) * slope *) + + (** (x,y) = current point on the line *) + fun normalLoop (x, y) = + if x > x1 then () else + ( plot (ipart x, ipart y , rfpart y) + ; plot (ipart x, ipart y + 1, fpart y) + ; normalLoop (x + 1.0, y + yxSlope) + ) + + fun steepUpLoop (x, y) = + if y > y1 then () else + ( plot (ipart x , ipart y, rfpart x) + ; plot (ipart x + 1, ipart y, fpart x) + ; steepUpLoop (x + xySlope, y + 1.0) + ) + + fun steepDownLoop (x, y) = + if y < y1 then () else + ( plot (ipart x , ipart y, rfpart x) + ; plot (ipart x + 1, ipart y, fpart x) + ; steepDownLoop (x - xySlope, y - 1.0) + ) + in + if Real.abs dx > Real.abs dy then + normalLoop (x0, y0) + else if y1 > y0 then + steepUpLoop (x0, y0) + else + steepDownLoop (x0, y0) + end + +(* +val _ = aaLine (0.25, 0.5) (0.25, 0.9) (* vertical *) +val _ = aaLine (0.25, 0.5) (0.5, 0.9) (* steep up *) +val _ = aaLine (0.25, 0.5) (0.5, 0.6) (* normal up *) +val _ = aaLine (0.25, 0.5) (0.5, 0.5) (* horizontal *) +val _ = aaLine (0.25, 0.5) (0.5, 0.4) (* normal down *) +val _ = aaLine (0.25, 0.5) (0.5, 0.1) (* steep down *) +val _ = aaLine (0.25, 0.5) (0.25, 0.1) (* vertical down *) +*) + +(* draw all triangle edges as straight red lines *) +val _ = ForkJoin.parfor 1000 (0, T.numTriangles mesh) (fn i => + let + (** cut off anything that is outside the image (not important other than + * a little faster this way). + *) + fun constrain (x, y) = + (Real.min (1.0, Real.max (0.0, x)), Real.min (1.0, Real.max (0.0, y))) + fun vpos v = constrain (T.vdata mesh v) + fun doLineIf b (u, v) = + if b then aaLine (vpos u) (vpos v) else () + + val T.Tri {vertices=(u,v,w), neighbors=(a,b,c)} = T.tdata mesh i + in + (** This ensures that each line segment is only drawn once. The person + * responsible for drawing it is the triangle with larger id. + *) + doLineIf (i > a) (w, u); + doLineIf (i > b) (u, v); + doLineIf (i > c) (v, w) + end) + +(* mark input points as a pixel *) +val _ = + ForkJoin.parfor 10000 (0, Seq.length input) (fn i => + let + val (x, y) = Seq.nth input i + val (x, y) = (px x, px y) + fun b spot = setxy spot Color.black + in + b (x-1, y); + b (x, y-1); + b (x, y); + b (x, y+1); + b (x+1, y) + end) + +val t1 = Time.now () + +val _ = print ("generated image in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val (_, tm) = Util.getTime (fn _ => PPM.write filename image) +val _ = print ("wrote to " ^ filename ^ " in " ^ Time.fmt 4 tm ^ "s\n") diff --git a/tests/delaunay/test.sml b/tests/delaunay/test.sml new file mode 100644 index 000000000..a82638d33 --- /dev/null +++ b/tests/delaunay/test.sml @@ -0,0 +1,125 @@ +structure CLA = CommandLineArgs +structure T = Topology2D +structure DT = DelaunayTriangulation + +val (filename, testPtStr) = + case CLA.positional () of + [x, y] => (x, y) + | _ => Util.die "usage: ./foo " + +val testType = CLA.parseString "test" "split" + +val testPoint = + case List.mapPartial Real.fromString (String.tokens (fn c => c = #",") testPtStr) of + [x,y] => (x,y) + | _ => Util.die ("bad test point") + +val (mesh, tm) = Util.getTime (fn _ => T.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (T.numVertices mesh) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (T.numTriangles mesh) ^ "\n") + +val _ = print ("\n" ^ T.toString mesh ^ "\n\n") + +val start: T.simplex = (0, 0) + +fun simpToString (t, i) = + "triangle " ^ Int.toString t ^ " orientation " ^ Int.toString i ^ ": " ^ + let + val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t + in + String.concatWith " " (List.map Int.toString + (if i = 0 then [a,b,c] + else if i = 1 then [b,c,a] + else [c,a,b])) + end + +fun triToString t = + "triangle " ^ Int.toString t ^ ": " ^ + let + val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t + in + String.concatWith " " (List.map Int.toString [a,b,c]) + end + +val _ = Util.for (0, T.numVertices mesh) (fn v => + let + val s = T.find mesh v start + in + print ("found " ^ Int.toString v ^ ": " ^ simpToString s ^ "\n") + end) + + +(* ======================================================================== *) + +fun testSplit () = + let + val _ = print ("===================================\nTESTING SPLIT\n") + + val ((center, tris), verts) = T.findCavityAndPerimeter mesh start testPoint + val _ = + print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") + val _ = + print ("CAVITY MEMBERS ARE:\n" + ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") + val _ = + print ("CAVITY PERIMETER VERTICES ARE:\n " + ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") + + + val mesh' = T.split mesh center testPoint + val _ = + print ("===================================\nAFTER SPLIT:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +fun testFlip () = + let + val _ = print ("===================================\nTESTING FLIP\n") + + val simp = T.findPoint mesh testPoint start + val _ = + print ("SIMPLEX CONTAINING POINT:\n " ^ simpToString simp ^ "\n") + + val mesh' = T.flip mesh simp + val _ = + print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +fun testRipAndTent () = + let + val _ = print ("===================================\nTESTING RIP-AND-TENT\n") + + val (cavity as (center, tris), verts) = + T.findCavityAndPerimeter mesh start testPoint + + val _ = + print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") + val _ = + print ("CAVITY MEMBERS ARE:\n" + ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") + val _ = + print ("CAVITY PERIMETER VERTICES ARE:\n " + ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") + + val mesh' = T.ripAndTentOne (cavity, testPoint) mesh + val _ = + print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") + in + () + end + +(* ======================================================================== *) + +val _ = + case testType of + "split" => testSplit () + | "flip" => testFlip () + | "rip-and-tent" => testRipAndTent () + | _ => Util.die ("unknown test type") diff --git a/tests/dense-matmul/dense-matmul.mlb b/tests/dense-matmul/dense-matmul.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/dense-matmul/dense-matmul.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/dense-matmul/main.sml b/tests/dense-matmul/main.sml new file mode 100644 index 000000000..aed5ec288 --- /dev/null +++ b/tests/dense-matmul/main.sml @@ -0,0 +1,29 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" 1024 +val _ = + if Util.boundPow2 n = n then () + else Util.die "sidelength N must be a power of two" + +val _ = print ("generating matrices of sidelength " ^ Int.toString n ^ "\n") +val input = TreeMatrix.tabulate n (fn (i, j) => 1.0) + +val result = + Benchmark.run "multiplying" (fn _ => TreeMatrix.multiply (input, input)) + +val doCheck = CLA.parseFlag "check" +val _ = + if not doCheck then () else + let + val stuff = TreeMatrix.flatten result + val correct = + Array.length stuff = n * n + andalso + SeqBasis.reduce 1000 (fn (a, b) => a andalso b) true (0, Array.length stuff) + (fn i => Util.closeEnough (Array.sub (stuff, i), Real.fromInt n)) + in + print ("correct? "); + if correct then print "yes" else print "no"; + print "\n" + end + diff --git a/tests/fib/fib.mlb b/tests/fib/fib.mlb new file mode 100644 index 000000000..32bdc20f1 --- /dev/null +++ b/tests/fib/fib.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +fib.sml diff --git a/tests/fib/fib.sml b/tests/fib/fib.sml new file mode 100644 index 000000000..c989ac710 --- /dev/null +++ b/tests/fib/fib.sml @@ -0,0 +1,41 @@ +structure CLA = CommandLineArgs + +val grain = CLA.parseInt "grain" 20 + +fun sfib n = + if n <= 1 then n else sfib (n-1) + sfib (n-2) + +fun fib n = + if n <= grain then sfib n + else + let + val (x,y) = ForkJoin.par (fn _ => fib (n-1), fn _ => fib (n-2)) + in + x + y + end + +fun fully_par_fib n = + if n < 2 then n + else op+ (ForkJoin.par (fn _ => fully_par_fib (n-1), fn _ => fully_par_fib (n-2))) + +val no_gran_control = CLA.parseFlag "no-gran-control" +val n = CLA.parseInt "N" 39 +val _ = print ("N " ^ Int.toString n ^ "\n") + +val result = Benchmark.run "running fib" (fn _ => + if no_gran_control then + fully_par_fib n + else + fib n) + +val _ = print ("result " ^ Int.toString result ^ "\n") + +val doCheck = CLA.parseFlag "check" +val _ = + if not doCheck then + print ("do --check to check correctness\n") + else if result = sfib n then + print ("correct? yes\n") + else + print ("correct? no\n") + diff --git a/tests/flatten/AllBSFlatten.sml b/tests/flatten/AllBSFlatten.sml new file mode 100644 index 000000000..b271ea8ef --- /dev/null +++ b/tests/flatten/AllBSFlatten.sml @@ -0,0 +1,19 @@ +structure AllBSFlatten = +struct + + fun flatten s = + let + val (offsets, total) = Seq.scan op+ 0 (Seq.map Seq.length s) + + fun getElem i = + let + val segIdx = (BinarySearch.numLeq offsets i) - 1 + val segOff = Seq.nth offsets segIdx + in + Seq.nth (Seq.nth s segIdx) (i - segOff) + end + in + Seq.tabulate getElem total + end + +end diff --git a/tests/flatten/BinarySearch.sml b/tests/flatten/BinarySearch.sml new file mode 100644 index 000000000..17d81e011 --- /dev/null +++ b/tests/flatten/BinarySearch.sml @@ -0,0 +1,96 @@ +structure BinarySearch: +sig + val numLt: int Seq.t -> int -> int + val numLeq: int Seq.t -> int -> int + + (** Compute both of the above. + * Should be slightly faster than two individual calls. + *) + val numLtAndLeq: int Seq.t -> int -> (int * int) +end = +struct + + fun lowSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, Seq.nth xs mid) of + LESS => lowSearch x xs (lo, mid) + | GREATER => lowSearch x xs (mid + 1, hi) + | EQUAL => lowSearchEq x xs (lo, mid) + end + + and lowSearchEq x xs (lo, mid) = + if (mid = 0) orelse (x > Seq.nth xs (mid-1)) + then mid + else lowSearch x xs (lo, mid) + + and highSearch x xs (lo, hi) = + case hi - lo of + 0 => lo + | n => let val mid = lo + n div 2 + in case Int.compare (x, Seq.nth xs mid) of + LESS => highSearch x xs (lo, mid) + | GREATER => highSearch x xs (mid + 1, hi) + | EQUAL => highSearchEq x xs (mid, hi) + end + + and highSearchEq x xs (mid, hi) = + if (mid = Seq.length xs - 1) orelse (x < Seq.nth xs (mid + 1)) + then mid + 1 + else highSearch x xs (mid + 1, hi) + + and search (x : int) (xs : int Seq.t) (lo, hi) : int * int = + case hi - lo of + 0 => (lo, lo) + | n => let val mid = lo + n div 2 + in case Int.compare (x, Seq.nth xs mid) of + LESS => search x xs (lo, mid) + | GREATER => search x xs (mid + 1, hi) + | EQUAL => (lowSearchEq x xs (lo, mid), highSearchEq x xs (mid, hi)) + end + + fun numLtAndLeq s x = search x s (0, Seq.length s) + + + fun numLeq (s: int Seq.t) (x: int) = + let + (* val _ = + print ("numLeq (" ^ Seq.toString Int.toString s ^ ") " ^ Int.toString x ^ "\n") *) + fun loop lo hi = + case hi - lo of + 0 => lo + | 1 => (if x < Seq.nth s lo then lo else hi) + | n => + let + val mid = lo + n div 2 + in + if x < Seq.nth s mid then + loop lo mid + else + loop (mid+1) hi + end + in + loop 0 (Seq.length s) + end + + fun numLt (s: int Seq.t) (x: int) = + let + fun loop lo hi = + case hi - lo of + 0 => lo + | 1 => (if x <= Seq.nth s lo then lo else hi) + | n => + let + val mid = lo + n div 2 + in + if x <= Seq.nth s mid then + loop lo mid + else + loop (mid+1) hi + end + in + loop 0 (Seq.length s) + end + +end diff --git a/tests/flatten/BlockedAllBSFlatten.sml b/tests/flatten/BlockedAllBSFlatten.sml new file mode 100644 index 000000000..e50504b19 --- /dev/null +++ b/tests/flatten/BlockedAllBSFlatten.sml @@ -0,0 +1,71 @@ +structure BlockedAllBSFlatten = +struct + + val blockSize = CommandLineArgs.parseInt "block-size" 16 + + fun flatten s = + let + val (offsets, total) = Seq.scan op+ 0 (Seq.map Seq.length s) + + (* val _ = print ("offsets " ^ Seq.toString Int.toString offsets ^ "\n") *) + + val numBlocks = Util.ceilDiv total blockSize + + (* fun blockOffsetLo b = + BinarySearch.numLeq offsets (b * blockSize) - 1 *) + (* fun blockOffsetHi b = + BinarySearch.numLt offsets ((b+1) * blockSize) *) + + (* val (blockOffsetLos, tm1) = Util.getTime (fn _ => + Seq.tabulate blockOffsetLo numBlocks + ) *) + (* val (blockOffsetHis, tm2) = Util.getTime (fn _ => + Seq.tabulate blockOffsetHi numBlocks + ) *) + + (* val _ = print ("offlos " ^ Time.fmt 4 tm1 ^ "\n") + val _ = print ("offhis " ^ Time.fmt 4 tm2 ^ "\n") *) + + fun boundary b = + BinarySearch.numLtAndLeq offsets (b * blockSize) + + val (boundaries, tm) = Util.getTime (fn _ => + Seq.tabulate boundary (numBlocks+1) + ) + val _ = print ("boundaries " ^ Time.fmt 4 tm ^ "\n") + + fun getElem i = + let + val blockNum = i div blockSize + (* val offlo = Seq.nth blockOffsetLos blockNum + val offhi = Seq.nth blockOffsetHis blockNum *) + val offlo = #2 (Seq.nth boundaries blockNum) - 1 + val offhi = #1 (Seq.nth boundaries (blockNum+1)) + val segIdx = + offlo + (BinarySearch.numLeq (Seq.subseq offsets (offlo, offhi-offlo))) i - 1 + (* val _ = + print ("getElem " ^ Int.toString i ^ + " blockNum " ^ Int.toString blockNum ^ + " offlo " ^ Int.toString offlo ^ + " offhi " ^ Int.toString offhi ^ + " segIdx " ^ Int.toString segIdx ^ + "\n") *) + val segOff = Seq.nth offsets segIdx + in + Seq.nth (Seq.nth s segIdx) (i - segOff) + end + (* handle Subscript => + ( print ("getElem " ^ Int.toString i ^ "\n") + ; raise Subscript + ) *) + + val (result, tm3) = Util.getTime (fn _ => + Seq.tabulate getElem total + ) + + val _ = print ("tabulate " ^ Time.fmt 4 tm3 ^ "\n") + in + result + end + +end diff --git a/tests/flatten/ExpandFlatten.sml b/tests/flatten/ExpandFlatten.sml new file mode 100644 index 000000000..30ff1e945 --- /dev/null +++ b/tests/flatten/ExpandFlatten.sml @@ -0,0 +1,102 @@ +structure ExpandFlatten = +struct + + val expansionFactor = CommandLineArgs.parseInt "f" 8 + val targetOffsetsPerElem = CommandLineArgs.parseInt "off-per-elem" 8 + val targetBlockSize = CommandLineArgs.parseInt "min-block-size" 64 + + fun reportTime msg f = + let + val (result, tm) = Util.getTime f + in + print (msg ^ " " ^ Time.fmt 4 tm ^ "\n"); + result + end + + fun seqNumLeq (xs: int Seq.t) x = + let + val n = Seq.length xs + fun loop i = + if i < n andalso Seq.nth xs i <= x then + loop (i+1) + else + i + in + loop 0 + end + + + fun tabulateG grain f n = + ArraySlice.full (SeqBasis.tabulate grain (0, n) f) + + + fun flatten s = + let + val (offsets, total) = Seq.scan op+ 0 (Seq.map Seq.length s) + + fun expand (prevBoundaries, prevBlockSize) = + let + val newBlockSize = prevBlockSize div expansionFactor + + fun newBoundary b = + let + val blockNum = (b * newBlockSize) div prevBlockSize + val offlo = #2 (Seq.nth prevBoundaries blockNum) - 1 + val offhi = + if blockNum+1 < Seq.length prevBoundaries then + #1 (Seq.nth prevBoundaries (blockNum+1)) + else + Seq.length offsets + + val (nlt, nleq) = + BinarySearch.numLtAndLeq + (Seq.subseq offsets (offlo, offhi-offlo)) + (b * newBlockSize) + in + (offlo + nlt, offlo + nleq) + end + + val newNumBlocks = Util.ceilDiv total newBlockSize + val bs = + reportTime "boundaries" (fn _ => + Seq.tabulate newBoundary newNumBlocks) + in + (bs, newBlockSize) + end + + fun expansionLoop (boundaries, blockSize) = + if + Seq.length boundaries >= Seq.length offsets div targetOffsetsPerElem + orelse blockSize <= targetBlockSize + then + (boundaries, blockSize) + else + expansionLoop (expand (boundaries, blockSize)) + + (** Initial block is the whole array, but we need to compute the proper + * boundary for the start of that block. Then we expand until we + * hit the target. + *) + val init = (Seq.fromList [BinarySearch.numLtAndLeq offsets 0], total) + val (boundaries, blockSize) = expansionLoop init + + (** Finally, pull individual elements *) + fun getElem i = + let + val blockNum = i div blockSize + val offlo = #2 (Seq.nth boundaries blockNum) - 1 + val segIdx = + offlo - 1 + + (seqNumLeq (Seq.drop offsets offlo) i) + val segOff = Seq.nth offsets segIdx + in + Seq.nth (Seq.nth s segIdx) (i - segOff) + end + + val result = reportTime "tabulate" (fn _ => + Seq.tabulate getElem total) + in + result + end + +end diff --git a/tests/flatten/FullExpandPow2Flatten.sml b/tests/flatten/FullExpandPow2Flatten.sml new file mode 100644 index 000000000..45cd4be2f --- /dev/null +++ b/tests/flatten/FullExpandPow2Flatten.sml @@ -0,0 +1,132 @@ +structure FullExpandPow2Flatten = +struct + + fun biggestPow2LessOrEqualTo x = + let + fun loop y = if 2*y > x then y else loop (2*y) + in + loop 1 + end + + (** Choose segment indices for n elements based on the given segment offsets. + * + * EXAMPLE INPUT: + * offsets = [0,3,4,4,4,4,7,8] + * n = 10 + * OUTPUT: + * [ 0,0,0, 1, 5,5,5, 6, 7,7 ] + * + *) + fun pickSegments (offsets, n) = + let + fun offmids half lo (offlo, offhi) = + let + val mid = lo + half + val (offmidlo, offmidhi) = + BinarySearch.numLtAndLeq (Seq.subseq offsets (offlo, offhi-offlo)) mid + in + (offlo + offmidlo, offlo + offmidhi - 1) + end + + fun loop toWidth width results = + if width = toWidth then + results + else + let + fun getOffmids i = + offmids (width div 2) (i * width) (Seq.nth results i) + + val (O, tm) = Util.getTime (fn _ => + Seq.tabulate getOffmids (Seq.length results) + ) + val _ = + if width > 64 then () + else print ("offsets " ^ Int.toString width ^ ": " ^ Time.fmt 4 tm ^ "\n") + + fun get i = + let + val i' = i div 2 + val lo = i' * width + val hi = lo + width + val (offlo, offhi) = Seq.nth results i' + val (offmidlo, offmidhi) = Seq.nth O i' + val mid = lo + (hi - lo) div 2 + in + if i mod 2 = 0 then + (offlo, offmidlo) (* "left" *) + else + (offmidhi, offhi) (* "right" *) + end + val (results', tm) = Util.getTime (fn _ => + Seq.tabulate get (2 * Seq.length results) + ) + val _ = + if width > 64 then () + else print ("expand " ^ Int.toString width ^ ": " ^ Time.fmt 4 tm ^ "\n") + in + loop toWidth (width div 2) results' + end + + (** (width, results) represents current prefix that we've finished + * processing: it's been decomposed into some number of subsequences + * each of the given width. + * + * (offlo, lo) is the remaining suffix, where lo is the starting index + * of the suffix and offlo is the segment index for lo. + *) + fun handleNonPow2Loop (width, results) (offlo, lo) = + if lo >= n then + loop 1 width results + else + let + val remainingSize = n - lo + val targetWidth = biggestPow2LessOrEqualTo remainingSize + val results' = loop targetWidth width results + + val (offmidlo, offmidhi) = offmids targetWidth lo (offlo, Seq.length offsets) + val new = (offlo, offmidlo) + val results'' = Seq.append (results', Seq.fromList [new]) + in + handleNonPow2Loop (targetWidth, results'') (offmidhi, lo+targetWidth) + end + + val targetWidth = biggestPow2LessOrEqualTo n + val offlo = (BinarySearch.numLeq offsets 0) - 1 + val (offmidlo, offmidhi) = offmids targetWidth 0 (0, Seq.length offsets) + val init = Seq.fromList [(offlo, offmidlo)] + in + Seq.map #1 (handleNonPow2Loop (targetWidth, init) (offmidhi, targetWidth)) + end + + fun flatten s = + let + val n = Seq.length s + val ((offsets, total), tm1) = Util.getTime (fn _ => + Seq.scan op+ 0 (Seq.map Seq.length s) + ) + + val (segIdxs, tm2) = Util.getTime (fn _ => + pickSegments (offsets, total) + ) + + fun getElem i = + let + val segIdx = Seq.nth segIdxs i + val segOff = Seq.nth offsets segIdx + val j = i - segOff + in + Seq.nth (Seq.nth s segIdx) j + end + + val (result, tm3) = Util.getTime (fn _ => + Seq.tabulate getElem total + ) + + val _ = print ("scan: " ^ Time.fmt 4 tm1 ^ "\n") + val _ = print ("pickSegments: " ^ Time.fmt 4 tm2 ^ "\n") + val _ = print ("tabulate: " ^ Time.fmt 4 tm3 ^ "\n") + in + result + end + +end diff --git a/tests/flatten/MultiBlockedBSFlatten.sml b/tests/flatten/MultiBlockedBSFlatten.sml new file mode 100644 index 000000000..6e43caba1 --- /dev/null +++ b/tests/flatten/MultiBlockedBSFlatten.sml @@ -0,0 +1,105 @@ +structure MultiBlockedBSFlatten = +struct + + val blockSizesStr = CommandLineArgs.parseString "bs" "1000,50" + val doReport = CommandLineArgs.parseFlag "report-times" + + val blockSizes = + List.map (valOf o Int.fromString) + (String.tokens (fn c => c = #",") blockSizesStr) + handle _ => raise Fail ("error parsing block sizes '" ^ blockSizesStr ^ "'") + + fun reportTime msg f = + let + val (result, tm) = Util.getTime f + in + if not doReport then () else print (msg ^ " " ^ Time.fmt 4 tm ^ "\n"); + result + end + + fun seqNumLeq (xs: int Seq.t) x = + let + val n = Seq.length xs + fun loop i = + if i < n andalso Seq.nth xs i <= x then + loop (i+1) + else + i + in + loop 0 + end + + + fun tabulateG grain f n = + ArraySlice.full (SeqBasis.tabulate grain (0, n) f) + + fun flatten s = + let + val (offsets, total) = Seq.scan op+ 0 (Seq.map Seq.length s) + + fun expand (prevBoundaries, prevBlockSize) newBlockSize = + let + fun newBoundary b = + let + val blockNum = (b * newBlockSize) div prevBlockSize + val offlo = #2 (Seq.nth prevBoundaries blockNum) - 1 + val offhi = + if blockNum+1 < Seq.length prevBoundaries then + #1 (Seq.nth prevBoundaries (blockNum+1)) + else + Seq.length offsets + + val (nlt, nleq) = + BinarySearch.numLtAndLeq + (Seq.subseq offsets (offlo, offhi-offlo)) + (b * newBlockSize) + in + (offlo + nlt, offlo + nleq) + end + + val newNumBlocks = Util.ceilDiv total newBlockSize + val bs = + reportTime "boundaries" (fn _ => + Seq.tabulate newBoundary newNumBlocks) + in + (bs, newBlockSize) + end + + (** compute initial boundary *) + val blockSize = List.hd blockSizes + val numBlocks1 = Util.ceilDiv total blockSize + fun boundary1 b = BinarySearch.numLtAndLeq offsets (b * blockSize) + val boundaries = + reportTime "boundaries" (fn _ => + Seq.tabulate boundary1 numBlocks1) + + fun expansionLoop (boundaries, blockSize) bs = + case bs of + [] => (boundaries, blockSize) + | b :: bs' => + expansionLoop (expand (boundaries, blockSize) b) bs' + + (** expand boundaries a few times *) + val (boundaries, blockSize) = + expansionLoop (boundaries, blockSize) (List.tl blockSizes) + + (** pull individual elements *) + fun getElem i = + let + val blockNum = i div blockSize + val offlo = #2 (Seq.nth boundaries blockNum) - 1 + val segIdx = + offlo - 1 + + (seqNumLeq (Seq.drop offsets offlo) i) + val segOff = Seq.nth offsets segIdx + in + Seq.nth (Seq.nth s segIdx) (i - segOff) + end + + val result = reportTime "tabulate" (fn _ => + Seq.tabulate getElem total) + in + result + end + +end diff --git a/tests/flatten/SimpleBlockedFlatten.sml b/tests/flatten/SimpleBlockedFlatten.sml new file mode 100644 index 000000000..ee0f1d1e7 --- /dev/null +++ b/tests/flatten/SimpleBlockedFlatten.sml @@ -0,0 +1,66 @@ +structure SimpleBlockedFlatten = +struct + + val blockSize = CommandLineArgs.parseInt "block-size" 4096 + val doReport = CommandLineArgs.parseFlag "report-times" + + fun reportTime msg f = + let + val (result, tm) = Util.getTime f + in + if not doReport then () else print (msg ^ " " ^ Time.fmt 4 tm ^ "\n"); + result + end + + fun tabulateG grain f n = + ArraySlice.full (SeqBasis.tabulate grain (0, n) f) + + fun flatten s = + let + val (offsets, total) = reportTime "scan" (fn _ => + Seq.scan op+ 0 (Seq.map Seq.length s)) + + val numBlocks = Util.ceilDiv total blockSize + + fun getBlock bidx = + let + val lo = bidx * blockSize + val hi = (if bidx+1 = numBlocks then total else lo + blockSize) + val size = hi - lo + + fun loop (count, elems) (segIdx, i) = + if count < size andalso i < Seq.length (Seq.nth s segIdx) then + loop (count+1, Seq.nth (Seq.nth s segIdx) i :: elems) (segIdx, i+1) + else if count >= size then + elems + else + loop (count, elems) (segIdx+1, 0) + + val segIdx = BinarySearch.numLeq offsets lo - 1 + val segOff = Seq.nth offsets segIdx + val elems = loop (0, []) (segIdx, lo - segOff) + in + Vector.fromList elems + end + + val blocks = reportTime "tab blocks" (fn _ => + tabulateG 1 getBlock numBlocks) + + fun getElem i = + let + val bidx = i div blockSize + val blo = bidx * blockSize + val block = Seq.nth blocks bidx + val blen = Vector.length block + in + (** The vector is reversed, because built from list. *) + Vector.sub (block, blen - 1 - (i - blo)) + end + + val result = reportTime "tabulate" (fn _ => + Seq.tabulate getElem total) + in + result + end + +end diff --git a/tests/flatten/SimpleExpandFlatten.sml b/tests/flatten/SimpleExpandFlatten.sml new file mode 100644 index 000000000..a5a188803 --- /dev/null +++ b/tests/flatten/SimpleExpandFlatten.sml @@ -0,0 +1,58 @@ +(** This is the algorithm I describe in my blog post. Asymptotically efficient + * and simple to understand, but not very well optimized. + *) +structure SimpleExpandFlatten = +struct + + fun flatten s = + let + val (offsets, total) = Seq.scan op+ 0 (Seq.map Seq.length s) + + fun expand (prevBoundaries, prevBlockSize) = + let + val newBlockSize = prevBlockSize div 2 + + fun newBoundary b = + let + val blockNum = (b * newBlockSize) div prevBlockSize + val offlo = #2 (Seq.nth prevBoundaries blockNum) - 1 + val offhi = + if blockNum+1 < Seq.length prevBoundaries then + #1 (Seq.nth prevBoundaries (blockNum+1)) + else + Seq.length offsets + + val (nlt, nleq) = + BinarySearch.numLtAndLeq + (Seq.subseq offsets (offlo, offhi-offlo)) + (b * newBlockSize) + in + (offlo + nlt, offlo + nleq) + end + + val newNumBlocks = Util.ceilDiv total newBlockSize + in + (Seq.tabulate newBoundary newNumBlocks, newBlockSize) + end + + fun expansionLoop (boundaries, blockSize) = + if blockSize = 1 then + boundaries + else + expansionLoop (expand (boundaries, blockSize)) + + val boundaries = + expansionLoop (Seq.fromList [BinarySearch.numLtAndLeq offsets 0], total) + + fun getElem i = + let + val segIdx = #2 (Seq.nth boundaries i) - 1 + val segOff = Seq.nth offsets segIdx + in + Seq.nth (Seq.nth s segIdx) (i - segOff) + end + in + Seq.tabulate getElem total + end + +end diff --git a/tests/flatten/flatten.mlb b/tests/flatten/flatten.mlb new file mode 100644 index 000000000..ffa7d8537 --- /dev/null +++ b/tests/flatten/flatten.mlb @@ -0,0 +1,10 @@ +../mpllib/sources.$(COMPAT).mlb +BinarySearch.sml +FullExpandPow2Flatten.sml +AllBSFlatten.sml +BlockedAllBSFlatten.sml +MultiBlockedBSFlatten.sml +ExpandFlatten.sml +SimpleBlockedFlatten.sml +SimpleExpandFlatten.sml +flatten.sml diff --git a/tests/flatten/flatten.sml b/tests/flatten/flatten.sml new file mode 100644 index 000000000..4183668a3 --- /dev/null +++ b/tests/flatten/flatten.sml @@ -0,0 +1,43 @@ +structure CLA = CommandLineArgs + +(* ========================================================================== + * parse command-line arguments and run + *) + +val numElems = CLA.parseInt "num-elems" (1000 * 1000 * 10) +val numSeqs = CLA.parseInt "num-seqs" (numElems div 5) +val impl = CLA.parseString "impl" "lib" +val _ = print ("num-elems " ^ Int.toString numElems ^ "\n") +val _ = print ("num-seqs " ^ Int.toString numSeqs ^ "\n") + +val doit = + case impl of + "lib" => Seq.flatten + | "full-expand-pow2" => FullExpandPow2Flatten.flatten + | "all-bs" => AllBSFlatten.flatten + | "blocked-all-bs" => BlockedAllBSFlatten.flatten + | "multi-blocked-bs" => MultiBlockedBSFlatten.flatten + | "expand" => ExpandFlatten.flatten + | "simple-blocked" => SimpleBlockedFlatten.flatten + | "simple-expand" => SimpleExpandFlatten.flatten + | _ => raise Fail ("unknown impl '" ^ impl ^ "'") + +val _ = print ("impl " ^ impl ^ "\n") + +val offsets = + Mergesort.sort Int.compare + (Seq.tabulate (fn i => Util.hash i mod numElems) numSeqs) +fun O i = + if i = 0 then 0 + else if i >= numSeqs then numElems + else Seq.nth offsets i +val elems = Seq.tabulate (fn i => i) numElems +val input = Seq.tabulate (fn i => Seq.subseq elems (O i, O (i+1) - O i)) numSeqs +val _ = print ("generated input\n") + +val result = Benchmark.run "flatten" (fn _ => doit input) + +val correct = Seq.equal op= (elems, result) +val _ = print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") +val _ = print ("result " ^ Util.summarizeArraySlice 10 Int.toString result ^ "\n") + diff --git a/tests/gif-encode/main.sml b/tests/gif-encode/main.sml new file mode 100644 index 000000000..a3e979a4f --- /dev/null +++ b/tests/gif-encode/main.sml @@ -0,0 +1,55 @@ +structure CLA = CommandLineArgs + +local +open GIF +in + +fun encode palette {width, height, numImages, getImage} = + if numImages <= 0 then + err "Must be at least one image" + else + let + val width16 = checkToWord16 "width" width + val height16 = checkToWord16 "height" height + + val numberOfColors = Seq.length (#colors palette) + + val _ = + if numberOfColors <= 256 then () + else err "Must have at most 256 colors in the palette" + + val imageData = + AS.full (SeqBasis.tabulate 1 (0, numImages) (fn i => + let + val img = getImage i + in + if Seq.length img <> height * width then + err "Not all images are the right dimensions" + else + LZW.packCodeStream numberOfColors + (LZW.codeStream numberOfColors img) + end)) + in + imageData + end + +end + +val width = CLA.parseInt "width" +val height = CLA.parseInt "height" + +fun pixel (i, j) = + Color.hsv + { h = 90.0 + (Real.fromInt i / Real.fromInt width) * 135.0 + , s = 0.5 + (Real.fromInt j / Real.fromInt height) * 0.5 + , v = 0.8 + } + +val image = + { height = height + , width = width + , data = Seq.tabulate (fn i => (i div width, i mod width)) (width * height) + } + +val imageData = + diff --git a/tests/graphio/graphio.mlb b/tests/graphio/graphio.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/graphio/graphio.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/graphio/main.sml b/tests/graphio/main.sml new file mode 100644 index 000000000..35c9c685b --- /dev/null +++ b/tests/graphio/main.sml @@ -0,0 +1,21 @@ +structure CLA = CommandLineArgs +structure G = AdjacencyGraph(Int) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val graph = G.parseFile filename +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val outfile = CLA.parseString "outfile" "" + +val _ = + if outfile = "" then () else + let + val (_, tm) = Util.getTime (fn _ => G.writeAsBinaryFormat graph outfile) + in + print ("wrote graph (binary format) to " ^ outfile ^ " in " ^ Time.fmt 4 tm ^ "s\n") + end diff --git a/tests/grep-old/Grep.sml b/tests/grep-old/Grep.sml new file mode 100644 index 000000000..3c64e2303 --- /dev/null +++ b/tests/grep-old/Grep.sml @@ -0,0 +1,129 @@ +structure Grep: +sig + (* returns number of matching lines, and output that unix grep would show *) + val grep : char Seq.t (* pattern *) + -> char Seq.t (* source text *) + -> int * (char Seq.t) +end = +struct + + structure A = Array + structure AS = ArraySlice + + type 'a seq = 'a Seq.t + + (* fun lines (s : char seq) : (char seq) Seq.seq = + let + val n = ASeq.length s + val indices = Seq.tabulate (fn i => i) n + fun isNewline i = (ASeq.nth s i = #"\n") + val locs = Seq.filter isNewline indices + val m = Seq.length locs + + fun line i = + let + val lo = (if i = 0 then 0 else 1 + Seq.nth locs (i-1)) + val hi = (if i = m then n else Seq.nth locs i) + in + ASeq.subseq s (lo, hi-lo) + end + in + Seq.tabulate line (m+1) + end *) + + (* check if line[i..] matches the pattern *) + fun checkMatch pattern line i = + (i + Seq.length pattern <= Seq.length line) andalso + let + val m = Seq.length pattern + (* pattern[j..] matches line[i+j..] *) + fun matchesFrom j = + (j >= m) orelse + ((Seq.nth line (i+j) = Seq.nth pattern j) andalso matchesFrom (j+1)) + in + matchesFrom 0 + end + + (* fun grep pat source = + let + val granularity = CommandLineArgs.parseOrDefaultInt "granularity" 1000 + val ff = FindFirst.findFirst granularity + (* val ff = FindFirst.findFirstSerial *) + fun containsPat line = + case ff (0, ASeq.length line) (checkMatch pat line) of + NONE => false + | SOME _ => true + + val linesWithPat = Seq.filter containsPat (lines source) + val newln = Seq.singleton #"\n" + + fun choose i = + if Util.even i + then Seq.fromArraySeq (Seq.nth linesWithPat (i div 2)) + else newln + in + Seq.toArraySeq (Seq.flatten (Seq.tabulate choose (2 * Seq.length linesWithPat))) + end *) + + val ffGrain = CommandLineArgs.parseInt "ff-grain" 1000 + val findFirst = FindFirst.findFirst ffGrain + + fun grep pat source = + let + fun isNewline i = (Seq.nth source i = #"\n") + + val nlPos = + AS.full (SeqBasis.filter 10000 (0, Seq.length source) (fn i => i) isNewline) + val numLines = Seq.length nlPos + 1 + fun lineStart i = + if i = 0 then 0 else 1 + Seq.nth nlPos (i-1) + fun lineEnd i = + if i = Seq.length nlPos then Seq.length source else Seq.nth nlPos i + fun line i = Seq.subseq source (lineStart i, lineEnd i - lineStart i) + + (* val _ = print ("got newline positions\n") *) + + (* compute whether or not each line contains the pattern *) + val hasPatFlags = AS.full (SeqBasis.tabulate 1000 (0, numLines) (fn i => + let + val ln = line i + in + case findFirst (0, Seq.length ln) (checkMatch pat ln) of + SOME _ => true + | NONE => false + end)) + + (* val _ = print ("found the patterns\n") *) + + val linesWithPat = + AS.full (SeqBasis.filter 4096 (0, numLines) (fn i => i) (Seq.nth hasPatFlags)) + val numLinesOutput = Seq.length linesWithPat + + (* val _ = print ("filtered the lines\n") *) + (* val _ = print ("num lines: " ^ Int.toString numLinesOutput ^ "\n") *) + + val outputOffsets = + AS.full (SeqBasis.scan 4096 op+ 0 (0, numLinesOutput) + (* +1 to include newline *) + (fn i => 1 + Seq.length (line (Seq.nth linesWithPat i)))) + + val outputLen = Seq.nth outputOffsets numLinesOutput + + (* val _ = print ("computed line offsets\n") *) + + val output = ForkJoin.alloc outputLen + fun put i c = A.update (output, i, c) + in + ForkJoin.parfor 1000 (0, numLinesOutput) (fn i => + let + val ln = line (Seq.nth linesWithPat i) + val off = Seq.nth outputOffsets i + in + Seq.foreach ln (fn (j, c) => put (off+j) c); + put (off + Seq.length ln) #"\n" + end); + + (numLinesOutput, AS.full output) + end + +end diff --git a/tests/grep-old/grep-old.mlb b/tests/grep-old/grep-old.mlb new file mode 100644 index 000000000..29645d46c --- /dev/null +++ b/tests/grep-old/grep-old.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +Grep.sml +main.sml diff --git a/tests/grep-old/main.sml b/tests/grep-old/main.sml new file mode 100644 index 000000000..1dbc2d345 --- /dev/null +++ b/tests/grep-old/main.sml @@ -0,0 +1,32 @@ +structure CLA = CommandLineArgs + +val (pat, file) = + case CLA.positional () of + [pat, file] => (pat, file) + | _ => Util.die ("[ERR] usage: grep PATTERN FILE") + +val benchmark = CLA.parseFlag "benchmark" + +fun bprint str = + if not benchmark then () + else print str + +val (source, tm) = Util.getTime (fn _ => ReadFile.contentsSeq file) +val _ = bprint ("read file in " ^ Time.fmt 4 tm ^ "s\n") + +val pattern = Seq.tabulate (fn i => String.sub (pat, i)) (String.size pat) + +val (matches, output) = + Benchmark.run "running grep" (fn _ => Grep.grep pattern source) + +val _ = bprint ("number of matched lines: " ^ Int.toString matches ^ "\n") +val _ = bprint ("length of output: " ^ Int.toString (Seq.length output) ^ "\n") + +val _ = + if benchmark then () + else + ArraySlice.app (fn c => TextIO.output1 (TextIO.stdOut, c)) output + +val _ = + if benchmark then GCStats.report () + else () diff --git a/tests/grep/grep.mlb b/tests/grep/grep.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/grep/grep.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/grep/main.sml b/tests/grep/main.sml new file mode 100644 index 000000000..75394c1a8 --- /dev/null +++ b/tests/grep/main.sml @@ -0,0 +1,53 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +(* chosen by subdirectory *) +structure Grep = MkGrep(OldDelayedSeq) + +(* +val pattern = CLA.parseString "pattern" "" +val filePath = CLA.parseString "infile" "" + +val input = + let + val (source, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filePath) + val _ = print ("loadtime " ^ Time.fmt 4 tm ^ "s\n") + in + source + end + +val pattern = + Seq.tabulate (fn i => String.sub (pattern, i)) (String.size pattern) +*) + +val (pat, file) = + case CLA.positional () of + [pat, file] => (pat, file) + | _ => Util.die ("[ERR] usage: grep PATTERN FILE") + +val (input, tm) = Util.getTime (fn _ => ReadFile.contentsSeq file) +val _ = print ("read file in " ^ Time.fmt 4 tm ^ "s\n") + +val pattern = Seq.tabulate (fn i => String.sub (pat, i)) (String.size pat) + +val n = Seq.length input +val _ = print ("n " ^ Int.toString n ^ "\n") + +fun task () = + Grep.grep pattern input + +val result = Benchmark.run "running grep" task +val _ = print ("num matching lines " ^ Int.toString (Seq.length result) ^ "\n") + +(* fun dumpLoop i = + if i >= Seq.length result then () else + let + val (s, e) = Seq.nth result i + val tok = CharVector.tabulate (e-s, fn k => Seq.nth input (s+k)) + in + print tok; + print "\n"; + dumpLoop (i+1) + end + +val _ = dumpLoop 0 *) diff --git a/tests/high-frag/high-frag.mlb b/tests/high-frag/high-frag.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/high-frag/high-frag.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/high-frag/main.sml b/tests/high-frag/main.sml new file mode 100644 index 000000000..5800d238a --- /dev/null +++ b/tests/high-frag/main.sml @@ -0,0 +1,51 @@ +structure CLA = CommandLineArgs +val nodes = CLA.parseInt "nodes-per-tree" 1000000 +val numTrees = CLA.parseInt "num-trees" 100 +val grain = CLA.parseInt "grain" 1 +val _ = print ("nodes-per-tree " ^ Int.toString nodes ^ "\n") +val _ = print ("num-trees " ^ Int.toString numTrees ^ "\n") + +datatype tree = Leaf of int | Node of tree * tree + + +fun go (i, j) = + case j-i of + 1 => Leaf (Util.hash i) + | n => + if n <= grain then + Node (go (i, i + n div 2), go (i + n div 2, j)) + else + Node (ForkJoin.par (fn _ => go (i, i + n div 2), + fn _ => go (i + n div 2, j))) + +fun benchmark () = + List.tabulate (numTrees, fn i => go (i*nodes, (i+1)*nodes)) + +val results = + Benchmark.run "high-fragmentation tree" benchmark + + +(** ====================================================================== + * Now do some arbitrary computation on the result to make sure it's not + * optimized out. + *) + +fun reduce f t = + let + fun loop depth t = + case t of + Leaf x => x + | Node (a, b) => + if depth > 10 then + f (loop (depth+1) a, loop (depth+1) b) + else + f (ForkJoin.par (fn _ => loop (depth+1) a, + fn _ => loop (depth+1) b)) + in + loop 0 t + end + +val foo = + List.foldl Int.min (valOf Int.maxInt) (List.map (reduce Int.max) results) + +val _ = print ("foo " ^ Int.toString foo ^ "\n") diff --git a/tests/integrate-opt/Integrate.sml b/tests/integrate-opt/Integrate.sml new file mode 100644 index 000000000..e77efffaf --- /dev/null +++ b/tests/integrate-opt/Integrate.sml @@ -0,0 +1,15 @@ +structure Integrate = +struct + + fun integrate f (s, e) n = + let + val delta = (e - s) / (Real.fromInt n) + val s' = s + delta / 2.0 + in + delta + * + SeqBasis.reduce 5000 op+ 0.0 (0, n) (fn i => + f (s' + (Real.fromInt i) * delta)) + end + +end diff --git a/tests/integrate-opt/integrate-opt.mlb b/tests/integrate-opt/integrate-opt.mlb new file mode 100644 index 000000000..d732a9a4a --- /dev/null +++ b/tests/integrate-opt/integrate-opt.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +Integrate.sml +main.sml diff --git a/tests/integrate-opt/main.sml b/tests/integrate-opt/main.sml new file mode 100644 index 000000000..eb3fbac85 --- /dev/null +++ b/tests/integrate-opt/main.sml @@ -0,0 +1,26 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val doCheck = CLA.parseFlag "check" + +val range = (1.0, 1000.0) + +val (f, correctAnswer) = + (fn x => Math.sqrt (1.0 / x), 61.245553203367586639977870888654371) + (* (fn x => 1.0 / x, 6.9077552789821370520539743640530926) *) + (* (fn x => Math.sin (1.0 / x), 6.8264726355070070694576392250122662) *) + +fun task () = + Integrate.integrate f range n + +fun check result = + if Util.closeEnough (result, correctAnswer) then + print ("correct? yes\n") + else + print ("correct? no (error = " + ^ Real.toString (Real.abs (result - correctAnswer)) + ^ ")\n") + +val result = Benchmark.run "integrate" task +val _ = print ("result " ^ Real.toString result ^ "\n") +val _ = check result diff --git a/tests/integrate/MkIntegrate.sml b/tests/integrate/MkIntegrate.sml new file mode 100644 index 000000000..014c1c96f --- /dev/null +++ b/tests/integrate/MkIntegrate.sml @@ -0,0 +1,13 @@ +functor MkIntegrate(Seq: SEQUENCE) = +struct + + fun integrate (f: real -> real) (s: real, e: real) (n: int) = + let + val delta = (e - s) / (Real.fromInt n) + val s' = s + delta / 2.0 + val X = Seq.tabulate (fn i => f (s' + (Real.fromInt i) * delta)) n + in + (Seq.reduce op+ 0.0 X) * delta + end + +end diff --git a/tests/integrate/integrate.mlb b/tests/integrate/integrate.mlb new file mode 100644 index 000000000..f4abb6c79 --- /dev/null +++ b/tests/integrate/integrate.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MkIntegrate.sml +main.sml diff --git a/tests/integrate/main.sml b/tests/integrate/main.sml new file mode 100644 index 000000000..23bf1020f --- /dev/null +++ b/tests/integrate/main.sml @@ -0,0 +1,39 @@ +structure CLA = CommandLineArgs + +structure IntegrateAS = MkIntegrate(Seq) +structure IntegrateDS = MkIntegrate(DelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val impl = CLA.parseString "impl" "delayed-seq" +val doCheck = CLA.parseFlag "check" + +val _ = print ("n " ^ Int.toString n ^ "\n") +val _ = print ("impl " ^ impl ^ "\n") +val _ = print ("check? " ^ (if doCheck then "yes" else "no") ^ "\n") + +val range = (1.0, 1000.0) + +val (f, correctAnswer) = + (fn x => Math.sqrt (1.0 / x), 61.245553203367586639977870888654371) +(* (fn x => 1.0 / x, 6.9077552789821370520539743640530926) *) +(* (fn x => Math.sin (1.0 / x), 6.8264726355070070694576392250122662) *) + +val task = + case impl of + "delayed-seq" => (fn () => IntegrateDS.integrate f range n) + | "array-seq" => (fn () => IntegrateAS.integrate f range n) + | _ => + Util.die + ("unknown impl: " ^ impl ^ "; options are: delayed-seq, array-seq") + +fun check result = + if Util.closeEnough (result, correctAnswer) then + print ("correct? yes\n") + else + print + ("correct? no (error = " + ^ Real.toString (Real.abs (result - correctAnswer)) ^ ")\n") + +val result = Benchmark.run "integrate" task +val _ = print ("result " ^ Real.toString result ^ "\n") +val _ = check result diff --git a/tests/interval-tree/IntervalTree.sml b/tests/interval-tree/IntervalTree.sml new file mode 100644 index 000000000..60c69a2c6 --- /dev/null +++ b/tests/interval-tree/IntervalTree.sml @@ -0,0 +1,47 @@ +structure ITree : Aug = +struct + type key = int + type value = int + type aug = int + val compare = Int.compare + val g = fn (x, y) => y + val f = fn (x, y) => Int.max (x, y) + val id = ~1073741824 + val balance = WB 0.28 + fun debug (k, v, a) = Int.toString k ^ ", " ^ Int.toString v ^ ", " ^ Int.toString a +end + +signature INTERVAL_MAP = +sig + type point + type interval = point * point + type imap + + val interval_map : interval Seq.t -> int -> imap + val multi_insert : imap -> interval Seq.t -> imap + val stab : imap -> point -> bool + val report_all : imap -> point -> imap + val size : imap -> int + val print : imap -> unit +end + +structure IntervalMap : INTERVAL_MAP = +struct + structure amap = PAM(ITree) + type point = ITree.key + type interval = point * point + type imap = amap.am + + fun interval_map s n = amap.build s 0 n + + fun multi_insert im s = amap.multi_insert im s (Int.max) + + fun stab im p = (amap.aug_left im p) > p + + fun report_all im p = amap.aug_filter (amap.up_to im p) (fn q => q > p) + + fun size im = (amap.size im) + + fun print im = amap.print_tree im "" +end + diff --git a/tests/interval-tree/interval-tree.mlb b/tests/interval-tree/interval-tree.mlb new file mode 100644 index 000000000..463c11a88 --- /dev/null +++ b/tests/interval-tree/interval-tree.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +IntervalTree.sml +new-main.sml diff --git a/tests/interval-tree/main.sml b/tests/interval-tree/main.sml new file mode 100644 index 000000000..3418eb0f7 --- /dev/null +++ b/tests/interval-tree/main.sml @@ -0,0 +1,87 @@ +fun randRange i j = + i + Word64.toInt (Word64.mod (Util.hash64 (Word64.fromInt i), Word64.fromInt (j - i))) + + +fun uniform_input n w shuffle = + let + fun g i = (randRange 1 w, i) + val pairs = Seq.tabulate (fn i => g i) n + in + pairs + end + + +fun eval_build_im n = +let + val max_size = 2147483647 + val ilint = uniform_input n max_size false + val v = Seq.map (fn (i, j) => (i, (randRange (LargeInt.fromInt i) max_size))) ilint + val t0 = Time.now () + val i = IntervalMap.interval_map v n + val t1 = Time.now () +in + (t0, t1, i) +end + +fun eval_queries_im im q = + let + val max_size = 2147483647 + val queries = Seq.map (fn i => (#1 i)) (uniform_input q max_size false) + val t0 = Time.now() + val r = Seq.map (IntervalMap.stab im) queries + val t1 = Time.now() + in + (t0, t1, r) + end + +fun eval_multi_insert_im im n = + let + val max_size = 2147483647 + val ilint = uniform_input n max_size false + val v = Seq.map (fn (i, j) => (i, (randRange (LargeInt.fromInt i) max_size))) ilint + val t0 = Time.now () + val i = IntervalMap.multi_insert im v + val t1 = Time.now () + in + (t0, t1, i) + end + + + + +fun run_rounds f r = + let + fun round_rec i diff = + if i = 0 then diff + else + let + val (t0, t1, _) = f() + val new_diff = Time.- (t1, t0) + val _ = print ("round " ^ (Int.toString (r - i + 1)) ^ " in " ^ Time.fmt 4 (new_diff) ^ "s\n") + in + round_rec (i - 1) (Time.+ (diff, new_diff)) + end + in + round_rec r Time.zeroTime + end + +val query_size = CommandLineArgs.parseInt "q" 100000000 +val size = CommandLineArgs.parseInt "n" 100000000 +val rep = CommandLineArgs.parseInt "repeat" 1 + +val diff = + if query_size = 0 then + run_rounds (fn _ => eval_build_im size) rep + else + let + val c = eval_build_im size + val curr = eval_queries_im (#3 c) + in + run_rounds (fn _ => curr query_size) rep + end + +val _ = print ("total " ^ Time.fmt 4 diff ^ "s\n") +val avg = Time.toReal diff / (Real.fromInt rep) +val _ = print ("average " ^ Real.fmt (StringCvt.FIX (SOME 4)) avg ^ "s\n") + + diff --git a/tests/interval-tree/new-main.sml b/tests/interval-tree/new-main.sml new file mode 100644 index 000000000..e8499065b --- /dev/null +++ b/tests/interval-tree/new-main.sml @@ -0,0 +1,65 @@ +structure CLA = CommandLineArgs + +val q = CLA.parseInt "q" 100 +val n = CLA.parseInt "n" 1000000 + +val _ = print ("n " ^ Int.toString n ^ "\n") +val _ = print ("q " ^ Int.toString q ^ "\n") + +val max_size = 1000000000 +(* val gap_size = 100 *) + +fun randRange i j seed = + i + Word64.toInt + (Word64.mod (Util.hash64 (Word64.fromInt seed), Word64.fromInt (j - i))) + +(* +fun randSeg seed = + let + val p = gap_size * (randRange 1 (max_size div gap_size) seed) + val hi = Int.min (p + gap_size, max_size) + in + (p, randRange p hi (seed+1)) + end +*) + +fun randSeg seed = + let + val p = randRange 1 max_size seed + val space = max_size - p + val hi = p + 1 + space div 100 + in + (p, randRange p hi (seed+1)) + end + +(* fun query seed = + IntervalMap.stab tree (randRange 1 max_size seed) *) + +fun query tree seed = + IntervalMap.size (IntervalMap.report_all tree (randRange 1 max_size seed)) + +fun bench () = + let + val (tree, tm) = Util.getTime (fn _ => + IntervalMap.interval_map (Seq.tabulate (fn i => randSeg (2*i)) n) n) + val _ = print ("generated tree in " ^ Time.fmt 4 tm ^ "s\n") + in + ArraySlice.full (SeqBasis.tabulate 1 (0, q) (fn i => query tree (2*n + i))) + end + +val result = Benchmark.run "generating and stabbing intervals..." bench + +(* val numHits = Seq.reduce op+ 0 (Seq.map (fn true => 1 | _ => 0) result) +val _ = print ("hits " ^ Int.toString numHits ^ "\n") + +val hitrate = Real.round (100.0 * (Real.fromInt numHits / Real.fromInt q)) +val _ = print ("hitrate " ^ Int.toString hitrate ^ "%\n") *) + +val numHits = Seq.reduce op+ 0 result +val minHits = Seq.reduce Int.min (valOf Int.maxInt) result +val maxHits = Seq.reduce Int.max 0 result +val avgHits = Real.round (Real.fromInt numHits / Real.fromInt q) +val _ = print ("hits " ^ Int.toString numHits ^ "\n") +val _ = print ("min " ^ Int.toString minHits ^ "\n") +val _ = print ("avg " ^ Int.toString avgHits ^ "\n") +val _ = print ("max " ^ Int.toString maxHits ^ "\n") diff --git a/tests/linearrec-opt/LinearRec.sml b/tests/linearrec-opt/LinearRec.sml new file mode 100644 index 000000000..b6023f1b2 --- /dev/null +++ b/tests/linearrec-opt/LinearRec.sml @@ -0,0 +1,66 @@ +structure LinearRec = +struct + + structure A = Array + structure AS = ArraySlice + + fun upd a i x = A.update (a, i, x) + fun nth a i = A.sub (a, i) + + val parfor = ForkJoin.parfor + val par = ForkJoin.par + val allocate = ForkJoin.alloc + + type elem = real * real + + fun scanMap grain (g: elem * elem -> elem) b (lo, hi) (f : int -> elem) (out: elem -> 'a) = + if hi - lo <= grain then + let + val n = hi - lo + val result = allocate (n+1) + fun bump ((j,b),x) = (upd result j (out b); (j+1, g (b, x))) + val (_, total) = SeqBasis.foldl bump (0, b) (lo, hi) f + in + upd result n (out total); + result + end + else + let + val n = hi - lo + val k = grain + val m = 1 + (n-1) div k (* number of blocks *) + val sums = SeqBasis.tabulate 1 (0, m) (fn i => + let val start = lo + i*k + in SeqBasis.foldl g b (start, Int.min (start+k, hi)) f + end) + val partials = SeqBasis.scan grain g b (0, m) (nth sums) + val result = allocate (n+1) + in + parfor 1 (0, m) (fn i => + let + fun bump ((j,b),x) = (upd result j (out b); (j+1, g (b, x))) + val start = lo + i*k + in + SeqBasis.foldl bump (i*k, nth partials i) (start, Int.min (start+k, hi)) f; + () + end); + upd result n (out (nth partials m)); + result + end + + (* ====================================================================== *) + + fun combine ((x1, y1), (x2, y2)) = + (x1 * x2, y1 * x2 + y2) + + val id = (1.0, 0.0) + + fun linearRec s = + let + val result = + scanMap 5000 combine id (0, Seq.length s) (Seq.nth s) #2 + in + Seq.drop (AS.full result) 1 + end + +end diff --git a/tests/linearrec-opt/linearrec-opt.mlb b/tests/linearrec-opt/linearrec-opt.mlb new file mode 100644 index 000000000..bcb449aeb --- /dev/null +++ b/tests/linearrec-opt/linearrec-opt.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +LinearRec.sml +main.sml diff --git a/tests/linearrec-opt/main.sml b/tests/linearrec-opt/main.sml new file mode 100644 index 000000000..55a07b315 --- /dev/null +++ b/tests/linearrec-opt/main.sml @@ -0,0 +1,21 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure L = LinearRec + +val n = CLA.parseInt "n" (1000 * 1000 * 100) + +val _ = print ("n " ^ Int.toString n ^ "\n") + +(* fun gen i = + Real.fromInt ((Util.hash i) mod 1000 - 500) / 500.0 *) + +val input = + Seq.tabulate (fn i => (1.0, 1.0)) n + +fun task () = + L.linearRec input + +val result = Benchmark.run "linear recurrence" task +val x = Seq.nth result (n-1) +val _ = print ("result " ^ Real.toString x ^ "\n") diff --git a/tests/linearrec/MkLinearRec.sml b/tests/linearrec/MkLinearRec.sml new file mode 100644 index 000000000..e57ebb22e --- /dev/null +++ b/tests/linearrec/MkLinearRec.sml @@ -0,0 +1,12 @@ +functor MkLinearRec (Seq: SEQUENCE) = +struct + + fun combine ((x1, y1), (x2, y2)) = + (x1 * x2, y1 * x2 + y2) + + val id = (1.0, 0.0) + + fun linearRec s = + Seq.toArraySeq (Seq.map #2 (Seq.scanIncl combine id (Seq.fromArraySeq s))) + +end diff --git a/tests/linearrec/linearrec.mlb b/tests/linearrec/linearrec.mlb new file mode 100644 index 000000000..6345c31e0 --- /dev/null +++ b/tests/linearrec/linearrec.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MkLinearRec.sml +main.sml diff --git a/tests/linearrec/main.sml b/tests/linearrec/main.sml new file mode 100644 index 000000000..4eb449dfb --- /dev/null +++ b/tests/linearrec/main.sml @@ -0,0 +1,21 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure L = MkLinearRec(OldDelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) + +val _ = print ("n " ^ Int.toString n ^ "\n") + +(* fun gen i = + Real.fromInt ((Util.hash i) mod 1000 - 500) / 500.0 *) + +val input = + Seq.tabulate (fn i => (1.0, 1.0)) n + +fun task () = + L.linearRec input + +val result = Benchmark.run "linear recurrence" task +val x = Seq.nth result (n-1) +val _ = print ("result " ^ Real.toString x ^ "\n") diff --git a/tests/linefit-opt/LineFit.sml b/tests/linefit-opt/LineFit.sml new file mode 100644 index 000000000..94e8bfd68 --- /dev/null +++ b/tests/linefit-opt/LineFit.sml @@ -0,0 +1,25 @@ +structure LineFit = +struct + + fun linefit (points : (real * real) Seq.t) = + let + + val n = Real.fromInt (Seq.length points) + fun sumPair((x1,y1),(x2,y2)) = (x1 + x2, y1 + y2) + fun sum f = + SeqBasis.reduce 5000 sumPair (0.0, 0.0) + (0, Seq.length points) + (f o Seq.nth points) + (* Seq.reduce sumPair (0.0, 0.0) (Seq.map f points) *) + + fun square x = x * x + val (xsum, ysum) = sum (fn (x,y) => (x,y)) + val (xa, ya) = (xsum/n, ysum/n) + val (Stt, bb) = sum (fn (x,y) => (square(x - xa), (x - xa) * y)) + val b = bb / Stt + val a = ya - xa * b + in + (a, b) + end + +end diff --git a/tests/linefit-opt/linefit-opt.mlb b/tests/linefit-opt/linefit-opt.mlb new file mode 100644 index 000000000..5eca75c27 --- /dev/null +++ b/tests/linefit-opt/linefit-opt.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +LineFit.sml +main.sml diff --git a/tests/linefit-opt/main.sml b/tests/linefit-opt/main.sml new file mode 100644 index 000000000..974964ac8 --- /dev/null +++ b/tests/linefit-opt/main.sml @@ -0,0 +1,32 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence +structure LF = LineFit + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val doCheck = CLA.parseFlag "check" + +fun gen i = + (Real.fromInt i, Real.fromInt i) + +val input = + Seq.tabulate gen n + +fun task () = + LF.linefit input + +fun check result = + if not doCheck then () else + let + val (a, b) = result + val correct = + Real.< (Real.abs a , 0.000001) andalso + Real.< (Real.abs (b-1.0), 0.000001) + in + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + end + +val result = Benchmark.run "linefit" task +val _ = check result diff --git a/tests/linefit/MkLineFit.sml b/tests/linefit/MkLineFit.sml new file mode 100644 index 000000000..d4c858371 --- /dev/null +++ b/tests/linefit/MkLineFit.sml @@ -0,0 +1,21 @@ +functor MkLineFit (Seq : SEQUENCE) = +struct + + fun linefit (points : (real * real) ArraySequence.t) = + let + val points = Seq.fromArraySeq points + + val n = Real.fromInt (Seq.length points) + fun sumPair((x1,y1),(x2,y2)) = (x1 + x2, y1 + y2) + fun sum f = Seq.reduce sumPair (0.0, 0.0) (Seq.map f points) + fun square x = x * x + val (xsum, ysum) = sum (fn (x,y) => (x,y)) + val (xa, ya) = (xsum/n, ysum/n) + val (Stt, bb) = sum (fn (x,y) => (square(x - xa), (x - xa) * y)) + val b = bb / Stt + val a = ya - xa * b + in + (a, b) + end + +end diff --git a/tests/linefit/linefit.mlb b/tests/linefit/linefit.mlb new file mode 100644 index 000000000..e979f3232 --- /dev/null +++ b/tests/linefit/linefit.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MkLineFit.sml +main.sml diff --git a/tests/linefit/main.sml b/tests/linefit/main.sml new file mode 100644 index 000000000..06170ddd2 --- /dev/null +++ b/tests/linefit/main.sml @@ -0,0 +1,34 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +(* chosen by subdirectory *) +structure LF = MkLineFit(OldDelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val doCheck = CLA.parseFlag "check" + +fun gen i = + (Real.fromInt i, Real.fromInt i) + +val input = + Seq.tabulate gen n + +fun task () = + LF.linefit input + +fun check result = + if not doCheck then () else + let + val (a, b) = result + val correct = + Real.< (Real.abs a , 0.000001) andalso + Real.< (Real.abs (b-1.0), 0.000001) + in + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + end + +val result = Benchmark.run "linefit" task +val _ = check result diff --git a/tests/low-d-decomp/LDD.sml b/tests/low-d-decomp/LDD.sml new file mode 100644 index 000000000..32c1b67b1 --- /dev/null +++ b/tests/low-d-decomp/LDD.sml @@ -0,0 +1,172 @@ +structure LDD = +struct + type 'a seq = 'a Seq.t + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + structure AS = ArraySlice + structure DS = DelayedSeq + + type vertex = G.vertex + + fun strip s = + let val (s', start, _) = AS.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun vertex_map g f h = + AS.full (SeqBasis.tabFilter 10000 (0, G.numVertices g) (fn i => if h(i) then f(i) else NONE)) + + (* inplace Knuth shuffle [l, r) *) + fun seq_random_shuffle s l r seed = + let + fun item i = AS.sub (s, i) + fun set (i, v) = AS.update (s, i, v) + (* get a random idx in [l, i] *) + fun rand_idx i = Int.mod (Util.hash (seed + i), i - l + 1) + l + fun swap (i,j) = + let + val tmp = item i + in + set(i, item j); set(j, tmp) + end + fun shuffle_helper li = + if r - li < 2 then () + else (swap (li, rand_idx li); shuffle_helper (li + 1)) + in + shuffle_helper l + end + + fun log2_up n = Real.ceil (Math.log10 (Real.fromInt n) / (Math.log10 2.0)) + + fun bit_and (n, mask) = Word.toInt (Word.andb (Word.fromInt n, mask)) + fun range_check s n = Seq.length (Seq.filter (fn i => i >= n) s) = 0 + + fun shuffle s (n : int) seed = + if n < 1000 then + let + val cs = Seq.map (fn i => i) s + val _ = seq_random_shuffle cs 0 n 0 + in + cs + end + else + let + val l = log2_up n + val bits = if n < Real.floor (Math.pow (2.0, 27.0)) then Int.div ((l - 7), 2) + else l - 17 + val num_buckets = Real.floor (Math.pow (2.0, Real.fromInt bits)) + val mask = Word.fromInt (num_buckets - 1) + fun rand_pos i = bit_and (Util.hash (seed + i), mask) + (* size of bucket_offsets = num_buckets + 1 *) + val (s', bucket_offsets) = CountingSort.sort s rand_pos num_buckets + fun bucket_shuffle i = seq_random_shuffle s' (Seq.nth bucket_offsets i) (Seq.nth bucket_offsets (i + 1)) seed + val _ = ForkJoin.parfor 1 (0, num_buckets) bucket_shuffle + in + s' + end + + fun partition n b = + let + val s = shuffle (Seq.tabulate (fn i => i) n) n 0 + fun subseq s (i, j) = Seq.subseq s (i, j - i) + fun distribute w acc = + let + val i = Real.fromInt (List.length acc) + val bi = Real.floor (Math.pow (Math.e, b*i)) + val r = w + bi + in + if r >= n then (subseq s (w, n))::acc + else distribute r ((subseq s (w, r))::acc) + end + in + Seq.rev (Seq.fromList (distribute 0 [])) + end + + fun ldd g b = + let + val n = G.numVertices g + val (pr, tm) = Util.getTime (fn _ => partition n b) + val _ = print ("partition: " ^ Time.fmt 4 tm ^ "\n") + val pr_len = Seq.length pr + val visited = strip (Seq.tabulate (fn i => false) n) + val cluster = Seq.tabulate (fn i => n + 1) n + val parent = Seq.tabulate (fn i => n + 1) n + + fun item s i = AS.sub (s, i) + fun set s i v = AS.update (s, i, v) + + fun initialize_cluster u = + if (Seq.nth cluster u) > n then + (set cluster u u; Array.update (visited, u, true); SOME(u)) + else NONE + + fun update (s, d) = + if not (Concurrency.casArray (visited, d) (false, true)) then + (set cluster d (item cluster s); set parent d s; (SOME d)) + else NONE + + fun cond u = not (Array.sub (visited, u)) + + fun ldd_helper fr i = + if i >= pr_len then () + else + let + val (fr', tm) = Util.getTime (fn _ => + let + val pri = Seq.nth pr i + val new_clusters = SeqBasis.tabFilter 1000 (0, Seq.length pri) (fn i => initialize_cluster (Seq.nth pri i)) + (* val new_clusters = Seq.filter (fn v => (Seq.nth cluster v) > n) (Seq.nth pr i) *) + (* val _ = Seq.foreach new_clusters initialize_cluster *) + val fr_len = Seq.length fr + val nc = AS.full new_clusters + val app_frontier = fn i => if (i < fr_len) then Seq.nth fr i else Seq.nth nc (i - fr_len) + (* val fr' = Seq.append (fr, nc) *) + in + (app_frontier, fr_len + (Seq.length nc)) + end) + val _ = print ("round " ^ Int.toString i ^ ": new_clusters: " ^ Time.fmt 4 tm ^ "\n") + val (fr'', tm) = Util.getTime (fn _ => AdjInt.edge_map g fr' update cond) + val _ = print ("round " ^ Int.toString i ^ ": edge_map: " ^ Time.fmt 4 tm ^ "\n") + in + ldd_helper fr'' (i + 1) + end + in + (ldd_helper (Seq.empty ()) 0; + (cluster, parent)) + end + + fun check_ldd g c p = + let + val m = G.numEdges g + val n = G.numVertices g + val arr_set = strip (Seq.tabulate (fn i => false) n) + val tups = Seq.tabulate (fn i => (i, Seq.nth c i)) (Seq.length c) + fun outgoing cid = + let + val s = Seq.map (fn (i, j) => i) (Seq.filter (fn (i, j) => j = cid) tups) + val _ = Seq.foreach s (fn (_ ,v) => Array.update (arr_set, v, true)) + fun greater_neighbors v = Seq.filter (fn u => v < u) (G.neighbors g v) + val grt_neighbors = Seq.flatten (Seq.map greater_neighbors s) + val grt_out_neighbors = Seq.filter (fn v => not (Array.sub(arr_set, v))) grt_neighbors + val _ = Seq.foreach s (fn (_ ,v) => Array.update (arr_set, v, false)) + in + if (Seq.length s) = 0 then ~1 + else Seq.length grt_out_neighbors + end + + fun check_helper i cc ce = + if i >= n then (cc, ce) + else + let + val num_outgoing = outgoing i + in + if num_outgoing = ~1 then check_helper (i + 1) cc ce + else (check_helper (i + 1) (cc + 1) (ce + num_outgoing)) + end + val (cc, ce) = check_helper 0 0 0 + in + print ("num clusters = " ^ (Int.toString cc) ^ ", num edges = " ^ (Int.toString m) ^ ", inter-edges = " ^ (Int.toString ce) ^ "\n") + end + fun slts s = " " ^ Int.toString (Seq.length s) ^ " " + fun print_seq si s se = (print (si ^ " "); Seq.foreach s (fn (_,v) => print ((Int.toString v)^ " ") ) ; print (" " ^ se ^ "\n")) +end diff --git a/tests/low-d-decomp/ldd-alt.sml b/tests/low-d-decomp/ldd-alt.sml new file mode 100644 index 000000000..c32a54cb7 --- /dev/null +++ b/tests/low-d-decomp/ldd-alt.sml @@ -0,0 +1,132 @@ +structure LDD = +struct + type 'a seq = 'a Seq.t + structure G = AdjacencyGraph(Int) + (* structure VS = G.VertexSubset *) + structure V = G.Vertex + structure AS = ArraySlice + + type vertex = G.vertex + + fun strip s = + let val (s', start, _) = AS.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun partition n b = + let + val s = Shuffle.shuffle (Seq.tabulate (fn i => i) n) 0 + fun subseq s (i, j) = Seq.subseq s (i, j - i) + fun distribute w acc = + let + val i = Real.fromInt (List.length acc) + val bi = Real.floor (Math.pow (Math.e, b*i)) + val r = w + bi + in + if r >= n then (subseq s (w, n))::acc + else distribute r ((subseq s (w, r))::acc) + end + in + Seq.rev (Seq.fromList (distribute 0 [])) + end + + fun ldd g b = + let + val n = G.numVertices g + val (pr, tm) = Util.getTime (fn _ => partition n b) + val _ = print ("partition: " ^ Time.fmt 4 tm ^ "\n") + val pr_len = Seq.length pr + val visited = strip (Seq.tabulate (fn i => false) n) + val cluster = Seq.tabulate (fn i => n + 1) n + val parent = Seq.tabulate (fn i => n + 1) n + + fun item s i = AS.sub (s, i) + fun set s i v = AS.update (s, i, v) + + fun initialize_cluster u = + if (Seq.nth cluster u) > n then + (set cluster u u; Array.update (visited, u, true); SOME(u)) + else NONE + + fun updateseq (s, d) = + if (Array.sub (visited, d)) then NONE + else + (Array.update(visited, d, true); set cluster d (item cluster s); set parent d s; (SOME d)) + + fun updatepar (s, d) = + if not (Concurrency.casArray (visited, d) (false, true)) then + (set cluster d (item cluster s); set parent d s; (SOME d)) + else NONE + + val update = (updatepar, updateseq) + + fun cond u = not (Array.sub (visited, u)) + val deg = Int.div (G.numEdges g, G.numVertices g) + val denseThreshold = G.numEdges g div (20*(1 + deg)) + + fun ldd_helper fr i = + if i >= pr_len then () + else + let + val (fr', tm) = Util.getTime (fn _ => + let + val pri = Seq.nth pr i + val new_clusters = SeqBasis.tabFilter 1000 (0, Seq.length pri) (fn i => initialize_cluster (Seq.nth pri i)) + (* val new_clusters = Seq.filter (fn v => (Seq.nth cluster v) > n) (Seq.nth pr i) *) + (* val _ = Seq.foreach new_clusters initialize_cluster *) + (* val fr_len = Seq.length fr *) + val nc = AS.full new_clusters + val app_frontier = AdjInt.append fr nc n + (* fn i => if (i < fr_len) then Seq.nth fr i else Seq.nth nc (i - fr_len) *) + (* val fr' = Seq.append (fr, nc) *) + in + (* (app_frontier, fr_len + (Seq.length nc)) *) + app_frontier + end) + val _ = print ("round " ^ Int.toString i ^ ": new_clusters: " ^ Time.fmt 4 tm ^ "\n") + val (fr'', tm) = Util.getTime (fn _ => AdjInt.edge_map g fr' update cond) + (* val b = if (should_process_sparse g fr') then "sparse" else "dense" *) + val _ = print ("round " ^ Int.toString i ^ " edge_map: " ^ Time.fmt 4 tm ^ "\n") + in + ldd_helper fr'' (i + 1) + end + in + (ldd_helper (AdjInt.empty (denseThreshold)) 0; + (cluster, parent)) + end + + fun check_ldd g c p = + let + val m = G.numEdges g + val n = G.numVertices g + val arr_set = strip (Seq.tabulate (fn i => false) n) + val tups = Seq.tabulate (fn i => (i, Seq.nth c i)) (Seq.length c) + fun outgoing cid = + let + val s = Seq.map (fn (i, j) => i) (Seq.filter (fn (i, j) => j = cid) tups) + val _ = Seq.foreach s (fn (_ ,v) => Array.update (arr_set, v, true)) + fun greater_neighbors v = Seq.filter (fn u => v < u) (G.neighbors g v) + val grt_neighbors = Seq.flatten (Seq.map greater_neighbors s) + val grt_out_neighbors = Seq.filter (fn v => not (Array.sub(arr_set, v))) grt_neighbors + val _ = Seq.foreach s (fn (_ ,v) => Array.update (arr_set, v, false)) + in + if (Seq.length s) = 0 then ~1 + else Seq.length grt_out_neighbors + end + + fun check_helper i cc ce = + if i >= n then (cc, ce) + else + let + val num_outgoing = outgoing i + in + if num_outgoing = ~1 then check_helper (i + 1) cc ce + else (check_helper (i + 1) (cc + 1) (ce + num_outgoing)) + end + val (cc, ce) = check_helper 0 0 0 + in + print ("num clusters = " ^ (Int.toString cc) ^ ", num edges = " ^ (Int.toString m) ^ ", inter-edges = " ^ (Int.toString ce) ^ "\n") + end + fun slts s = " " ^ Int.toString (Seq.length s) ^ " " + fun print_seq si s se = (print (si ^ " "); Seq.foreach s (fn (_,v) => print ((Int.toString v)^ " ") ) ; print (" " ^ se ^ "\n")) +end diff --git a/tests/low-d-decomp/low-d-decomp.mlb b/tests/low-d-decomp/low-d-decomp.mlb new file mode 100644 index 000000000..3253cfe34 --- /dev/null +++ b/tests/low-d-decomp/low-d-decomp.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +ldd-alt.sml +main.sml + diff --git a/tests/low-d-decomp/main.sml b/tests/low-d-decomp/main.sml new file mode 100644 index 000000000..ebf7d2163 --- /dev/null +++ b/tests/low-d-decomp/main.sml @@ -0,0 +1,80 @@ +structure CLA = CommandLineArgs +structure G = AdjacencyGraph(Int) + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + + +val b = (CommandLineArgs.parseReal "b" 0.3) + +val (cluster, parent) = + Benchmark.run "running ldd: " (fn _ => LDD.ldd graph b) + +(* val numClusters = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length cluster) (fn i => + if Seq.nth cluster i = i then 1 else 0) *) +(* val _ = print ("num clusters " ^ Int.toString numClusters ^ "\n") *) + +(* val _ = print ("num-triangles = " ^ (Int.toString P) ^ "\n") *) +(* val _ = LDD.check_ldd graph cluster parent *) +(* val _ = Benchmark.run "running connectivity" (fn _ => LDD.connectivity graph b) *) +(* +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + +val _ = GCStats.report () *) diff --git a/tests/max-indep-set/MIS.sml b/tests/max-indep-set/MIS.sml new file mode 100644 index 000000000..adaf94935 --- /dev/null +++ b/tests/max-indep-set/MIS.sml @@ -0,0 +1,105 @@ +structure MIS = +struct + + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + structure AS = ArraySlice + + fun zero_out_neighbors g roots pr = + let + fun updateseq (s, d) = + if (Array.sub (pr, d) = 0) then NONE + else + (Array.update (pr, d, 0); SOME d) + + fun updatepar (s, d) = + let + val v = Array.sub (pr, d) + in + if (v = 0) then NONE + else if (Concurrency.casArray (pr, d) (v, 0) = v) then (SOME d) + else NONE + end + + val update = (updatepar, updatepar) + val removed = AdjInt.edge_map g roots update (fn d => Array.sub (pr, d) > 0) + in + removed + end + + fun strip s = + let val (s', start, _) = AS.base s + in if start = 0 then s' else raise Fail "strip base <> 0" + end + + fun mis g = + let + val (n, m) = (G.numVertices g, G.numEdges g) + val deg = Int.div (m, n) + val P = Shuffle.shuffle (Seq.tabulate (fn i => i) n) 0 + val pr = SeqBasis.tabulate 100 (0, n) + (fn i => let + val my_prio = Seq.nth P i + in + Seq.iterate (fn (acc, j) => acc + (if (Seq.nth P j) < my_prio then 1 else 0)) 0 (G.neighbors g i) + end) + val pr_seq = AS.full pr + + val denseThreshold = G.numEdges g div (20*(1 + deg)) + val sparse_rep = AS.full (SeqBasis.filter 10000 (0, n) (fn i => i) (fn i => Seq.nth pr_seq i = 0)) + val ind_set = Seq.tabulate (fn _ => false) n + + val roots = AdjInt.from_sparse_rep sparse_rep denseThreshold n + fun loop_roots finished roots = + if finished < n then + let + val _ = AdjInt.vertex_foreach g roots (fn u => if (Seq.nth ind_set u) then () else AS.update (ind_set, u, true)) + val removed = zero_out_neighbors g roots pr + fun decrement_priority_par (s, d) = + let + val p = Seq.nth P s + val q = Seq.nth P d + in + if p >= q then NONE + else if (Concurrency.faaArray (pr, d) (~1) = 1) then SOME d + else NONE + end + fun decrement_priority_seq (s, d) = + let + val p = Seq.nth P s + val q = Seq.nth P d + val prio = Array.sub (pr, d) + val _ = if prio <= 0 orelse p >= q then () + else Array.update (pr, d, prio-1) + in + if (prio = 1) then SOME d + else NONE + end + val dec = (decrement_priority_par, decrement_priority_seq) + val new_roots = AdjInt.edge_map g removed dec (fn d => Array.sub (pr, d) > 0) + in + loop_roots (finished + (AdjInt.size roots) + (AdjInt.size removed)) new_roots + end + else () + in + loop_roots 0 roots; + ind_set + end + + fun verify_mis g ind_set = + let + val (n, m) = (G.numVertices g, G.numEdges g) + val int_ind = Seq.tabulate (fn i => 0) n + fun ok_f u = + let + val count = Seq.iterate (fn (acc, b) => if (Seq.nth ind_set b) then acc + 1 else acc) 0 (G.neighbors g u) + in + (Seq.nth ind_set u) orelse (not (count = 0)) + end + val bool_ok = Seq.tabulate ok_f n + val all_ok = Seq.reduce (fn (b, acc) => b andalso acc) true bool_ok + in + if all_ok then () + else print ("Invalid Independent Set\n") + end +end diff --git a/tests/max-indep-set/faa.mlton.sml b/tests/max-indep-set/faa.mlton.sml new file mode 100644 index 000000000..fa2992415 --- /dev/null +++ b/tests/max-indep-set/faa.mlton.sml @@ -0,0 +1,11 @@ +structure Concurrency = +struct + open Concurrency + + fun faaArray (a, i) x = + let + val rx = Array.sub (a, i) + in + (Array.update (a, i, rx + x); rx) + end +end diff --git a/tests/max-indep-set/faa.mpl.sml b/tests/max-indep-set/faa.mpl.sml new file mode 100644 index 000000000..3056feee4 --- /dev/null +++ b/tests/max-indep-set/faa.mpl.sml @@ -0,0 +1,5 @@ +structure Concurrency = +struct + open Concurrency + val faaArray = MLton.Parallel.arrayFetchAndAdd +end diff --git a/tests/max-indep-set/main.sml b/tests/max-indep-set/main.sml new file mode 100644 index 000000000..804e23b2d --- /dev/null +++ b/tests/max-indep-set/main.sml @@ -0,0 +1,86 @@ +structure CLA = CommandLineArgs +structure G = AdjacencyGraph(Int) + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + + +val b = (CommandLineArgs.parseReal "b" 0.3) + +val ind_set = + Benchmark.run "running independent set: " (fn _ => MIS.mis graph) + +val c = Seq.reduce op+ 0 (Seq.map (fn i => if i then 1 else 0) ind_set) +val _ = print ("num elements = " ^ (Int.toString c) ^ "\n") + +val _ = MIS.verify_mis graph ind_set + + +(* val numClusters = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length cluster) (fn i => + if Seq.nth cluster i = i then 1 else 0) *) +(* val _ = print ("num clusters " ^ Int.toString numClusters ^ "\n") *) + +(* val _ = print ("num-triangles = " ^ (Int.toString P) ^ "\n") *) +(* val _ = LDD.check_ldd graph cluster parent *) +(* val _ = Benchmark.run "running connectivity" (fn _ => LDD.connectivity graph b) *) +(* +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + +val _ = GCStats.report () *) diff --git a/tests/max-indep-set/max-indep-set.mlb b/tests/max-indep-set/max-indep-set.mlb new file mode 100644 index 000000000..ff8c5eb1b --- /dev/null +++ b/tests/max-indep-set/max-indep-set.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +faa.$(COMPAT).sml +MIS.sml +main.sml + diff --git a/tests/mcss-opt/MCSS.sml b/tests/mcss-opt/MCSS.sml new file mode 100644 index 000000000..4ed408de2 --- /dev/null +++ b/tests/mcss-opt/MCSS.sml @@ -0,0 +1,30 @@ +structure MCSS = +struct + + val max = Real.max + + fun combine((l1,r1,b1,t1),(l2,r2,b2,t2)) = + (max(l1, t1+l2), + max(r2, r1+t2), + max(r1+l2, max(b1,b2)), + t1+t2) + + val id = (0.0, 0.0, 0.0, 0.0) + + fun singleton v = + let + val vp = max (v, 0.0) + in + (vp, vp, vp, v) + end + + fun mcss (s : real Seq.t) : real = + let + val (_,_,b,_) = + SeqBasis.reduce 5000 combine id (0, Seq.length s) + (fn i => singleton (Seq.nth s i)) + in + b + end + +end diff --git a/tests/mcss-opt/main.sml b/tests/mcss-opt/main.sml new file mode 100644 index 000000000..845f60dc3 --- /dev/null +++ b/tests/mcss-opt/main.sml @@ -0,0 +1,20 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure M = MCSS + +val n = CLA.parseInt "n" (1000 * 1000 * 100) + +fun gen i = + Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 (Word64.fromInt i), 0w1000)) - 500) / 500.0 + +val input = + Seq.tabulate gen n + +fun task () = + M.mcss input + +val result = Benchmark.run "mcss" task +val _ = print ("result " ^ Real.toString result ^ "\n") + +val _ = print ("input " ^ Util.summarizeArraySlice 12 Real.toString input ^ "\n") diff --git a/tests/mcss-opt/mcss-opt.mlb b/tests/mcss-opt/mcss-opt.mlb new file mode 100644 index 000000000..bc959d4e7 --- /dev/null +++ b/tests/mcss-opt/mcss-opt.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MCSS.sml +main.sml diff --git a/tests/mcss/MkMapReduceMCSS.sml b/tests/mcss/MkMapReduceMCSS.sml new file mode 100644 index 000000000..7a1af36a1 --- /dev/null +++ b/tests/mcss/MkMapReduceMCSS.sml @@ -0,0 +1,29 @@ +functor MkMapReduceMCSS (Seq : SEQUENCE) = +struct + + val max = Real.max + + fun combine((l1,r1,b1,t1),(l2,r2,b2,t2)) = + (max(l1, t1+l2), + max(r2, r1+t2), + max(r1+l2, max(b1,b2)), + t1+t2) + + val id = (0.0, 0.0, 0.0, 0.0) + + fun singleton v = + let + val vp = max (v, 0.0) + in + (vp, vp, vp, v) + end + + fun mcss (s : real ArraySequence.t) : real = + let + val (_,_,b,_) = + Seq.reduce combine id (Seq.map singleton (Seq.fromArraySeq s)) + in + b + end + +end diff --git a/tests/mcss/MkScanMCSS.sml b/tests/mcss/MkScanMCSS.sml new file mode 100644 index 000000000..f037c7c44 --- /dev/null +++ b/tests/mcss/MkScanMCSS.sml @@ -0,0 +1,24 @@ +functor MkScanMCSS (Seq: SEQUENCE) = +struct + + fun mcss (s: real ArraySequence.t) : real = + let + val s = Seq.fromArraySeq s + val t = Util.startTiming () + + val p = Seq.scanIncl op+ 0.0 s + val t = Util.tick t "plus scan" + + val (m, _) = Seq.scan Real.min Real.posInf p + val t = Util.tick t "min scan" + + val b = Seq.zipWith op- (p, m) + val t = Util.tick t "zipWith" + + val result = Seq.reduce Real.max Real.negInf b + val t = Util.tick t "reduce" + in + result + end + +end diff --git a/tests/mcss/main.sml b/tests/mcss/main.sml new file mode 100644 index 000000000..43dd22a26 --- /dev/null +++ b/tests/mcss/main.sml @@ -0,0 +1,18 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure M = MkMapReduceMCSS (DelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) + +fun gen i = + Real.fromInt ((Util.hash i) mod 1000 - 500) / 500.0 + +val input = + Seq.tabulate gen n + +fun task () = + M.mcss input + +val result = Benchmark.run "mcss" task +val _ = print ("result " ^ Real.toString result ^ "\n") diff --git a/tests/mcss/mcss.mlb b/tests/mcss/mcss.mlb new file mode 100644 index 000000000..80d1c2a88 --- /dev/null +++ b/tests/mcss/mcss.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MkMapReduceMCSS.sml +main.sml diff --git a/tests/msort-int32/msort-int32.mlb b/tests/msort-int32/msort-int32.mlb new file mode 100644 index 000000000..394068b7b --- /dev/null +++ b/tests/msort-int32/msort-int32.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +msort.sml diff --git a/tests/msort-int32/msort.sml b/tests/msort-int32/msort.sml new file mode 100644 index 000000000..c88cd5aca --- /dev/null +++ b/tests/msort-int32/msort.sml @@ -0,0 +1,16 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" (100 * 1000 * 1000) +val _ = print ("N " ^ Int.toString n ^ "\n") + +val _ = print ("generating " ^ Int.toString n ^ " random integers\n") + +fun elem i = + Int32.fromInt (Word64.toInt (Word64.mod (Util.hash64 (Word64.fromInt i), Word64.fromInt n))) +val input = ArraySlice.full (SeqBasis.tabulate 10000 (0, n) elem) + +val result = + Benchmark.run "running mergesort" (fn _ => Mergesort.sort Int32.compare input) + +val _ = print ("result " ^ Util.summarizeArraySlice 8 Int32.toString result ^ "\n") + diff --git a/tests/msort-strings/msort-strings.mlb b/tests/msort-strings/msort-strings.mlb new file mode 100644 index 000000000..394068b7b --- /dev/null +++ b/tests/msort-strings/msort-strings.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +msort.sml diff --git a/tests/msort-strings/msort.sml b/tests/msort-strings/msort.sml new file mode 100644 index 000000000..6986e6ab2 --- /dev/null +++ b/tests/msort-strings/msort.sml @@ -0,0 +1,34 @@ +structure CLA = CommandLineArgs + +fun usage () = + let + val msg = + "usage: msort-strings FILE\n" + in + TextIO.output (TextIO.stdErr, msg); + OS.Process.exit OS.Process.failure + end + +val filename = + case CLA.positional () of + [x] => x + | _ => usage () + +val makeLong = CLA.parseFlag "long" + +val (contents, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filename) +val _ = print ("read file in " ^ Time.fmt 4 tm ^ "s\n") +val (tokens, tm) = Util.getTime (fn _ => Tokenize.tokens Char.isSpace contents) +val _ = print ("tokenized in " ^ Time.fmt 4 tm ^ "s\n") + +val prefix = CharVector.tabulate (32, fn _ => #"a") + +val tokens = + if not makeLong then tokens + else Seq.map (fn str => prefix ^ str) tokens + +val result = + Benchmark.run "running mergesort" (fn _ => Mergesort.sort String.compare tokens) + +val _ = print ("result " ^ Util.summarizeArraySlice 8 (fn x => x) result ^ "\n") + diff --git a/tests/msort/msort.mlb b/tests/msort/msort.mlb new file mode 100644 index 000000000..394068b7b --- /dev/null +++ b/tests/msort/msort.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +msort.sml diff --git a/tests/msort/msort.sml b/tests/msort/msort.sml new file mode 100644 index 000000000..292204fcb --- /dev/null +++ b/tests/msort/msort.sml @@ -0,0 +1,17 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" (100 * 1000 * 1000) +val _ = print ("N " ^ Int.toString n ^ "\n") + +val _ = print ("generating " ^ Int.toString n ^ " random integers\n") + +fun elem i = + Word64.toInt (Word64.mod (Util.hash64 (Word64.fromInt i), Word64.fromInt n)) +val input = ArraySlice.full (SeqBasis.tabulate 10000 (0, n) elem) + +val result = + Benchmark.run "running mergesort" (fn _ => Mergesort.sort Int.compare input) + +val _ = print ("input " ^ Util.summarizeArraySlice 8 Int.toString input ^ "\n") +val _ = print ("result " ^ Util.summarizeArraySlice 8 Int.toString result ^ "\n") + diff --git a/tests/nearest-nbrs/ParseFile.sml b/tests/nearest-nbrs/ParseFile.sml new file mode 100644 index 000000000..c7eb4e101 --- /dev/null +++ b/tests/nearest-nbrs/ParseFile.sml @@ -0,0 +1,190 @@ +(** SAM_NOTE: copy/pasted... some repetition here with Parse. *) +structure ParseFile = +struct + + structure RF = ReadFile + structure Seq = ArraySequence + structure DS = OldDelayedSeq + + fun tokens (f: char -> bool) (cs: char Seq.t) : (char DS.t) DS.t = + let + val n = Seq.length cs + val s = DS.tabulate (Seq.nth cs) n + val indices = DS.tabulate (fn i => i) (n+1) + fun check i = + if (i = n) then not (f(DS.nth s (n-1))) + else if (i = 0) then not (f(DS.nth s 0)) + else let val i1 = f (DS.nth s i) + val i2 = f (DS.nth s (i-1)) + in (i1 andalso not i2) orelse (i2 andalso not i1) end + val ids = DS.filter check indices + val res = DS.tabulate (fn i => + let val (start, e) = (DS.nth ids (2*i), DS.nth ids (2*i+1)) + in DS.tabulate (fn i => Seq.nth cs (start+i)) (e - start) + end) + ((DS.length ids) div 2) + in + res + end + + fun eqStr str (chars : char DS.t) = + let + val n = String.size str + fun checkFrom i = + i >= n orelse + (String.sub (str, i) = DS.nth chars i andalso checkFrom (i+1)) + in + DS.length chars = n + andalso + checkFrom 0 + end + + fun parseDigit char = + let + val code = Char.ord char + val code0 = Char.ord #"0" + val code9 = Char.ord #"9" + in + if code < code0 orelse code9 < code then + NONE + else + SOME (code - code0) + end + + (* This implementation doesn't work with mpl :( + * Need to fix the basis library... *) + (* + fun parseReal chars = + let + val str = CharVector.tabulate (DS.length chars, DS.nth chars) + in + Real.fromString str + end + *) + + fun parseInt (chars : char DS.t) = + let + val n = DS.length chars + fun c i = DS.nth chars i + + fun build x i = + if i >= n then SOME x else + case c i of + #"," => build x (i+1) + | #"_" => build x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => build (x * 10 + dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1) (build 0 1) + else if (c 0 = #"+") then + build 0 1 + else + build 0 0 + end + + fun parseReal (chars : char DS.t) = + let + val n = DS.length chars + fun c i = DS.nth chars i + + fun buildAfterE x i = + let + val chars' = DS.subseq chars (i, n-i) + in + Option.map (fn e => x * Math.pow (10.0, Real.fromInt e)) + (parseInt chars') + end + + fun buildAfterPoint m x i = + if i >= n then SOME x else + case c i of + #"," => buildAfterPoint m x (i+1) + | #"_" => buildAfterPoint m x (i+1) + | #"." => NONE + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildAfterPoint (m * 0.1) (x + m * (Real.fromInt dig)) (i+1) + + fun buildBeforePoint x i = + if i >= n then SOME x else + case c i of + #"," => buildBeforePoint x (i+1) + | #"_" => buildBeforePoint x (i+1) + | #"." => buildAfterPoint 0.1 x (i+1) + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildBeforePoint (x * 10.0 + Real.fromInt dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1.0) (buildBeforePoint 0.0 1) + else + buildBeforePoint 0.0 0 + end + + fun readSequencePoint2d filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequencePoint2d" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun r i = Option.valOf (parseReal (tok (1 + i))) + + fun pt i = + (r (2*i), r (2*i+1)) + handle e => raise Fail ("error parsing point " ^ Int.toString i ^ " (" ^ exnMessage e ^ ")") + + val result = Seq.tabulate pt (n div 2) + in + result + end + + fun readSequenceInt filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequenceInt" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun p i = + Option.valOf (parseInt (tok (1 + i))) + handle e => raise Fail ("error parsing integer " ^ Int.toString i) + in + Seq.tabulate p n + end + + fun readSequenceReal filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequenceDouble" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun p i = + Option.valOf (parseReal (tok (1 + i))) + handle e => raise Fail ("error parsing double value " ^ Int.toString i) + in + Seq.tabulate p n + end + +end diff --git a/tests/nearest-nbrs/main.sml b/tests/nearest-nbrs/main.sml new file mode 100644 index 000000000..b358805ec --- /dev/null +++ b/tests/nearest-nbrs/main.sml @@ -0,0 +1,160 @@ +structure NN = NearestNeighbors +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" 1000000 +val inputFile = CLA.parseString "input" "" +val leafSize = CLA.parseInt "leafSize" 50 +val grain = CLA.parseInt "grain" 1000 +val seed = CLA.parseInt "seed" 15210 + +fun genReal i = + let + val x = Word64.fromInt (seed + i) + in + Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 x, 0w1000000))) + / 1000000.0 + end + +fun genPoint i = (genReal (2*i), genReal (2*i + 1)) + +(* This silly thing helps ensure good placement, by + * forcing points to be reallocated more adjacent. + * It's a no-op, but gives us as much as 2x time + * improvement (!) + *) +fun swap pts = Seq.map (fn (x, y) => (y, x)) pts +fun compactify pts = swap (swap pts) + +val input = + case inputFile of + "" => + let + val (input, tm) = Util.getTime (fn _ => Seq.tabulate genPoint n) + in + print ("generated input in " ^ Time.fmt 4 tm ^ "s\n"); + input + end + + | filename => + let + val (points, tm) = Util.getTime (fn _ => + compactify (ParseFile.readSequencePoint2d filename)) + in + print ("parsed input points in " ^ Time.fmt 4 tm ^ "s\n"); + points + end + +fun nnEx() = + let + val (tree, tm) = Util.getTime (fn _ => NN.makeTree leafSize input) + val _ = print ("built quadtree in " ^ Time.fmt 4 tm ^ "s\n") + + val (nbrs, tm) = Util.getTime (fn _ => NN.allNearestNeighbors grain tree) + val _ = print ("found all neighbors in " ^ Time.fmt 4 tm ^ "s\n") + in + (tree, nbrs) + end + +val (tree, nbrs) = Benchmark.run "running nearest neighbors" nnEx +val _ = + print ("result " ^ Util.summarizeArraySlice 12 Int.toString nbrs ^ "\n") + +(* now input[nbrs[i]] is the closest point to input[i] *) + +(* ========================================================================== + * write image to output + * this only works if all input points are within [0,1) *) + +val filename = CLA.parseString "output" "" +val _ = + if filename <> "" then () + else ( print ("to see output, use -output and -resolution arguments\n" ^ + "for example: nn -N 10000 -output result.ppm -resolution 1000\n") + ; GCStats.report () + ; OS.Process.exit OS.Process.success + ) + +val t0 = Time.now () + +val resolution = CLA.parseInt "resolution" 1000 +val width = resolution +val height = resolution + +val image = + { width = width + , height = height + , data = Seq.tabulate (fn _ => Color.white) (width*height) + } + +fun set (i, j) x = + if 0 <= i andalso i < height andalso + 0 <= j andalso j < width + then ArraySlice.update (#data image, i*width + j, x) + else () + +val r = Real.fromInt resolution +fun px x = Real.floor (x * r) +fun pos (x, y) = (resolution - px x - 1, px y) + +fun horizontalLine i (j0, j1) = + if j1 < j0 then horizontalLine i (j1, j0) + else Util.for (j0, j1) (fn j => set (i, j) Color.red) + +fun sign xx = + case Int.compare (xx, 0) of LESS => ~1 | EQUAL => 0 | GREATER => 1 + +(* Bresenham's line algorithm *) +fun line (x1, y1) (x2, y2) = + let + val w = x2 - x1 + val h = y2 - y1 + val dx1 = sign w + val dy1 = sign h + val (longest, shortest, dx2, dy2) = + if Int.abs w > Int.abs h then + (Int.abs w, Int.abs h, dx1, 0) + else + (Int.abs h, Int.abs w, 0, dy1) + + fun loop i numerator x y = + if i > longest then () else + let + val numerator = numerator + shortest; + in + set (x, y) Color.red; + if numerator >= longest then + loop (i+1) (numerator-longest) (x+dx1) (y+dy1) + else + loop (i+1) numerator (x+dx2) (y+dy2) + end + in + loop 0 (longest div 2) x1 y1 + end + +(* mark all nearest neighbors with straight red lines *) +val t0 = Time.now () + +val _ = ForkJoin.parfor 10000 (0, Seq.length input) (fn i => + line (pos (Seq.nth input i)) (pos (Seq.nth input (Seq.nth nbrs i)))) + +(* mark input points as a pixel *) +val _ = + ForkJoin.parfor 10000 (0, Seq.length input) (fn i => + let + val (x, y) = pos (Seq.nth input i) + fun b spot = set spot Color.black + in + b (x-1, y); + b (x, y-1); + b (x, y); + b (x, y+1); + b (x+1, y) + end) + +val t1 = Time.now () + +val _ = print ("generated image in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val (_, tm) = Util.getTime (fn _ => PPM.write filename image) +val _ = print ("wrote to " ^ filename ^ " in " ^ Time.fmt 4 tm ^ "s\n") + diff --git a/tests/nearest-nbrs/nearest-nbrs.mlb b/tests/nearest-nbrs/nearest-nbrs.mlb new file mode 100644 index 000000000..dcf87164b --- /dev/null +++ b/tests/nearest-nbrs/nearest-nbrs.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +ParseFile.sml +main.sml diff --git a/tests/nqueens-simple/main.sml b/tests/nqueens-simple/main.sml new file mode 100644 index 000000000..4f05b84ce --- /dev/null +++ b/tests/nqueens-simple/main.sml @@ -0,0 +1,104 @@ +(* ========================================================================== + * VERSION 1: NO GRANULARITY CONTROL + * ========================================================================== + *) + + +(* compute the sum of f(lo), f(lo+1), ..., f(hi-1) *) +fun sum (lo, hi, f) = + if lo >= hi then + 0 + else if lo + 1 = hi then + f lo + else + let + val mid = lo + (hi - lo) div 2 + val (left, right) = ForkJoin.par (fn () => sum (lo, mid, f), fn () => + sum (mid, hi, f)) + in + left + right + end + + +(* queens at positions (row, col) *) +type locations = (int * int) list + + +fun queen_is_threatened (i, j) (other_queens: locations) = + List.exists + (fn (x, y) => i = x orelse j = y orelse i - j = x - y orelse i + j = x + y) + other_queens + + +fun nqueens_count_solutions n = + let + fun search i queens = + if i >= n then + 1 + else + let + fun do_column j = + if queen_is_threatened (i, j) queens then 0 + else search (i + 1) ((i, j) :: queens) + in + sum (0, n, do_column) + end + in + search 0 [] + end + + +(* ========================================================================== + * VERSION 2: MANUAL GRANULARITY CONTROL + * ========================================================================== + *) + + +(* sequential alternative *) +fun sum_serial (lo, hi, f) = + Util.loop (lo, hi) 0 (fn (acc, i) => acc + f i) + + +fun nqueens_count_solutions_manual_gran_control n = + let + fun search i queens = + if i >= n then + 1 + else + let + fun do_column j = + if queen_is_threatened (i, j) queens then 0 + else search (i + 1) ((i, j) :: queens) + in + if i >= 3 then + (* simple heuristic for granularity control: swich to sequential + * algorithm after getting a few levels deep. + *) + sum_serial (0, n, do_column) + else + sum (0, n, do_column) + end + in + search 0 [] + end + + +(* ========================================================================== + * parse command-line arguments and run + * ========================================================================== + *) + +val n = CommandLineArgs.parseInt "N" 13 +val do_gran_control = CommandLineArgs.parseFlag "do-gran-control" +val _ = print ("N " ^ Int.toString n ^ "\n") +val _ = print + ("do-gran-control? " ^ (if do_gran_control then "yes" else "no") ^ "\n") + +val msg = + "counting number of " ^ Int.toString n ^ "x" ^ Int.toString n ^ " solutions" + +val result = Benchmark.run msg (fn _ => + if do_gran_control then nqueens_count_solutions_manual_gran_control n + else nqueens_count_solutions n) + +val _ = print ("result " ^ Int.toString result ^ "\n") diff --git a/tests/nqueens-simple/nqueens-simple.mlb b/tests/nqueens-simple/nqueens-simple.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/nqueens-simple/nqueens-simple.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/nqueens/nqueens.mlb b/tests/nqueens/nqueens.mlb new file mode 100644 index 000000000..8a7897d39 --- /dev/null +++ b/tests/nqueens/nqueens.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +nqueens.sml diff --git a/tests/nqueens/nqueens.sml b/tests/nqueens/nqueens.sml new file mode 100644 index 000000000..e309daa94 --- /dev/null +++ b/tests/nqueens/nqueens.sml @@ -0,0 +1,39 @@ +structure CLA = CommandLineArgs + +type board = (int * int) list + +fun threatened (i,j) [] = false + | threatened (i,j) ((x,y)::Q) = + i = x orelse j = y orelse i-j = x-y orelse i+j = x+y + orelse threatened (i,j) Q + +structure Seq = FuncSequence + +fun countSol n = + let + fun search i b = + if i >= n then 1 else + let + fun tryCol j = + if threatened (i, j) b then 0 else search (i+1) ((i,j)::b) + in + if i >= 3 then + (* if we're already a few levels deep, then just go sequential *) + Seq.iterate op+ 0 (Seq.tabulate tryCol n) + else + Seq.reduce op+ 0 (Seq.tabulate tryCol n) + end + in + search 0 [] + end + +val n = CommandLineArgs.parseInt "N" 13 +val _ = print ("N " ^ Int.toString n ^ "\n") + +val msg = + "counting number of " ^ Int.toString n ^ "x" ^ Int.toString n ^ " solutions" + +val result = Benchmark.run msg (fn _ => countSol n) + +val _ = print ("result " ^ Int.toString result ^ "\n") + diff --git a/tests/ocaml-binarytrees5/main.sml b/tests/ocaml-binarytrees5/main.sml new file mode 100644 index 000000000..cd328b38f --- /dev/null +++ b/tests/ocaml-binarytrees5/main.sml @@ -0,0 +1,90 @@ +structure CLA = CommandLineArgs + +val max_depth = CLA.parseInt "max_depth" 10 +val num_domains = Concurrency.numberOfProcessors + +val _ = print ("max_depth " ^ Int.toString max_depth ^ "\n") + +datatype tree = Empty | Node of tree * tree + +fun make d = + if d = 0 then Node (Empty, Empty) + else let val d = d-1 + in Node (make d, make d) + end + +fun check t = + case t of + Empty => 0 + | Node (l, r) => 1 + check l + check r + +val min_depth = 4 +val max_depth = Int.max (min_depth + 2, max_depth) +val stretch_depth = max_depth + 1 + +val _ = check (make stretch_depth) + +val long_lived_tree = make max_depth + +val values = + Array.array (num_domains, 0) + +fun calculate d st en ind = + let + val c = ref 0 + in + Util.for (st, en+1) (fn _ => + c := !c + check (make d) + ); + Array.update (values, ind, !c) + end + +fun calculate d st en ind = + let + val c = + Util.loop (st, en+1) 0 (fn (c, _) => + c + check (make d) + ) + in + Array.update (values, ind, c) + end + +fun parfor g (i, j) f = + if j-i <= 1 then + (** MPL relies on `par` for its GC policy. This particular benchmark + * happens to not call par on 1 processor, so let's fix that. + *) + (ForkJoin.par (fn _ => ForkJoin.parfor g (i, j) f, fn () => ()); ()) + else + ForkJoin.parfor g (i, j) f + + +fun loop_depths d = + Util.for (0, (max_depth - d) div 2 + 1) (fn i => + let + val d = d + i * 2 + val niter = Util.pow2 (max_depth - d + min_depth) + in + (* ocaml source does N-way async/await loop, but this is just + * a parallel for. *) + parfor 1 (0, num_domains) (fn i => + calculate d + (i * niter div num_domains) + (((i + 1) * niter div num_domains) - 1) + i); + + Array.foldl op+ 0 values; + + () + end) + +val result = Benchmark.run "running binary trees" (fn _ => + let + in + loop_depths min_depth; + check long_lived_tree + end) + +val _ = print ("result " ^ Int.toString result ^ "\n") +val _ = print ("values " ^ Util.summarizeArray 10 Int.toString values ^ "\n") + diff --git a/tests/ocaml-binarytrees5/ocaml-binarytrees5.mlb b/tests/ocaml-binarytrees5/ocaml-binarytrees5.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/ocaml-binarytrees5/ocaml-binarytrees5.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/ocaml-binarytrees5/ocaml-source.ml b/tests/ocaml-binarytrees5/ocaml-source.ml new file mode 100644 index 000000000..031bcd38b --- /dev/null +++ b/tests/ocaml-binarytrees5/ocaml-source.ml @@ -0,0 +1,74 @@ +(* Copied from + * https://github.com/ocaml-bench/sandmark + * file benchmarks/multicore-numerical/binarytrees5_multicore.ml + * commit 861f568d869c95bc9aa8fc1fd90a13ab6cbe7afb + *) + +module T = Domainslib.Task + +let num_domains = try int_of_string Sys.argv.(1) with _ -> 1 +let max_depth = try int_of_string Sys.argv.(2) with _ -> 10 +let pool = T.setup_pool ~num_domains:(num_domains - 1) + +type 'a tree = Empty | Node of 'a tree * 'a tree + +let rec make d = +(* if d = 0 then Empty *) + if d = 0 then Node(Empty, Empty) + else let d = d - 1 in Node(make d, make d) + +let rec check t = + Domain.Sync.poll (); + match t with + | Empty -> 0 + | Node(l, r) -> 1 + check l + check r + +let min_depth = 4 +let max_depth = max (min_depth + 2) max_depth +let stretch_depth = max_depth + 1 + +let () = + (* Gc.set { (Gc.get()) with Gc.minor_heap_size = 1024 * 1024; max_overhead = -1; }; *) + let _ = check (make stretch_depth) in + () + (* Printf.printf "stretch tree of depth %i\t check: %i\n" stretch_depth c *) + +let long_lived_tree = make max_depth + +let values = Array.make num_domains 0 + +let calculate d st en ind = + (* Printf.printf "st = %d en = %d\n" st en; *) + let c = ref 0 in + for _ = st to en do + c := !c + check (make d) + done; + (* Printf.printf "ind = %d\n" ind; *) + values.(ind) <- !c + +let loop_depths d = + for i = 0 to ((max_depth - d) / 2 + 1) - 1 do + let d = d + i * 2 in + let niter = 1 lsl (max_depth - d + min_depth) in + let rec loop acc i num_domains = + if i = num_domains then begin + List.rev acc |> List.iter (fun pr -> T.await pool pr) + end else begin + loop + ((T.async pool (fun _ -> + calculate d (i * niter / num_domains) (((i + 1) * niter / num_domains) - 1) i)) :: acc) + (i + 1) + num_domains + end in + + loop [] 0 num_domains; + let _ = Array.fold_left (+) 0 values in + () + (* Printf.printf "%i\t trees of depth %i\t check: %i\n" niter d sum *) + done + +let () = + loop_depths min_depth; + let _ = max_depth in + let _ = (check long_lived_tree) in + T.teardown_pool pool diff --git a/tests/ocaml-game-of-life-pure/main.sml b/tests/ocaml-game-of-life-pure/main.sml new file mode 100644 index 000000000..956aa4c6a --- /dev/null +++ b/tests/ocaml-game-of-life-pure/main.sml @@ -0,0 +1,114 @@ +structure CLA = CommandLineArgs +val n_times = CLA.parseInt "n_times" 2 +val board_size = CLA.parseInt "board_size" 1024 +val _ = print ("n_times " ^ Int.toString n_times ^ "\n") +val _ = print ("board_size " ^ Int.toString board_size ^ "\n") + +fun randBool pos = + Util.hash pos mod 2 + +val bs = board_size + +val g = + PureSeq.tabulate (fn i => PureSeq.tabulate (fn j => randBool (i*bs + j)) bs) bs + +fun get g x y = + PureSeq.nth (PureSeq.nth g x) y + handle _ => 0 + +fun neighbourhood g x y = + (get g (x-1) (y-1)) + + (get g (x-1) (y )) + + (get g (x-1) (y+1)) + + (get g (x ) (y-1)) + + (get g (x ) (y+1)) + + (get g (x+1) (y-1)) + + (get g (x+1) (y )) + + (get g (x+1) (y+1)) + +fun next_cell g x y = + let + val n = neighbourhood g x y + in + (* Why not just write it like this?? + case (Seq.nth (Seq.nth g x) y, n) of + (1, 2) => 1 (* lives *) + | (1, 3) => 1 (* lives *) + | (0, 3) => 1 (* get birth *) + | _ => 0 + *) + + (* I could enable MLton or-patterns, but whatever *) + case (PureSeq.nth (PureSeq.nth g x) y, n) of + (1, 0) => 0 (* lonely *) + | (1, 1) => 0 (* lonely *) + | (1, 4) => 0 (* overcrowded *) + | (1, 5) => 0 (* overcrowded *) + | (1, 6) => 0 (* overcrowded *) + | (1, 7) => 0 (* overcrowded *) + | (1, 8) => 0 (* overcrowded *) + | (1, 2) => 1 (* lives *) + | (1, 3) => 1 (* lives *) + | (0, 3) => 1 (* get birth *) + | _ (* 0, (0|1|2|4|5|6|7|8) *) => 0 (* barren *) + + (* With or-patterns it would look like: + case (Seq.nth (Seq.nth g x) y, n) of + (1, 0) | (1, 1) => 0 (* lonely *) + | (1, 4) | (1, 5) | (1, 6) | (1, 7) | (1, 8) => 0 (* overcrowded *) + | (1, 2) | (1, 3) => 1 (* lives *) + | (0, 3) => 1 (* get birth *) + | _ (* 0, (0|1|2|4|5|6|7|8) *) => 0 (* barren *) + *) + end + +fun loop curr remaining = + if remaining <= 0 then + curr + else + let + (* SAM_NOTE: this is my own granularity control. The ocaml source does + * static partitioning based on num_domains, but this is unnecessary. + * Just choose a static granularity that is reasonable, and then it will + * work decently for any number of processors. *) + val target_granularity = 5000 + val chunk_size = Int.max (1, target_granularity div board_size) + + val next = + PureSeq.tabulateG chunk_size (fn x => + PureSeq.tabulate (fn y => next_cell curr x y) board_size) + board_size + in + loop next (remaining-1) + end + +val msg = "doing " ^ Int.toString n_times ^ " iterations" +val result = Benchmark.run msg (fn _ => loop g n_times) + +(* =========================================================================== + * SAM_NOTE: rest is my stuff. Just outputting the result. + *) + +val output = CLA.parseString "output" "" +val _ = + if output = "" then + print ("use -output XXX.ppm to see result\n") + else + let + val g = result + + fun color 0 = Color.white + | color _ = Color.black + + val image = + { height = board_size + , width = board_size + , data = Seq.tabulate (fn k => + color (get g (k div board_size) (k mod board_size))) + (board_size * board_size) + } + val (_, tm) = Util.getTime (fn _ => PPM.write output image) + in + print ("wrote output in " ^ Time.fmt 4 tm ^ "s\n") + end + diff --git a/tests/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb b/tests/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/ocaml-game-of-life/main.sml b/tests/ocaml-game-of-life/main.sml new file mode 100644 index 000000000..25da1ee9b --- /dev/null +++ b/tests/ocaml-game-of-life/main.sml @@ -0,0 +1,231 @@ +(* ocaml source: + * +## let num_domains = try int_of_string Sys.argv.(1) with _ -> 1 +## let n_times = try int_of_string Sys.argv.(2) with _ -> 2 +## let board_size = try int_of_string Sys.argv.(3) with _ -> 1024 + *) +structure CLA = CommandLineArgs +val n_times = CLA.parseInt "n_times" 2 +val board_size = CLA.parseInt "board_size" 1024 +val _ = print ("n_times " ^ Int.toString n_times ^ "\n") +val _ = print ("board_size " ^ Int.toString board_size ^ "\n") + +(* ocaml source: + * +## module T = Domainslib.Task +## +## let rg = +## ref (Array.init board_size (fun _ -> +## Array.init board_size (fun _ -> Random.int 2))) +## let rg' = +## ref (Array.init board_size (fun _ -> +## Array.init board_size (fun _ -> Random.int 2))) +## let buf = Bytes.create board_size + * + * The buf is not used. + * + * Most obvious adaptation is just nested sequences. We'll use + * a hash function to seed the initial state. + *) + +fun randBool pos = + Util.hash pos mod 2 + +val bs = board_size + +fun vtab f n = Vector.tabulate (n, f) +fun vnth v i = Vector.sub (v, i) + +fun atab f n = Array.tabulate (n, f) +fun anth a i = Array.sub (a, i) + +val rg = + (*ref*) (vtab (fn i => atab (fn j => randBool (i*bs + j)) bs) bs) +val rg' = + (*ref*) (vtab (fn i => atab (fn j => randBool (bs*bs + i*bs + j)) bs) bs) + +(* ocaml source: + * +## let get g x y = +## try g.(x).(y) +## with _ -> 0 +## +## let neighbourhood g x y = +## (get g (x-1) (y-1)) + +## (get g (x-1) (y )) + +## (get g (x-1) (y+1)) + +## (get g (x ) (y-1)) + +## (get g (x ) (y+1)) + +## (get g (x+1) (y-1)) + +## (get g (x+1) (y )) + +## (get g (x+1) (y+1)) +## +## let next_cell g x y = +## let n = neighbourhood g x y in +## match g.(x).(y), n with +## | 1, 0 | 1, 1 -> 0 (* lonely *) +## | 1, 4 | 1, 5 | 1, 6 | 1, 7 | 1, 8 -> 0 (* overcrowded *) +## | 1, 2 | 1, 3 -> 1 (* lives *) +## | 0, 3 -> 1 (* get birth *) +## | _ (* 0, (0|1|2|4|5|6|7|8) *) -> 0 (* barren *) + *) + +fun get g x y = + anth (vnth g x) y + handle _ => 0 + +(* fun neighbourhood g x y = + (get g (x-1) (y-1)) + + (get g (x-1) (y )) + + (get g (x-1) (y+1)) + + (get g (x ) (y-1)) + + (get g (x ) (y+1)) + + (get g (x+1) (y-1)) + + (get g (x+1) (y )) + + (get g (x+1) (y+1)) *) + +fun neighbourhood g x y = + let + fun get_element s y = + anth s y + handle _ => 0 + fun sum_row(x, y) = + let + val gx = vnth g x + in + (get_element gx (y-1)) + (get_element gx y) + (get_element gx (y+1)) + end + handle _ => 0 + in + sum_row(x-1, y) + sum_row(x, y) + sum_row(x + 1, y) + end + +fun next_cell g x y = + let + val n = neighbourhood g x y + in + (* Why not just write it like this?? + case (Seq.nth (Seq.nth g x) y, n) of + (1, 2) => 1 (* lives *) + | (1, 3) => 1 (* lives *) + | (0, 3) => 1 (* get birth *) + | _ => 0 + *) + + (* I could enable MLton or-patterns, but whatever *) + case (anth (vnth g x) y, n) of + (1, 0) => 0 (* lonely *) + | (1, 1) => 0 (* lonely *) + | (1, 4) => 0 (* overcrowded *) + | (1, 5) => 0 (* overcrowded *) + | (1, 6) => 0 (* overcrowded *) + | (1, 7) => 0 (* overcrowded *) + | (1, 8) => 0 (* overcrowded *) + | (1, 2) => 1 (* lives *) + | (1, 3) => 1 (* lives *) + | (0, 3) => 1 (* get birth *) + | _ (* 0, (0|1|2|4|5|6|7|8) *) => 0 (* barren *) + + (* With or-patterns it would look like: + case (Seq.nth (Seq.nth g x) y, n) of + (1, 0) | (1, 1) => 0 (* lonely *) + | (1, 4) | (1, 5) | (1, 6) | (1, 7) | (1, 8) => 0 (* overcrowded *) + | (1, 2) | (1, 3) => 1 (* lives *) + | (0, 3) => 1 (* get birth *) + | _ (* 0, (0|1|2|4|5|6|7|8) *) => 0 (* barren *) + *) + end + +(* ocaml source: + * +## (* let print g = +## for x = 0 to board_size - 1 do +## for y = 0 to board_size - 1 do +## if g.(x).(y) = 0 +## then Bytes.set buf y '.' +## else Bytes.set buf y 'o' +## done; +## print_endline (Bytes.unsafe_to_string buf) +## done; +## print_endline "" *) +## +## let next pool = +## let g = !rg in +## let new_g = !rg' in +## T.parallel_for pool ~chunk_size:(board_size/num_domains) ~start:0 +## ~finish:(board_size - 1) ~body:(fun x -> +## for y = 0 to board_size - 1 do +## new_g.(x).(y) <- next_cell g x y +## done); +## rg := new_g; +## rg' := g +## +## +## let rec repeat pool n = +## match n with +## | 0-> () +## | _-> next pool; repeat pool (n-1) +## +## let ()= +## let pool = T.setup_pool ~num_domains:(num_domains - 1) in +## (* print !rg; *) +## repeat pool n_times; +## (* print !rg; *) +## T.teardown_pool pool + *) + +fun next (g, new_g) = + let + (* val g = !rg + val new_g = !rg' *) + + (* SAM_NOTE: this is my own granularity control. The ocaml source does + * static partitioning based on num_domains, but this is unnecessary. + * Just choose a static granularity that is reasonable, and then it will + * work decently for any number of processors. *) + val target_granularity = 10000 + val chunk_size = Int.max (1, target_granularity div board_size) + in + ForkJoin.parfor chunk_size (0, board_size) (fn x => + Util.for (0, board_size) (fn y => + Array.update (vnth new_g x, y, next_cell g x y))); + + (new_g, g) + end + +fun repeat state n = + case n of + 0 => state + | _ => repeat (next state) (n-1) + +val msg = "doing " ^ Int.toString n_times ^ " iterations" +val (result, _) = Benchmark.run msg (fn _ => repeat (rg, rg') n_times) + +(* =========================================================================== + * SAM_NOTE: rest is my stuff. Just outputting the result. + *) + +val output = CLA.parseString "output" "" +val _ = + if output = "" then + print ("use -output XXX to see result\n") + else + let + (* val g = !rg *) + val g = result + + fun color 0 = Color.white + | color _ = Color.black + + val image = + { height = board_size + , width = board_size + , data = Seq.tabulate (fn k => + color (get g (k div board_size) (k mod board_size))) + (board_size * board_size) + } + val (_, tm) = Util.getTime (fn _ => PPM.write output image) + in + print ("wrote output in " ^ Time.fmt 4 tm ^ "s\n") + end + diff --git a/tests/ocaml-game-of-life/ocaml-game-of-life.mlb b/tests/ocaml-game-of-life/ocaml-game-of-life.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/ocaml-game-of-life/ocaml-game-of-life.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/ocaml-game-of-life/ocaml-source.ml b/tests/ocaml-game-of-life/ocaml-source.ml new file mode 100644 index 000000000..3c9df39d9 --- /dev/null +++ b/tests/ocaml-game-of-life/ocaml-source.ml @@ -0,0 +1,75 @@ +(* Copied from + * https://github.com/ocaml-bench/sandmark + * directory benchmarks/multicore-numerical/game_of_life_multicore.ml + * commit 23a73b28c803763cfb7c8282d2a847261cb7d4a9 + *) + +let num_domains = try int_of_string Sys.argv.(1) with _ -> 1 +let n_times = try int_of_string Sys.argv.(2) with _ -> 2 +let board_size = try int_of_string Sys.argv.(3) with _ -> 1024 + +module T = Domainslib.Task + +let rg = + ref (Array.init board_size (fun _ -> Array.init board_size (fun _ -> Random.int 2))) +let rg' = + ref (Array.init board_size (fun _ -> Array.init board_size (fun _ -> Random.int 2))) +let buf = Bytes.create board_size + +let get g x y = + try g.(x).(y) + with _ -> 0 + +let neighbourhood g x y = + (get g (x-1) (y-1)) + + (get g (x-1) (y )) + + (get g (x-1) (y+1)) + + (get g (x ) (y-1)) + + (get g (x ) (y+1)) + + (get g (x+1) (y-1)) + + (get g (x+1) (y )) + + (get g (x+1) (y+1)) + +let next_cell g x y = + let n = neighbourhood g x y in + match g.(x).(y), n with + | 1, 0 | 1, 1 -> 0 (* lonely *) + | 1, 4 | 1, 5 | 1, 6 | 1, 7 | 1, 8 -> 0 (* overcrowded *) + | 1, 2 | 1, 3 -> 1 (* lives *) + | 0, 3 -> 1 (* get birth *) + | _ (* 0, (0|1|2|4|5|6|7|8) *) -> 0 (* barren *) + +(* let print g = + for x = 0 to board_size - 1 do + for y = 0 to board_size - 1 do + if g.(x).(y) = 0 + then Bytes.set buf y '.' + else Bytes.set buf y 'o' + done; + print_endline (Bytes.unsafe_to_string buf) + done; + print_endline "" *) + +let next pool = + let g = !rg in + let new_g = !rg' in + T.parallel_for pool ~chunk_size:(board_size/num_domains) ~start:0 + ~finish:(board_size - 1) ~body:(fun x -> + for y = 0 to board_size - 1 do + new_g.(x).(y) <- next_cell g x y + done); + rg := new_g; + rg' := g + + +let rec repeat pool n = + match n with + | 0-> () + | _-> next pool; repeat pool (n-1) + +let ()= + let pool = T.setup_pool ~num_domains:(num_domains - 1) in + (* print !rg; *) + repeat pool n_times; + (* print !rg; *) + T.teardown_pool pool diff --git a/tests/ocaml-lu-decomp/main.sml b/tests/ocaml-lu-decomp/main.sml new file mode 100644 index 000000000..9f2af65d0 --- /dev/null +++ b/tests/ocaml-lu-decomp/main.sml @@ -0,0 +1,212 @@ +(* ocaml source: + * +## module T = Domainslib.Task +## let num_domains = try int_of_string Sys.argv.(1) with _ -> 1 +## let mat_size = try int_of_string Sys.argv.(2) with _ -> 1200 +## let chunk_size = try int_of_string Sys.argv.(3) with _ -> 16 + *) + +structure CLA = CommandLineArgs +val mat_size = CLA.parseInt "mat_size" 1200 +val chunk_size = CLA.parseInt "chunk_size" 16 +val _ = print ("mat_size " ^ Int.toString mat_size ^ "\n") +val _ = print ("chunk_size " ^ Int.toString chunk_size ^ "\n") + +(* ocaml source: + * +## +## module SquareMatrix = struct +## +## let create f : float array = +## let fa = Array.create_float (mat_size * mat_size) in +## for i = 0 to mat_size * mat_size - 1 do +## fa.(i) <- f (i / mat_size) (i mod mat_size) +## done; +## fa +## let parallel_create pool f : float array = +## let fa = Array.create_float (mat_size * mat_size) in +## T.parallel_for pool ~chunk_size:(mat_size * mat_size / num_domains) ~start:0 +## ~finish:( mat_size * mat_size - 1) ~body:(fun i -> +## fa.(i) <- f (i / mat_size) (i mod mat_size)); +## fa +## +## let get (m : float array) r c = m.(r * mat_size + c) +## let set (m : float array) r c v = m.(r * mat_size + c) <- v +## let parallel_copy pool a = +## let n = Array.length a in +## let copy_part a b i = +## let s = (i * n / num_domains) in +## let e = (i+1) * n / num_domains - 1 in +## Array.blit a s b s (e - s + 1) in +## let b = Array.create_float n in +## let rec aux acc num_domains i = +## if (i = num_domains) then +## (List.iter (fun e -> T.await pool e) acc) +## else begin +## aux ((T.async pool (fun _ -> copy_part a b i))::acc) num_domains (i+1) +## end +## in +## aux [] num_domains 0; +## b +## end +*) + +structure SquareMatrix = +struct + fun create f: real array = + let + val fa = ForkJoin.alloc (mat_size * mat_size) + in + Util.for (0, mat_size * mat_size) (fn i => + Array.update (fa, i, f (i div mat_size, i mod mat_size))); + fa + end + + fun parallel_create f: real array = + let + val fa = ForkJoin.alloc (mat_size * mat_size) + in + ForkJoin.parfor 10000 (0, mat_size * mat_size) (fn i => + Array.update (fa, i, f (i div mat_size, i mod mat_size))); + fa + end + + fun get m r c = Array.sub (m, r * mat_size + c) + fun set m r c v = Array.update (m, r * mat_size + c, v) + + (* SAM_NOTE: This function is a bit overengineered in the ocaml source in + * a way that is probably negatively impacting performance. The easiest way + * to do it would just be this: + * + * fun parallel_copy a = + * let + * val n = Array.length a + * val b = allocate n + * in + * parfor GRAIN (0, n) (fn i => Array.update (b, i, Array.sub (a, i))); + * b + * end + * + * But, rather than implement it like this, I'll try to be faithful to the + * original ocaml code here. + * + * The ocaml code for this function uses futures (async/await), which MPL + * doesn't support. But using futures is unnecessary, because the ocaml code + * just does uses them in fork-join style anyway! Also, the ocaml code + * relies upon knowing the number of processors to do granularity control, + * but this is unnecessary. Instead we can choose a static GRAIN and then + * divide the array into ceil(n/GRAIN) parts. + * + * The ocaml source seems like it has a bug, if n is not divisible + * by num_domains. Easy fix is below, when calculating the end of the + * part (variable e below). + *) + fun parallel_copy a = + let + val n = Array.length a + val GRAIN = 10000 (* same role as n/num_domains in ocaml source *) + fun copy_part a b i = + let + val s = i * GRAIN + val e = Int.min (n, (i+1) * GRAIN) (* fixed bug! take min. *) + in + Util.for (s, e) (fn j => Array.update (b, j, Array.sub (a, j))) + end + val num_parts = Util.ceilDiv n GRAIN + val b = ForkJoin.alloc n + in + ForkJoin.parfor 1 (0, num_parts) (copy_part a b); + b + end +end + +(* ocaml source: + * +## +## open SquareMatrix +## +## let lup pool (a0 : float array) = +## let a = parallel_copy pool a0 in +## for k = 0 to (mat_size - 2) do +## T.parallel_for pool ~chunk_size:chunk_size ~start:(k + 1) ~finish:(mat_size -1) +## ~body:(fun row -> +## let factor = get a row k /. get a k k in +## for col = k + 1 to mat_size-1 do +## set a row col (get a row col -. factor *. (get a k col)) +## done; +## set a row k factor ) +## done ; +## a + *) + +open SquareMatrix + +fun lup a0 = + let + val a = parallel_copy a0 + in + Util.for (0, mat_size-1) (fn k => + ForkJoin.parfor chunk_size (k+1, mat_size) (fn row => + let + val factor = get a row k / get a k k + in + Util.for (k+1, mat_size) (fn col => + set a row col (get a row col - factor * (get a k col))); + set a row k factor + end)); + a + end + +(* ocaml source: + * +## let () = +## let pool = T.setup_pool ~num_domains:(num_domains - 1) in +## let a = create (fun _ _ -> (Random.float 100.0) +. 1.0 ) in +## let lu = lup pool a in +## let _l = parallel_create pool (fun i j -> if i > j then get lu i j else if i = j then 1.0 else 0.0) in +## let _u = parallel_create pool (fun i j -> if i <= j then get lu i j else 0.0) in +## T.teardown_pool pool + *) + +(* SAM_NOTE: It seems like the ocaml source chose to initialize sequentially + * because of the stateful RNG. We'll do a PRNG based on a hash function, to + * be safe for parallelism, and initialize in parallel. + *) +(* +val rand = Random.rand (15, 210) (* seed the generator *) +fun randReal bound = + bound * Random.randReal rand +*) + +fun randReal bound seed = + bound * (Real.fromInt (Util.hash seed mod 1000001) / 1000000.0) + +val (a, tm) = Util.getTime (fn _ => + create (fn (i, j) => 1.0 + randReal 100.0 (i * mat_size + j))) +val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +fun ludEx () = + let + val (lu, tm) = Util.getTime (fn _ => lup a) + val _ = print ("main decomposition in " ^ Time.fmt 4 tm ^ "s\n") + + val ((l, u), tm) = Util.getTime (fn _ => + let + val l = + parallel_create (fn (i, j) => + if i > j then get lu i j + else if i = j then 1.0 + else 0.0) + val u = + parallel_create (fn (i, j) => + if i <= j then get lu i j else 0.0) + in + (l, u) + end) + val _ = print ("extracted L and U in " ^ Time.fmt 4 tm ^ "s\n") + in + (l, u) + end + +val _ = Benchmark.run "running LU decomposition" ludEx + diff --git a/tests/ocaml-lu-decomp/ocaml-lu-decomp.mlb b/tests/ocaml-lu-decomp/ocaml-lu-decomp.mlb new file mode 100644 index 000000000..d0c4aed76 --- /dev/null +++ b/tests/ocaml-lu-decomp/ocaml-lu-decomp.mlb @@ -0,0 +1,7 @@ +../mpllib/sources.$(COMPAT).mlb +(*local + $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb +in + structure Random +end*) +main.sml diff --git a/tests/ocaml-lu-decomp/ocaml-source.ml b/tests/ocaml-lu-decomp/ocaml-source.ml new file mode 100644 index 000000000..d5bda24cc --- /dev/null +++ b/tests/ocaml-lu-decomp/ocaml-source.ml @@ -0,0 +1,62 @@ +module T = Domainslib.Task +let num_domains = try int_of_string Sys.argv.(1) with _ -> 1 +let mat_size = try int_of_string Sys.argv.(2) with _ -> 1200 +let chunk_size = try int_of_string Sys.argv.(3) with _ -> 16 + +module SquareMatrix = struct + + let create f : float array = + let fa = Array.create_float (mat_size * mat_size) in + for i = 0 to mat_size * mat_size - 1 do + fa.(i) <- f (i / mat_size) (i mod mat_size) + done; + fa + let parallel_create pool f : float array = + let fa = Array.create_float (mat_size * mat_size) in + T.parallel_for pool ~chunk_size:(mat_size * mat_size / num_domains) ~start:0 + ~finish:( mat_size * mat_size - 1) ~body:(fun i -> + fa.(i) <- f (i / mat_size) (i mod mat_size)); + fa + + let get (m : float array) r c = m.(r * mat_size + c) + let set (m : float array) r c v = m.(r * mat_size + c) <- v + let parallel_copy pool a = + let n = Array.length a in + let copy_part a b i = + let s = (i * n / num_domains) in + let e = (i+1) * n / num_domains - 1 in + Array.blit a s b s (e - s + 1) in + let b = Array.create_float n in + let rec aux acc num_domains i = + if (i = num_domains) then + (List.iter (fun e -> T.await pool e) acc) + else begin + aux ((T.async pool (fun _ -> copy_part a b i))::acc) num_domains (i+1) + end + in + aux [] num_domains 0; + b +end + +open SquareMatrix + +let lup pool (a0 : float array) = + let a = parallel_copy pool a0 in + for k = 0 to (mat_size - 2) do + T.parallel_for pool ~chunk_size:chunk_size ~start:(k + 1) ~finish:(mat_size -1) + ~body:(fun row -> + let factor = get a row k /. get a k k in + for col = k + 1 to mat_size-1 do + set a row col (get a row col -. factor *. (get a k col)) + done; + set a row k factor ) + done ; + a + +let () = + let pool = T.setup_pool ~num_domains:(num_domains - 1) in + let a = create (fun _ _ -> (Random.float 100.0) +. 1.0 ) in + let lu = lup pool a in + let _l = parallel_create pool (fun i j -> if i > j then get lu i j else if i = j then 1.0 else 0.0) in + let _u = parallel_create pool (fun i j -> if i <= j then get lu i j else 0.0) in + T.teardown_pool pool diff --git a/tests/ocaml-mandelbrot/main.sml b/tests/ocaml-mandelbrot/main.sml new file mode 100644 index 000000000..87d14732b --- /dev/null +++ b/tests/ocaml-mandelbrot/main.sml @@ -0,0 +1,187 @@ +structure CLA = CommandLineArgs + +(* ocaml source: + * +## let niter = 50 +## let limit = 4. +## +## let num_domains = int_of_string (Array.get Sys.argv 1) +## let w = int_of_string (Array.get Sys.argv 2) + * + *) + +val niter = 50 +val limit = 4.0 + +val w = CLA.parseInt "w" 16000 +val _ = print ("w " ^ Int.toString w ^ "\n") + +(* ocaml source: + * +## +## let worker w h_lo h_hi = +## let buf = +## Bytes.create ((w / 8 + (if w mod 8 > 0 then 1 else 0)) * (h_hi - h_lo)) +## and ptr = ref 0 in +## let fw = float w /. 2. in +## let fh = fw in +## let red_w = w - 1 and red_h_hi = h_hi - 1 and byte = ref 0 in +## for y = h_lo to red_h_hi do +## let ci = float y /. fh -. 1. in +## for x = 0 to red_w do +## let cr = float x /. fw -. 1.5 +## and zr = ref 0. and zi = ref 0. and trmti = ref 0. and n = ref 0 in +## begin try +## while true do +## Domain.Sync.poll (); +## zi := 2. *. !zr *. !zi +. ci; +## zr := !trmti +. cr; +## let tr = !zr *. !zr and ti = !zi *. !zi in +## if tr +. ti > limit then begin +## byte := !byte lsl 1; +## raise Exit +## end else if incr n; !n = niter then begin +## byte := (!byte lsl 1) lor 0x01; +## raise Exit +## end else +## trmti := tr -. ti +## done +## with Exit -> () +## end; +## if x mod 8 = 7 then begin +## Bytes.set buf !ptr (Char.chr !byte); +## incr ptr; +## byte := 0 +## end +## done; +## let rem = w mod 8 in +## if rem != 0 then begin +## Bytes.set buf !ptr (Char.chr (!byte lsl (8 - rem))); +## incr ptr; +## byte := 0 +## end +## done; +## buf + * + *) + +fun incr x = (x := !x + 1) + +fun worker w h_lo h_hi = + let + val buf = ForkJoin.alloc ((w div 8 + (if w mod 8 > 0 then 1 else 0)) * (h_hi - h_lo)) + val ptr = ref 0 + val fw = Real.fromInt w / 2.0 + val fh = fw + val byte = ref 0w0 + in + Util.for (h_lo, h_hi) (fn y => + let + val ci = Real.fromInt y / fh - 1.0 + in + (* print ("y=" ^ Int.toString y ^ "\n"); *) + Util.for (0, w) (fn x => + let + val cr = Real.fromInt x / fw - 1.5 + val zr = ref 0.0 + val zi = ref 0.0 + val trmti = ref 0.0 + val n = ref 0 + + fun loop () = + ( zi := 2.0 * !zr * !zi + ci + ; zr := !trmti + cr + ; let + val tr = !zr * !zr + val ti = !zi * !zi + in + if tr + ti > limit then + (byte := Word.<< (!byte, 0w1)) + else if (incr n; !n = niter) then + (byte := Word.orb (Word.<< (!byte, 0w1), 0wx01)) + else + (trmti := tr - ti; loop ()) + end + ) + in + (* print ("x=" ^ Int.toString x ^ "\n"); *) + loop (); + if x mod 8 = 7 then + ( Array.update (buf, !ptr, Char.chr (Word.toInt (!byte))) + ; incr ptr + ; byte := 0w0 + ) + else () + end); + + let + val rem = w mod 8 + in + if rem <> 0 then + ( Array.update (buf, !ptr, + Char.chr (Word.toInt (Word.<< (!byte, Word.fromInt (8 - rem))))) + ; incr ptr + ; byte := 0w0 + ) + else () + end + + end); + + buf + end + +(* ocaml source: + * +## let _ = +## let pool = T.setup_pool ~num_domains:(num_domains - 1) in +## let rows = w / num_domains and rem = w mod num_domains in +## Printf.printf "P4\n%i %i\n%!" w w; +## let work i () = +## worker w (i * rows + min i rem) ((i+1) * rows + min (i+1) rem) +## in +## let doms = +## Array.init (num_domains - 1) (fun i -> T.async pool (work i)) in +## let r = work (num_domains-1) () in +## Array.iter (fun d -> Printf.printf "%a%!" output_bytes (T.await pool d)) doms; +## Printf.printf "%a%!" output_bytes r; +## T.teardown_pool pool + *) + +(* GRAIN is the target work for one worker (in terms of number of pixels); + * this should be big enough to amortize the cost of parallelism. To match up + * with the original code, pick the maximum "number of domains" so that each + * domain has approximately at least GRAIN work to do (but cap this at some + * reasonably large number...) + *) +val GRAIN = 1000 +val num_domains = Int.min (500, Int.min (w, Util.ceilDiv (w * w) GRAIN)) + +val rows = w div num_domains +val rem = w mod num_domains + +fun work i = + worker w (i * rows + Int.min (i, rem)) ((i+1) * rows + Int.min (i+1, rem)) + +val results = + Benchmark.run "running mandelbrot" (fn _ => + SeqBasis.tabulate 1 (0, num_domains) work) + +val outfile = CLA.parseString "output" "" + +val _ = + if outfile = "" then + print ("use -output XXX to see result\n") + else + let + val file = TextIO.openOut outfile + fun dump1 c = TextIO.output1 (file, c) + fun dump str = TextIO.output (file, str) + in + ( dump "P4\n" + ; dump (Int.toString w ^ " " ^ Int.toString w ^ "\n") + ; Array.app (Array.app dump1) results + ; TextIO.closeOut file + ) + end + diff --git a/tests/ocaml-mandelbrot/ocaml-mandelbrot.mlb b/tests/ocaml-mandelbrot/ocaml-mandelbrot.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/ocaml-mandelbrot/ocaml-mandelbrot.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/ocaml-mandelbrot/ocaml-source.ml b/tests/ocaml-mandelbrot/ocaml-source.ml new file mode 100644 index 000000000..52536421e --- /dev/null +++ b/tests/ocaml-mandelbrot/ocaml-source.ml @@ -0,0 +1,80 @@ +(* + * The Computer Language Benchmarks Game + * https://salsa.debian.org/benchmarksgame-team/benchmarksgame/ + * + * Contributed by Paolo Ribeca + * + * (Very loosely based on previous version Ocaml #3, + * which had been contributed by + * Christophe TROESTLER + * and enhanced by + * Christian Szegedy and Yaron Minsky) + * + * fix compile errors by using Bytes instead of String, by Tony Tavener + *) + +module T = Domainslib.Task + +let niter = 50 +let limit = 4. + +let num_domains = int_of_string (Array.get Sys.argv 1) +let w = int_of_string (Array.get Sys.argv 2) + +let worker w h_lo h_hi = + let buf = + Bytes.create ((w / 8 + (if w mod 8 > 0 then 1 else 0)) * (h_hi - h_lo)) + and ptr = ref 0 in + let fw = float w /. 2. in + let fh = fw in + let red_w = w - 1 and red_h_hi = h_hi - 1 and byte = ref 0 in + for y = h_lo to red_h_hi do + let ci = float y /. fh -. 1. in + for x = 0 to red_w do + let cr = float x /. fw -. 1.5 + and zr = ref 0. and zi = ref 0. and trmti = ref 0. and n = ref 0 in + begin try + while true do + Domain.Sync.poll (); + zi := 2. *. !zr *. !zi +. ci; + zr := !trmti +. cr; + let tr = !zr *. !zr and ti = !zi *. !zi in + if tr +. ti > limit then begin + byte := !byte lsl 1; + raise Exit + end else if incr n; !n = niter then begin + byte := (!byte lsl 1) lor 0x01; + raise Exit + end else + trmti := tr -. ti + done + with Exit -> () + end; + if x mod 8 = 7 then begin + Bytes.set buf !ptr (Char.chr !byte); + incr ptr; + byte := 0 + end + done; + let rem = w mod 8 in + if rem != 0 then begin + Bytes.set buf !ptr (Char.chr (!byte lsl (8 - rem))); + incr ptr; + byte := 0 + end + done; + buf + +let _ = + let pool = T.setup_pool ~num_domains:(num_domains - 1) in + let rows = w / num_domains and rem = w mod num_domains in + Printf.printf "P4\n%i %i\n%!" w w; + let work i () = + worker w (i * rows + min i rem) ((i+1) * rows + min (i+1) rem) + in + let doms = + Array.init (num_domains - 1) (fun i -> T.async pool (work i)) in + let r = work (num_domains-1) () in + Array.iter (fun d -> Printf.printf "%a%!" output_bytes (T.await pool d)) doms; + Printf.printf "%a%!" output_bytes r; + T.teardown_pool pool diff --git a/tests/ocaml-nbody-imm/README b/tests/ocaml-nbody-imm/README new file mode 100644 index 000000000..029bf5032 --- /dev/null +++ b/tests/ocaml-nbody-imm/README @@ -0,0 +1,3 @@ +Difference from ocaml-nbody: + - switch from mutable fields in planet to fully immutable record + - switch from imperative main loop to purely functional diff --git a/tests/ocaml-nbody-imm/main.sml b/tests/ocaml-nbody-imm/main.sml new file mode 100644 index 000000000..b8ac55e4f --- /dev/null +++ b/tests/ocaml-nbody-imm/main.sml @@ -0,0 +1,148 @@ +structure CLA = CommandLineArgs + +val num_domains = Concurrency.numberOfProcessors + +val n = CLA.parseInt "n" 500 +val num_bodies = CLA.parseInt "num_bodies" 1024 +val gran = CLA.parseInt "gran" 20 + +val pi = 3.141592653589793 +val solar_mass = 4.0 * pi * pi +val days_per_year = 365.24 + +type planet = + { x: real, y: real, z: real + , vx: real, vy: real, vz: real + , mass: real + } + +fun advance bodies dt = + let + fun velocity i = + let + val b = Array.sub (bodies, i) + val (vx, vy, vz) = (#vx b, #vy b, #vz b) + in + Util.loop (0, Array.length bodies) (vx, vy, vz) (fn ((vx, vy, vz), j) => + let + val b' = Array.sub (bodies, j) + in + if i <> j then + let + val dx = #x b - #x b' + val dy = #y b - #y b' + val dz = #z b - #z b' + val dist2 = dx * dx + dy * dy + dz * dz + val mag = dt / (dist2 * Math.sqrt(dist2)) + in + ( vx - dx * #mass b' * mag + , vy - dy * #mass b' * mag + , vz - dz * #mass b' * mag + ) + end + else (vx, vy, vz) + end) + end + + val velocities = PureSeq.tabulateG gran velocity num_bodies + in + Util.for (0, num_bodies) (fn i => + let + val b = Array.sub (bodies, i) + val (vx, vy, vz) = PureSeq.nth velocities i + in + Array.update (bodies, i, + { x = #x b + dt * vx + , y = #y b + dt * vy + , z = #z b + dt * vz + , vx = vx + , vy = vy + , vz = vz + , mass = #mass b + }) + end) + end + + +fun energy bodies = + let + in + SeqBasis.reduce 1 op+ 0.0 (0, Array.length bodies) (fn i => + let + val b = Array.sub (bodies, i) + val e = ref 0.0 + in + e := !e + 0.5 * #mass b * + (#vx b * #vx b + #vy b * #vy b + #vz b * #vz b); + + Util.for (i+1, Array.length bodies) (fn j => + let + val b' = Array.sub (bodies, j) + val dx = #x b - #x b' + val dy = #y b - #y b' + val dz = #z b - #z b' + val distance = Math.sqrt (dx * dx + dy * dy + dz * dz) + in + e := !e - (#mass b * #mass b') / distance + end); + + !e + end) + end + +fun offset_momentum bodies = + let + val px = ref 0.0 + val py = ref 0.0 + val pz = ref 0.0 + val b0 = Array.sub (bodies, 0) + in + Util.for (0, Array.length bodies) (fn i => + let + val b = Array.sub (bodies, i) + in + px := !px + #vx b * #mass b; + py := !py + #vy b * #mass b; + pz := !pz + #vz b * #mass b + end); + Array.update (bodies, 0, + { x = #x b0 + , y = #y b0 + , z = #z b0 + , vx = ~ (!px) / solar_mass + , vy = ~ (!py) / solar_mass + , vz = ~ (!pz) / solar_mass + , mass = #mass b0 + }) + end + +val seed = Random.rand (42, 15210) +fun randFloat _ bound = + bound * (Random.randReal seed) + +(* fun randFloat seed bound = + bound * (Real.fromInt (Util.hash seed mod 1000000000) / 1000000000.0) *) + +val bodies = + Array.tabulate (num_bodies, fn i => + let + val seed = 7*i + in + { x = randFloat seed 10.0 + , y = randFloat (seed+1) 10.0 + , z = randFloat (seed+2) 10.0 + , vx = randFloat (seed+3) 5.0 * days_per_year + , vy = randFloat (seed+4) 4.0 * days_per_year + , vz = randFloat (seed+5) 5.0 * days_per_year + , mass = randFloat (seed+6) 10.0 * solar_mass + } + end) + +val _ = offset_momentum bodies +val _ = print ("initial energy: " ^ Real.toString (energy bodies) ^ "\n") + +val _ = Benchmark.run "running simulation" (fn _ => + Util.for (0, n) (fn _ => advance bodies 0.01)) + +val _ = print ("final energy: " ^ Real.toString (energy bodies) ^ "\n") + diff --git a/tests/ocaml-nbody-imm/ocaml-nbody-imm.mlb b/tests/ocaml-nbody-imm/ocaml-nbody-imm.mlb new file mode 100644 index 000000000..c7b6ed5b0 --- /dev/null +++ b/tests/ocaml-nbody-imm/ocaml-nbody-imm.mlb @@ -0,0 +1,7 @@ +../mpllib/sources.$(COMPAT).mlb +local + $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb +in + structure Random +end +main.sml diff --git a/tests/ocaml-nbody-packed/README b/tests/ocaml-nbody-packed/README new file mode 100644 index 000000000..9e16289f4 --- /dev/null +++ b/tests/ocaml-nbody-packed/README @@ -0,0 +1,5 @@ +difference from ocaml-nbody: + - explicit packing of planet type into a single array of floats + (this guarantees that flattening occurred, but after measuring + no performance improvement suggests that the other implementations + do actually manage to flatten.) diff --git a/tests/ocaml-nbody-packed/main.sml b/tests/ocaml-nbody-packed/main.sml new file mode 100644 index 000000000..0d7b08425 --- /dev/null +++ b/tests/ocaml-nbody-packed/main.sml @@ -0,0 +1,156 @@ +structure CLA = CommandLineArgs + +val _ = print ("hello\n") + +val num_domains = Concurrency.numberOfProcessors + +val n = CLA.parseInt "n" 500 +val num_bodies = CLA.parseInt "num_bodies" 1024 + +val pi = 3.141592653589793 +val solar_mass = 4.0 * pi * pi +val days_per_year = 365.24 + +type planet = + { x: real ref, y: real ref, z: real ref + , vx: real ref, vy: real ref, vz: real ref + , mass: real + } + +(** manually packed. size = 7 * num bodies *) +type bodies = real array +fun get_x bodies i = Array.sub (bodies, 7*i) +fun get_y bodies i = Array.sub (bodies, 7*i + 1) +fun get_z bodies i = Array.sub (bodies, 7*i + 2) +fun get_vx bodies i = Array.sub (bodies, 7*i + 3) +fun get_vy bodies i = Array.sub (bodies, 7*i + 4) +fun get_vz bodies i = Array.sub (bodies, 7*i + 5) +fun get_mass bodies i = Array.sub (bodies, 7*i + 6) +fun set_x bodies i x = Array.update (bodies, 7*i, x) +fun set_y bodies i x = Array.update (bodies, 7*i + 1, x) +fun set_z bodies i x = Array.update (bodies, 7*i + 2, x) +fun set_vx bodies i x = Array.update (bodies, 7*i + 3, x) +fun set_vy bodies i x = Array.update (bodies, 7*i + 4, x) +fun set_vz bodies i x = Array.update (bodies, 7*i + 5, x) +fun set_mass bodies i x = Array.update (bodies, 7*i + 6, x) + + +fun advance bodies dt = + let + in + ForkJoin.parfor 1 (0, num_bodies) (fn i => + let + val (vx, vy, vz) = (get_vx bodies i, get_vy bodies i, get_vz bodies i) + val (vx, vy, vz) = + Util.loop (0, num_bodies) (vx, vy, vz) (fn ((vx, vy, vz), j) => + let + (* val b' = Array.sub (bodies, j) *) + in + if i <> j then + let + val dx = get_x bodies i - get_x bodies j + val dy = get_y bodies i - get_y bodies j + val dz = get_z bodies i - get_z bodies j + val dist2 = dx * dx + dy * dy + dz * dz + val mag = dt / (dist2 * Math.sqrt(dist2)) + in + ( vx - dx * get_mass bodies j * mag + , vy - dy * get_mass bodies j * mag + , vz - dz * get_mass bodies j * mag + ) + end + else (vx, vy, vz) + end); + in + set_vx bodies i vx; + set_vy bodies i vy; + set_vz bodies i vz + end); + + Util.for (0, num_bodies) (fn i => + let + (* val b = Array.sub (bodies, i) *) + in + set_x bodies i (get_x bodies i + dt * get_vx bodies i); + set_y bodies i (get_y bodies i + dt * get_vy bodies i); + set_z bodies i (get_z bodies i + dt * get_vz bodies i) + end) + end + +fun energy bodies = + let + in + SeqBasis.reduce 1 op+ 0.0 (0, num_bodies) (fn i => + let + (* val b = Array.sub (bodies, i) *) + val e = ref 0.0 + in + e := !e + 0.5 * get_mass bodies i * + (get_vx bodies i * get_vx bodies i + + get_vy bodies i * get_vy bodies i + + get_vz bodies i * get_vz bodies i); + + Util.for (i+1, num_bodies) (fn j => + let + (* val b' = Array.sub (bodies, j) *) + val dx = get_x bodies i - get_x bodies j + val dy = get_y bodies i - get_y bodies j + val dz = get_z bodies i - get_z bodies j + val distance = Math.sqrt (dx * dx + dy * dy + dz * dz) + in + e := !e - (get_mass bodies i * get_mass bodies j) / distance + end); + + !e + end) + end + +fun offset_momentum bodies = + let + val px = ref 0.0 + val py = ref 0.0 + val pz = ref 0.0 + in + Util.for (0, num_bodies) (fn i => + let + (* val b = Array.sub (bodies, i) *) + in + px := !px + get_vx bodies i * get_mass bodies i; + py := !py + get_vy bodies i * get_mass bodies i; + pz := !pz + get_vz bodies i * get_mass bodies i + end); + set_vx bodies 0 (~ (!px) / solar_mass); + set_vy bodies 0 (~ (!py) / solar_mass); + set_vz bodies 0 (~ (!pz) / solar_mass) + end + +val seed = Random.rand (42, 15210) +fun randFloat bound = + bound * (Random.randReal seed) + +val _ = print ("initializing bodies...\n") + +val bodies = Array.array (num_bodies * 7, 0.0); +val _ = + Util.for (0, num_bodies) (fn i => + ( set_x bodies i (randFloat 10.0) + ; set_y bodies i (randFloat 10.0) + ; set_z bodies i (randFloat 10.0) + ; set_vx bodies i (randFloat 5.0 * days_per_year) + ; set_vy bodies i (randFloat 4.0 * days_per_year) + ; set_vz bodies i (randFloat 5.0 * days_per_year) + ; set_mass bodies i (randFloat 10.0 * solar_mass) + ) + ) + +val _ = print ("offset momentum...\n"); +val _ = offset_momentum bodies + +val _ = print ("calculating initial energy...\n"); +val _ = print ("initial energy: " ^ Real.toString (energy bodies) ^ "\n") + +val _ = Benchmark.run "running simulation" (fn _ => + Util.for (0, n) (fn _ => advance bodies 0.01)) + +val _ = print ("final energy: " ^ Real.toString (energy bodies) ^ "\n") + diff --git a/tests/ocaml-nbody-packed/ocaml-nbody-packed.mlb b/tests/ocaml-nbody-packed/ocaml-nbody-packed.mlb new file mode 100644 index 000000000..c7b6ed5b0 --- /dev/null +++ b/tests/ocaml-nbody-packed/ocaml-nbody-packed.mlb @@ -0,0 +1,7 @@ +../mpllib/sources.$(COMPAT).mlb +local + $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb +in + structure Random +end +main.sml diff --git a/tests/ocaml-nbody/main.sml b/tests/ocaml-nbody/main.sml new file mode 100644 index 000000000..589b33051 --- /dev/null +++ b/tests/ocaml-nbody/main.sml @@ -0,0 +1,128 @@ +structure CLA = CommandLineArgs + +val num_domains = Concurrency.numberOfProcessors + +val n = CLA.parseInt "n" 500 +val num_bodies = CLA.parseInt "num_bodies" 1024 + +val pi = 3.141592653589793 +val solar_mass = 4.0 * pi * pi +val days_per_year = 365.24 + +type planet = + { x: real ref, y: real ref, z: real ref + , vx: real ref, vy: real ref, vz: real ref + , mass: real + } + +fun advance bodies dt = + let + in + ForkJoin.parfor 20 (0, num_bodies) (fn i => + let + val b = Array.sub (bodies, i) + val (vx, vy, vz) = (ref (!(#vx b)), ref (!(#vy b)), ref (!(#vz b))) + in + Util.for (0, Array.length bodies) (fn j => + let + val b' = Array.sub (bodies, j) + in + if i <> j then + let + val dx = !(#x b) - !(#x b') + val dy = !(#y b) - !(#y b') + val dz = !(#z b) - !(#z b') + val dist2 = dx * dx + dy * dy + dz * dz + val mag = dt / (dist2 * Math.sqrt(dist2)) + in + vx := !vx - dx * #mass b' * mag; + vy := !vy - dy * #mass b' * mag; + vz := !vz - dz * #mass b' * mag + end + else () + end); + + #vx b := !vx; + #vy b := !vy; + #vz b := !vz + end); + + Util.for (0, num_bodies) (fn i => + let + val b = Array.sub (bodies, i) + in + #x b := !(#x b) + dt * !(#vx b); + #y b := !(#y b) + dt * !(#vy b); + #z b := !(#z b) + dt * !(#vz b) + end) + + end + +fun energy bodies = + let + in + SeqBasis.reduce 1 op+ 0.0 (0, Array.length bodies) (fn i => + let + val b = Array.sub (bodies, i) + val e = ref 0.0 + in + e := !e + 0.5 * #mass b * + (!(#vx b) * !(#vx b) + !(#vy b) * !(#vy b) + !(#vz b) * !(#vz b)); + + Util.for (i+1, Array.length bodies) (fn j => + let + val b' = Array.sub (bodies, j) + val dx = !(#x b) - !(#x b') + val dy = !(#y b) - !(#y b') + val dz = !(#z b) - !(#z b') + val distance = Math.sqrt (dx * dx + dy * dy + dz * dz) + in + e := !e - (#mass b * #mass b') / distance + end); + + !e + end) + end + +fun offset_momentum bodies = + let + val px = ref 0.0 + val py = ref 0.0 + val pz = ref 0.0 + in + Util.for (0, Array.length bodies) (fn i => + let + val b = Array.sub (bodies, i) + in + px := !px + !(#vx b) * #mass b; + py := !py + !(#vy b) * #mass b; + pz := !pz + !(#vz b) * #mass b + end); + #vx (Array.sub (bodies, 0)) := ~ (!px) / solar_mass; + #vy (Array.sub (bodies, 0)) := ~ (!py) / solar_mass; + #vz (Array.sub (bodies, 0)) := ~ (!pz) / solar_mass + end + +val seed = Random.rand (42, 15210) +fun randFloat bound = + bound * (Random.randReal seed) + +val bodies = + Array.tabulate (num_bodies, fn _ => + { x = ref (randFloat 10.0) + , y = ref (randFloat 10.0) + , z = ref (randFloat 10.0) + , vx = ref (randFloat 5.0 * days_per_year) + , vy = ref (randFloat 4.0 * days_per_year) + , vz = ref (randFloat 5.0 * days_per_year) + , mass = randFloat 10.0 * solar_mass + }) + +val _ = offset_momentum bodies +val _ = print ("initial energy: " ^ Real.toString (energy bodies) ^ "\n") + +val _ = Benchmark.run "running simulation" (fn _ => + Util.for (0, n) (fn _ => advance bodies 0.01)) + +val _ = print ("final energy: " ^ Real.toString (energy bodies) ^ "\n") + diff --git a/tests/ocaml-nbody/ocaml-nbody.mlb b/tests/ocaml-nbody/ocaml-nbody.mlb new file mode 100644 index 000000000..c7b6ed5b0 --- /dev/null +++ b/tests/ocaml-nbody/ocaml-nbody.mlb @@ -0,0 +1,7 @@ +../mpllib/sources.$(COMPAT).mlb +local + $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb +in + structure Random +end +main.sml diff --git a/tests/ocaml-nbody/ocaml-source.ml b/tests/ocaml-nbody/ocaml-source.ml new file mode 100644 index 000000000..fa0af0d9e --- /dev/null +++ b/tests/ocaml-nbody/ocaml-source.ml @@ -0,0 +1,101 @@ +(* Copied from + * https://github.com/ocaml-bench/sandmark + * file benchmarks/multicore-numerical/nbody_multicore.ml + * commit fc1d270db57db643031deb66a25dba9147904a05 + *) + +(* The Computer Language Benchmarks Game + * https://salsa.debian.org/benchmarksgame-team/benchmarksgame/ + * + * Contributed by Troestler Christophe + *) + +module T = Domainslib.Task + +let num_domains = try int_of_string Sys.argv.(1) with _ -> 1 +let n = try int_of_string Sys.argv.(2) with _ -> 500 +let num_bodies = try int_of_string Sys.argv.(3) with _ -> 1024 + +let pi = 3.141592653589793 +let solar_mass = 4. *. pi *. pi +let days_per_year = 365.24 + +type planet = { mutable x : float; mutable y : float; mutable z : float; + mutable vx: float; mutable vy: float; mutable vz: float; + mass : float } + +let advance pool bodies dt = + T.parallel_for pool + ~start:0 + ~finish:(num_bodies - 1) + ~body:(fun i -> + let b = bodies.(i) in + let vx, vy, vz = ref b.vx, ref b.vy, ref b.vz in + for j = 0 to Array.length bodies - 1 do + Domain.Sync.poll(); + let b' = bodies.(j) in + if (i!=j) then begin + let dx = b.x -. b'.x and dy = b.y -. b'.y and dz = b.z -. b'.z in + let dist2 = dx *. dx +. dy *. dy +. dz *. dz in + let mag = dt /. (dist2 *. sqrt(dist2)) in + vx := !vx -. dx *. b'.mass *. mag; + vy := !vy -. dy *. b'.mass *. mag; + vz := !vz -. dz *. b'.mass *. mag; + end + done; + b.vx <- !vx; + b.vy <- !vy; + b.vz <- !vz); + for i = 0 to num_bodies - 1 do + Domain.Sync.poll(); + let b = bodies.(i) in + b.x <- b.x +. dt *. b.vx; + b.y <- b.y +. dt *. b.vy; + b.z <- b.z +. dt *. b.vz; + done + +let energy pool bodies = + T.parallel_for_reduce pool (+.) 0. + ~start:0 + ~finish:(Array.length bodies -1) + ~body:(fun i -> + let b = bodies.(i) and e = ref 0. in + e := !e +. 0.5 *. b.mass *. (b.vx *. b.vx +. b.vy *. b.vy +. b.vz *. b.vz); + for j = i+1 to Array.length bodies - 1 do + let b' = bodies.(j) in + let dx = b.x -. b'.x and dy = b.y -. b'.y and dz = b.z -. b'.z in + let distance = sqrt(dx *. dx +. dy *. dy +. dz *. dz) in + e := !e -. (b.mass *. b'.mass) /. distance; + Domain.Sync.poll () + done; + !e) + +let offset_momentum bodies = + let px = ref 0. and py = ref 0. and pz = ref 0. in + for i = 0 to Array.length bodies - 1 do + let b = bodies.(i) in + px := !px +. b.vx *. b.mass; + py := !py +. b.vy *. b.mass; + pz := !pz +. b.vz *. b.mass; + done; + bodies.(0).vx <- -. !px /. solar_mass; + bodies.(0).vy <- -. !py /. solar_mass; + bodies.(0).vz <- -. !pz /. solar_mass + +let bodies = + Array.init num_bodies (fun _ -> + { x = (Random.float 10.); + y = (Random.float 10.); + z = (Random.float 10.); + vx= (Random.float 5.) *. days_per_year; + vy= (Random.float 4.) *. days_per_year; + vz= (Random.float 5.) *. days_per_year; + mass=(Random.float 10.) *. solar_mass; }) + +let () = + let pool = T.setup_pool ~num_additional_domains:(num_domains - 1) in + offset_momentum bodies; + Printf.printf "%.9f\n" (energy pool bodies); + for _i = 1 to n do advance pool bodies 0.01 done; + Printf.printf "%.9f\n" (energy pool bodies); + T.teardown_pool pool diff --git a/tests/palindrome/Pal.sml b/tests/palindrome/Pal.sml new file mode 100644 index 000000000..318aa20ce --- /dev/null +++ b/tests/palindrome/Pal.sml @@ -0,0 +1,107 @@ +structure Pal: +sig + val longest: char Seq.t -> int * int +end = +struct + + structure AS = ArraySlice + + val p = 1045678717: Int64.int + val base = 500000000: Int64.int + val length = Seq.length + fun sub (str, i) = Seq.nth str i + fun toStr str = CharVector.tabulate (length str, (fn i => sub (str, i))) + + fun check str = + let + val n = length str + fun fromChar c = Int64.fromInt (Char.ord c) + fun modp v = v mod p + fun mul (a, b) = modp (Int64.* (a, b)) + fun add (a, b) = modp (Int64.+ (a, b)) + fun charMul (c, b) = mul (fromChar c, b) + + (* val (basePowers, _) = Seq.scan mul 1 (Seq.tabulate (fn _ => base) n) *) + val basePowers = AS.full (SeqBasis.scan 10000 mul 1 (0, n) (fn _ => base)) + + fun genHash getChar = + let + val P = AS.full (SeqBasis.scan 10000 add 0 (0, n) + (fn i => charMul (getChar i, Seq.nth basePowers i))) + (* val (P, total) = Seq.scan add 0 (Seq.zipWith charMul (str, basePowers)) *) + fun H (i, j) = + let + val last = Seq.nth P j + (* val last = if (j = n) then total else Seq.nth P j *) + val first = Seq.nth P i + val offset = Seq.nth basePowers (n-i-1) + open Int64 + in + modp ((last - first) * offset) + end + in + H + end + + val forwadHash = genHash (Seq.nth str) + val backHash = genHash (fn i => Seq.nth str (n-i-1)) + + fun perhaps (i, j) = (forwadHash(i, j) = backHash(n-j, n-i)) + in + perhaps + end + + (* Verifies that str[i:j] is a palindrome. *) + fun verify str (i, j) = + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (i, i + (j-i) div 2) + (fn k => Seq.nth str k = Seq.nth str (i+(j-k-1))) + + fun binarySearch check = + let + fun bs (i, j) = + if j - i = 1 then i + else + let + val mid = (i+j) div 2 + in + if check mid then bs (mid, j) + else bs (i, mid) + end + fun double i = if check (2*i) then double (2*i) else bs (i, 2*i) + in + if check 1 then double 1 else 0 + end + + (* Generates a polynomial hash of the string and reversed string. Then for + * each index in the string, binary search on the longest palindrome whose + * middle is that index. + *) + fun longest str = + let + val n = length str + (* val isPalinrome = Util.printTime "build" (fn () => check str) *) + val isPalinrome = check str + + fun maxval ((ia, la), (ib, lb)) = if la > lb then (ia, la) else (ib, lb) + + (* fun getMax f = Seq.reduce maxval (0, 0) (Seq.tabulate f n) *) + + fun getMax f = SeqBasis.reduce 1000 maxval (0, 0) (0, n) f + + fun checkOdd i j = + (i - j >= 0) andalso (i + j < n) andalso isPalinrome (i - j, i + j + 1) + val (io, lo) = getMax (fn i => (i, binarySearch (checkOdd i))) + + fun checkEven i j = + (i - j + 1 >= 0) andalso (i + j < n) + andalso isPalinrome(i - j + 1, i + j + 1) + val (ie, le) = getMax (fn i => (i, binarySearch (checkEven i))) + + val (i, l) = if le > lo then (ie-le+1, 2*le) else (io-lo, 2*lo+1) + val _ = if not(verify str (i, i + l)) then print("Failed!\n") + else () (* print(toStr(Seq.subseq str (i,l)) ^ "\n") *) + in + (i, l) + end + +end diff --git a/tests/palindrome/main.sml b/tests/palindrome/main.sml new file mode 100644 index 000000000..bfb649ee4 --- /dev/null +++ b/tests/palindrome/main.sml @@ -0,0 +1,26 @@ +structure CLA = CommandLineArgs +structure P = Pal + +val n = CLA.parseInt "N" (1000 * 1000) + +(* makes the sequence `ababab...` *) +fun gen i = if i mod 2 = 0 then #"a" else #"b" +val (input, tm) = Util.getTime (fn _ => Seq.tabulate gen n) +val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +val result = + Benchmark.run "finding longest palindrome" (fn _ => Pal.longest input) + +val _ = print ("found longest palindrome in " ^ Time.fmt 4 tm ^ "s\n") + +val correct = + if n mod 2 = 0 + then result = (1, n-1) + else result = (0, n) + +val _ = + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + diff --git a/tests/palindrome/palindrome.mlb b/tests/palindrome/palindrome.mlb new file mode 100644 index 000000000..799fb44ba --- /dev/null +++ b/tests/palindrome/palindrome.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +Pal.sml +main.sml diff --git a/tests/parens/MkParens.sml b/tests/parens/MkParens.sml new file mode 100644 index 000000000..bd868e94a --- /dev/null +++ b/tests/parens/MkParens.sml @@ -0,0 +1,31 @@ +functor MkParens (Seq: SEQUENCE) : +sig + datatype paren = Left | Right + val parenMatch: (paren ArraySequence.t) -> bool +end = +struct + + datatype paren = Left | Right + + fun combine ((l1,r1),(l2,r2)) = + if r1 > l2 then + (l1,r1+r2-l2) + else + (l1+l2-r1, r2) + + val id = (0, 0) + + fun singleton v = + case v of + Left => (0,1) + | Right => (1,0) + + fun parenMatch s = + let + val (l, r) = + Seq.reduce combine id (Seq.map singleton (Seq.fromArraySeq s)) + in + l = 0 andalso r = 0 + end + +end diff --git a/tests/parens/main.sml b/tests/parens/main.sml new file mode 100644 index 000000000..b0170542d --- /dev/null +++ b/tests/parens/main.sml @@ -0,0 +1,33 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure Parens = MkParens(DelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val doCheck = CLA.parseFlag "check" + +(* makes the sequence `()()()...` *) +fun gen i = + if i mod 2 = 0 then Parens.Left else Parens.Right + +val input = Seq.tabulate gen n + +fun task () = + Parens.parenMatch input + +fun check result = + if not doCheck then () else + let + val correct = + (n mod 2 = 0 andalso result) + orelse + (n mod 2 = 1 andalso not result) + in + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + end + +val result = Benchmark.run "parens" task +val _ = check result diff --git a/tests/parens/parens.mlb b/tests/parens/parens.mlb new file mode 100644 index 000000000..6ff0720ce --- /dev/null +++ b/tests/parens/parens.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MkParens.sml +main.sml diff --git a/tests/primes-blocked/primes-blocked.mlb b/tests/primes-blocked/primes-blocked.mlb new file mode 100644 index 000000000..e5c5b5763 --- /dev/null +++ b/tests/primes-blocked/primes-blocked.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +primes-blocked.sml diff --git a/tests/primes-blocked/primes-blocked.sml b/tests/primes-blocked/primes-blocked.sml new file mode 100644 index 000000000..ad51aa8d2 --- /dev/null +++ b/tests/primes-blocked/primes-blocked.sml @@ -0,0 +1,61 @@ +structure CLA = CommandLineArgs + +(* primes: int -> int array + * generate all primes up to (and including) n *) +fun blockedPrimes n = + if n < 2 then + ForkJoin.alloc 0 + else + let + val sqrtN = Real.floor (Math.sqrt (Real.fromInt n)) + val sqrtPrimes = blockedPrimes sqrtN + + val flags = ForkJoin.alloc (n + 1) : Word8.word array + fun mark i = Array.update (flags, i, 0w0) + fun unmark i = Array.update (flags, i, 0w1) + fun isMarked i = + Array.sub (flags, i) = 0w0 + val _ = ForkJoin.parfor 10000 (0, n + 1) mark + + val blockSize = Int.max (sqrtN, 1000) + val numBlocks = Util.ceilDiv (n + 1) blockSize + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n + 1) + + fun loop i = + if i >= Array.length sqrtPrimes then + () + else if 2 * Array.sub (sqrtPrimes, i) >= hi then + () + else + let + val p = Array.sub (sqrtPrimes, i) + val lom = Int.max (2, Util.ceilDiv lo p) + val him = Util.ceilDiv hi p + in + Util.for (lom, him) (fn m => unmark (m * p)); + loop (i + 1) + end + in + loop 0 + end) + in + SeqBasis.filter 4096 (2, n + 1) (fn i => i) isMarked + end + +(* ========================================================================== + * parse command-line arguments and run + *) + +val n = CLA.parseInt "N" (100 * 1000 * 1000) + +val msg = "generating primes up to " ^ Int.toString n +val result = Benchmark.run msg (fn _ => blockedPrimes n) + +val numPrimes = Array.length result +val _ = print ("number of primes " ^ Int.toString numPrimes ^ "\n") +val _ = print ("result " ^ Util.summarizeArray 8 Int.toString result ^ "\n") + diff --git a/tests/primes-segmented/SegmentedPrimes.sml b/tests/primes-segmented/SegmentedPrimes.sml new file mode 100644 index 000000000..a26c3718b --- /dev/null +++ b/tests/primes-segmented/SegmentedPrimes.sml @@ -0,0 +1,104 @@ +functor SegmentedPrimes + (I: + sig + type t + val from_int: int -> t + val to_int: t -> int + end): +sig + val primes: int -> I.t Seq.t + val primes_with_params: {block_size_factor: real, report_times: bool} + -> int + -> I.t Seq.t +end = +struct + + (* The block size used in the algorithm is approximately + * sqrt(n)*block_size_factor + * + * Increasing block_size_factor will use larger blocks, which has all of the + * following effects on performance: + * (1) decreased theoretical work + * (2) less data locality (?) + * (3) less parallelism + *) + fun primes_with_params (params as {block_size_factor, report_times}) n : + I.t Seq.t = + if n < 2 then + Seq.empty () + else + let + val sqrt_n = Real.floor (Math.sqrt (Real.fromInt n)) + + val sqrt_primes = primes_with_params params sqrt_n + + (* Split the range [2,n+1) into blocks *) + val block_size = Real.ceil (Real.fromInt sqrt_n * block_size_factor) + val block_size = Int.max (block_size, 1000) + val num_blocks = Util.ceilDiv ((n + 1) - 2) block_size + + val (block_results, tm) = Util.getTime (fn _ => + SeqBasis.reduce 1 TreeSeq.append (TreeSeq.empty ()) (0, num_blocks) + (fn b => + let + val lo = 2 + b * block_size + val hi = Int.min (lo + block_size, n + 1) + + val flags = Array.array (hi - lo, 0w1 : Word8.word) + fun unmark i = + Array.update (flags, i - lo, 0w0) + + fun loop i = + if i >= Seq.length sqrt_primes then + () + else if 2 * I.to_int (Seq.nth sqrt_primes i) >= hi then + () + else + let + val p = I.to_int (Seq.nth sqrt_primes i) + val lom = Int.max (2, Util.ceilDiv lo p) + val him = Util.ceilDiv hi p + in + Util.for (lom, him) (fn m => unmark (m * p)); + loop (i + 1) + end + + val _ = loop 0 + + val numPrimes = Util.loop (0, hi - lo) 0 (fn (count, i) => + if Array.sub (flags, i) = 0w0 then count else count + 1) + + val output = ForkJoin.alloc numPrimes + + val _ = Util.loop (lo, hi) 0 (fn (outi, i) => + if Array.sub (flags, i - lo) = 0w0 then outi + else (Array.update (output, outi, I.from_int i); outi + 1)) + in + TreeSeq.from_array_seq (ArraySlice.full output) + end)) + + val _ = + if not report_times then + () + else + print + ("sieve (n = " ^ Int.toString n ^ "): " ^ Time.fmt 4 tm ^ "s\n") + + val (result, tm) = Util.getTime (fn _ => + TreeSeq.to_array_seq block_results) + + val _ = + if not report_times then + () + else + print + ("flatten (n = " ^ Int.toString n ^ "): " ^ Time.fmt 4 tm ^ "s\n") + in + result + end + + + fun primes n = + primes_with_params {block_size_factor = 8.0, report_times = false} n + +end diff --git a/tests/primes-segmented/TreeSeq.sml b/tests/primes-segmented/TreeSeq.sml new file mode 100644 index 000000000..0c8322569 --- /dev/null +++ b/tests/primes-segmented/TreeSeq.sml @@ -0,0 +1,83 @@ +structure TreeSeq = +struct + datatype 'a t = + Leaf + | Elem of 'a + | Flat of 'a Seq.t + | Node of {num_elems: int, num_blocks: int, left: 'a t, right: 'a t} + + type 'a seq = 'a t + + fun length Leaf = 0 + | length (Elem _) = 1 + | length (Flat s) = Seq.length s + | length (Node {num_elems = n, ...}) = n + + fun num_blocks Leaf = 0 + | num_blocks (Elem _) = 1 + | num_blocks (Flat _) = 1 + | num_blocks (Node {num_blocks = nb, ...}) = nb + + + fun append (t1, t2) = + Node + { num_elems = length t1 + length t2 + , num_blocks = num_blocks t1 + num_blocks t2 + , left = t1 + , right = t2 + } + + + fun to_blocks (t: 'a t) : 'a Seq.t Seq.t = + let + val blocks = ForkJoin.alloc (num_blocks t) + + fun putBlocks offset t = + case t of + Leaf => () + | Elem x => Array.update (blocks, offset, Seq.singleton x) + | Flat s => Array.update (blocks, offset, s) + | Node {num_blocks = nb, left = l, right = r, ...} => + let + fun left () = putBlocks offset l + fun right () = + putBlocks (offset + num_blocks l) r + in + if nb <= 1000 then (left (); right ()) + else (ForkJoin.par (left, right); ()) + end + in + putBlocks 0 t; + ArraySlice.full blocks + end + + + fun to_array_seq t = + let + val a = ForkJoin.alloc (length t) + fun put offset t = + case t of + Leaf => () + | Elem x => Array.update (a, offset, x) + | Flat s => Seq.foreach s (fn (i, x) => Array.update (a, offset + i, x)) + | Node {num_elems = n, left = l, right = r, ...} => + let + fun left () = put offset l + fun right () = + put (offset + length l) r + in + if n <= 4096 then (left (); right ()) + else (ForkJoin.par (left, right); ()) + end + in + put 0 t; + ArraySlice.full a + end + + fun from_array_seq a = Flat a + + fun empty () = Leaf + fun singleton x = Elem x + val $ = singleton + +end diff --git a/tests/primes-segmented/main.sml b/tests/primes-segmented/main.sml new file mode 100644 index 000000000..c1b3aad51 --- /dev/null +++ b/tests/primes-segmented/main.sml @@ -0,0 +1,64 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" (100 * 1000 * 1000) +val block_size_factor = CLA.parseReal "block-size-factor" 16.0 +val bits = CLA.parseInt "bits" 64 +val report_times = CLA.parseFlag "report-times" + +val _ = print ("N " ^ Int.toString n ^ "\n") +val _ = print ("block-size-factor " ^ Real.toString block_size_factor ^ "\n") +val _ = print ("bits " ^ Int.toString bits ^ "\n") +val _ = print ("report-times? " ^ (if report_times then "yes" else "no") ^ "\n") + +functor Main + (I: + sig + type t + val from_int: int -> t + val to_int: t -> int + val to_string: t -> string + end) = +struct + structure Primes = SegmentedPrimes(I) + + fun main () = + let + val params = + {block_size_factor = block_size_factor, report_times = report_times} + val msg = "generating primes up to " ^ Int.toString n + + val result = Benchmark.run msg (fn _ => + Primes.primes_with_params params n) + + val numPrimes = Seq.length result + val _ = print ("number of primes " ^ Int.toString numPrimes ^ "\n") + val _ = print + ("result " ^ Util.summarizeArraySlice 8 I.to_string result ^ "\n") + in + () + end +end + +structure Main32 = + Main + (struct + type t = Int32.int + val from_int = Int32.fromInt + val to_int = Int32.toInt + val to_string = Int32.toString + end) + +structure Main64 = + Main + (struct + type t = Int64.int + val from_int = Int64.fromInt + val to_int = Int64.toInt + val to_string = Int64.toString + end) + +val _ = + case bits of + 64 => Main64.main () + | 32 => Main32.main () + | _ => Util.die ("unknown -bits " ^ Int.toString bits ^ ": must be 32 or 64") diff --git a/tests/primes-segmented/primes-segmented.mlb b/tests/primes-segmented/primes-segmented.mlb new file mode 100644 index 000000000..e39bb30b2 --- /dev/null +++ b/tests/primes-segmented/primes-segmented.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +TreeSeq.sml +SegmentedPrimes.sml +main.sml diff --git a/tests/primes/check-stack-dir/check-stack b/tests/primes/check-stack-dir/check-stack new file mode 100755 index 0000000000000000000000000000000000000000..ae2f945fb025c2e2128608fa777711da873a0c42 GIT binary patch literal 1296216 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≫w6qnQ}oY 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 n_!7QW2^kNlt_|p@j0e;)>Wg_; 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̽x2SPptg%yZJ@TR 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

4e5pVg2QJh!{hLOUyRjFymn#{9|M*Y9BJ$cbI< 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 #)j1&O2Wd z=252cX^fxUm&^?2hS=6#nhqD%UQO>1_}!`XPPBNsV>H=an;X(hE;La5hnQAPBLkmA zsj!9b)X>sJPOmYanpR&w&FQf)CePC4^4^grs(o$ue%P_H2dh;O`$oBvzumx zj%l1lKaSFy=w`DNOp&J0wY0>k4vjrhu1iesOvKZfH04R^Kf<|7#>z@7>S*85un4LlsEP&_)2aoQTooMc%QJxd*Ly5DS^M>C{^G^FulPsF?!P9zkX z(^5Zwc4+p3S)mYJ@xZ}-Uz}!!vyNR*KfkfTdxx4}iiiAj7c36Vo(B!{X0xlIda z);EO~&Y9CXyDijKKeK6e$myb3Nz<18cqAB{+tfI-Ayi&kURvcW>!*x%1ru)sy?G8;hBdvkz5EY1qnS4c?a4j0M@ybRug4jD~tNgsc6JZvF%{j z7K)`?qTQT(sy&lI35O<7($@&0;S`<56ZB3@H0mu~b2vp_80`&(d%5upHqkbHG%|B= ze$;VqMWRcWMpG?O(vyyw%@lT2lTNaq+CVtv)k$*z(-1r9|BRI$ZEqUiH07sKl6z1! zbduuEzNA-8e$~=?7!{EG-K3pes;B6e40lGk-dE7ve=fZW(9jUdPCT4&Mtn5Zz*sk$Gx(lPTpaks0){{;(aO;i_?yC zTnt5pjMIw=UI}QTDBpp18MtB6jtsV8xb|rkgce>TofTF7G);AkaV*wHEiIKc&9}NTMD?F(OSDkG zO072%>*_L$*Psocu^s+-D!;2cZ!YGAFyrMiO zFwwh2loC5=LpaVpN^ODOXo*DAF>2Y_T5qMym>S^iQxgbSl4{MAkt2qw7V_{kM+p;& zVHX-7IsLJCrY}6umM{Z|R(bnK4YNoME3eYXe0^CuuXS`qDZK)bScXPkFQ!6~Fb4kW zg6UXy*lZ={$@Y<3Qk0~p_=`ehCUS3zOJOVKqw=zGof^Lwj1rT4OOVq696wM?Yof+? zWjVssa$}s|1>>PyHllIog&x_PKS_ZSrA;7bc{)x#XFStYP$r)4PtnS8M&>dF#-CkO zJsDI?4{yBk7pmG433-o%zPx2#|FNpJiCTjAGWv*CGE$^aGd9{0nPQ1l?s$><#`?q_O^4Uzh=UjQ|LLp;qJ5&+UzQhIZmVv>^{Cu^ONBT)d;Rzch8 zI&sb$6)$c+roLrXZvN8AXlJY|*6A&PnHRg6qT!nRNFR+Z2Dl%k9?KeGVSu2o(rHNI zKu0;Jwz+gBccI+seY3!{%6KM?0Jw*U@br^*ta!~K$D4WF zVqX50Y*T^BMbN1jlx8E41Ey$rlk=j6R*O4|Bhzvz*-1k2#0v9NcBzxr+^I6t(S|TN zCHc5d6`Hn1Ghxc8w~QQ%C|+a-q?8=xi!$0p1>&)U>6bhMI~kdTlkR)3W>!aAUWH7B zs;0L`JYAvN&h6eudO)Oa-d+`7DWsd)B&|9oQga(haY^JzxFR;HJa z!|5J!!t|_uo&(cbpM*V{T6H`9TfPM6EbyO~z^&x0A$c1`bQ z7S4><`FJ&%lgBZ}KD>`>Ml;kv#5(C@EKZY%r4;}8;LPO7wJ}y_I2G;c?=7(R+R6iV zu7Ui?Qwu#rujbG_%1wRW!c>f2^$wdbmFyWzbIoDx!?cl5UpQuUTD}psZQ+91^)0ho z+Z-%~*~0+uK)Qh@^kyJ!Sr5^#o|ht}6;Pj@+bpHE8?#b%N9Y#=$S~9mORtEc`T1=) zwUj0-)M)9&DhUM?YQnv}ym8myd1;Gℜt+!>PrzNJkIx+uf$YaiwGUsRMvKmt0J4 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 Node(ref(lis), x)) End (0, n) elem + +(*fun printList lis = + case lis of + End => print "end\n" + | Node(a, b) => (print (Int.toString b ^ " "); printList (!a)) + +fun delete (idx:int) (lis:linkedList) = + case lis of + End => raise IndexError + | Node(a, b) => + if idx = 0 then + (!a, Node(a, b)) + else if idx = 1 then + let + val delElement = !a + val delNext = case delElement of + End => raise IndexError + | Node(c, d) => !c + val _ = (a := delNext) + in + (Node(a, b), delElement) + end + else + delete (idx - 1) (!a)*) + +(*val x = delete (n-1) y*) diff --git a/tests/primes/primes.mlb b/tests/primes/primes.mlb new file mode 100644 index 000000000..72e5d0ac4 --- /dev/null +++ b/tests/primes/primes.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +primes.sml diff --git a/tests/primes/primes.sml b/tests/primes/primes.sml new file mode 100644 index 000000000..3e8308e23 --- /dev/null +++ b/tests/primes/primes.sml @@ -0,0 +1,46 @@ +structure CLA = CommandLineArgs + +(* primes: int -> int array + * generate all primes up to (and including) n *) +fun primes n = + if n < 2 then ForkJoin.alloc 0 else + let + (* all primes up to sqrt(n) *) + val sqrtPrimes = primes (Real.floor (Math.sqrt (Real.fromInt n))) + + (* allocate array of flags to mark primes. *) + val flags = ForkJoin.alloc (n+1) : Word8.word array + fun mark i = Array.update (flags, i, 0w0) + fun unmark i = Array.update (flags, i, 0w1) + fun isMarked i = Array.sub (flags, i) = 0w0 + + (* initially, mark every number *) + val _ = ForkJoin.parfor 10000 (0, n+1) mark + + (* unmark every multiple of every prime in sqrtPrimes *) + val _ = + ForkJoin.parfor 1 (0, Array.length sqrtPrimes) (fn i => + let + val p = Array.sub (sqrtPrimes, i) + val numMultiples = n div p - 1 + in + ForkJoin.parfor 4096 (0, numMultiples) (fn j => unmark ((j+2) * p)) + end) + in + (* for every i in 2 <= i <= n, filter those that are still marked *) + SeqBasis.filter 4096 (2, n+1) (fn i => i) isMarked + end + +(* ========================================================================== + * parse command-line arguments and run + *) + +val n = CLA.parseInt "N" (100 * 1000 * 1000) + +val msg = "generating primes up to " ^ Int.toString n +val result = Benchmark.run msg (fn _ => primes n) + +val numPrimes = Array.length result +val _ = print ("number of primes " ^ Int.toString numPrimes ^ "\n") +val _ = print ("result " ^ Util.summarizeArray 8 Int.toString result ^ "\n") + diff --git a/tests/primes/safe/primes.mlb b/tests/primes/safe/primes.mlb new file mode 100644 index 000000000..72e5d0ac4 --- /dev/null +++ b/tests/primes/safe/primes.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +primes.sml diff --git a/tests/primes/safe/primes.sml b/tests/primes/safe/primes.sml new file mode 100644 index 000000000..4db9b2354 --- /dev/null +++ b/tests/primes/safe/primes.sml @@ -0,0 +1,47 @@ +(* primes: int -> int array + * generate all primes up to (and including) n *) +fun primes n = + if n < 2 then ForkJoin.alloc 0 else + let + (* all primes up to sqrt(n) *) + val sqrtPrimes = primes (Real.floor (Math.sqrt (Real.fromInt n))) + + (* allocate array of flags to mark primes. *) + val flags = ForkJoin.alloc (n+1) : Word8.word array + fun mark i = Array.update (flags, i, 0w0) + fun unmark i = Array.update (flags, i, 0w1) + fun isMarked i = Array.sub (flags, i) = 0w0 + + (* initially, mark every number *) + val _ = ForkJoin.parfor 10000 (0, n+1) mark + + (* unmark every multiple of every prime in sqrtPrimes *) + val _ = + ForkJoin.parfor 1 (0, Array.length sqrtPrimes) (fn i => + let + val p = Array.sub (sqrtPrimes, i) + val numMultiples = n div p - 1 + in + ForkJoin.parfor 4096 (0, numMultiples) (fn j => unmark ((j+2) * p)) + end) + in + (* for every i in 2 <= i <= n, filter those that are still marked *) + SeqBasis.filter 4096 (2, n+1) (fn i => i) isMarked + end + +(* ========================================================================== + * parse command-line arguments and run + *) + +val n = CommandLineArgs.parseInt "N" (100 * 1000 * 1000) +val _ = print ("generating primes up to " ^ Int.toString n ^ "\n") + +val t0 = Time.now () +val result = primes n +val t1 = Time.now () + +val _ = print ("finished in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val numPrimes = Array.length result +val _ = print ("number of primes " ^ Int.toString numPrimes ^ "\n") +val _ = print ("result " ^ Util.summarizeArray 8 Int.toString result ^ "\n") diff --git a/tests/pure-msort-int32/msort.sml b/tests/pure-msort-int32/msort.sml new file mode 100644 index 000000000..064b82c95 --- /dev/null +++ b/tests/pure-msort-int32/msort.sml @@ -0,0 +1,37 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" (100 * 1000 * 1000) +val quicksortGrain = CLA.parseInt "quicksort" 1024 +val grain = CLA.parseInt "grain" 1024 +val _ = print ("N " ^ Int.toString n ^ "\n") + +val _ = print ("generating " ^ Int.toString n ^ " random integers\n") + +val max32 = Word64.fromLargeInt (Int32.toLarge (valOf Int32.maxInt)) + +fun elem i = + Int32.fromInt (Word64.toInt (Word64.mod (Util.hash64 (Word64.fromInt i), Word64.fromInt n))) +val input = PureSeq.tabulate elem n + +fun sort cmp xs = + if PureSeq.length xs <= quicksortGrain then + PureSeq.quicksort cmp xs + else + let + val n = PureSeq.length xs + val l = PureSeq.take xs (n div 2) + val r = PureSeq.drop xs (n div 2) + val (l', r') = + if n <= grain then + (sort cmp l, sort cmp r) + else + ForkJoin.par (fn _ => sort cmp l, fn _ => sort cmp r) + in + PureSeq.merge cmp (l', r') + end + +val result = + Benchmark.run "running mergesort" (fn _ => sort Int32.compare input) + +(* val _ = print ("result " ^ Util.summarizeArraySlice 8 Int.toString result ^ "\n") *) + diff --git a/tests/pure-msort-int32/pure-msort-int32.mlb b/tests/pure-msort-int32/pure-msort-int32.mlb new file mode 100644 index 000000000..394068b7b --- /dev/null +++ b/tests/pure-msort-int32/pure-msort-int32.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +msort.sml diff --git a/tests/pure-msort-strings/msort.sml b/tests/pure-msort-strings/msort.sml new file mode 100644 index 000000000..01d170e31 --- /dev/null +++ b/tests/pure-msort-strings/msort.sml @@ -0,0 +1,55 @@ +structure CLA = CommandLineArgs + +fun usage () = + let + val msg = + "usage: msort-strings FILE [-grain ...] [--long] \n" + in + TextIO.output (TextIO.stdErr, msg); + OS.Process.exit OS.Process.failure + end + +val filename = + case CLA.positional () of + [x] => x + | _ => usage () + +val makeLong = CLA.parseFlag "long" +val quicksortGrain = CLA.parseInt "quicksort" 1024 +val grain = CLA.parseInt "grain" 1024 + +val (contents, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filename) +val _ = print ("read file in " ^ Time.fmt 4 tm ^ "s\n") +val (tokens, tm) = Util.getTime (fn _ => Tokenize.tokens Char.isSpace contents) +val _ = print ("tokenized in " ^ Time.fmt 4 tm ^ "s\n") + +val prefix = CharVector.tabulate (32, fn _ => #"a") + +val tokens = + if not makeLong then tokens + else Seq.map (fn str => prefix ^ str) tokens + +val tokens = PureSeq.fromSeq tokens + +fun sort cmp xs = + if PureSeq.length xs <= quicksortGrain then + PureSeq.quicksort cmp xs + else + let + val n = PureSeq.length xs + val l = PureSeq.take xs (n div 2) + val r = PureSeq.drop xs (n div 2) + val (l', r') = + if n <= grain then + (sort cmp l, sort cmp r) + else + ForkJoin.par (fn _ => sort cmp l, fn _ => sort cmp r) + in + PureSeq.merge cmp (l', r') + end + +val result = + Benchmark.run "running mergesort" (fn _ => sort String.compare tokens) + +(* val _ = print ("result " ^ Util.summarizeArraySlice 8 (fn x => x) result ^ "\n") *) + diff --git a/tests/pure-msort-strings/pure-msort-strings.mlb b/tests/pure-msort-strings/pure-msort-strings.mlb new file mode 100644 index 000000000..394068b7b --- /dev/null +++ b/tests/pure-msort-strings/pure-msort-strings.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +msort.sml diff --git a/tests/pure-msort/msort.sml b/tests/pure-msort/msort.sml new file mode 100644 index 000000000..c61b9baed --- /dev/null +++ b/tests/pure-msort/msort.sml @@ -0,0 +1,35 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" (100 * 1000 * 1000) +val quicksortGrain = CLA.parseInt "quicksort" 1024 +val grain = CLA.parseInt "grain" 1024 +val _ = print ("N " ^ Int.toString n ^ "\n") + +val _ = print ("generating " ^ Int.toString n ^ " random integers\n") + +fun elem i = + Word64.toInt (Word64.mod (Util.hash64 (Word64.fromInt i), Word64.fromInt n)) +val input = PureSeq.tabulate elem n + +fun sort cmp xs = + if PureSeq.length xs <= quicksortGrain then + PureSeq.quicksort cmp xs + else + let + val n = PureSeq.length xs + val l = PureSeq.take xs (n div 2) + val r = PureSeq.drop xs (n div 2) + val (l', r') = + if n <= grain then + (sort cmp l, sort cmp r) + else + ForkJoin.par (fn _ => sort cmp l, fn _ => sort cmp r) + in + PureSeq.merge cmp (l', r') + end + +val result = + Benchmark.run "running mergesort" (fn _ => sort Int.compare input) + +val _ = print ("result " ^ PureSeq.summarize 10 Int.toString result ^ "\n") + diff --git a/tests/pure-msort/pure-msort.mlb b/tests/pure-msort/pure-msort.mlb new file mode 100644 index 000000000..394068b7b --- /dev/null +++ b/tests/pure-msort/pure-msort.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +msort.sml diff --git a/tests/pure-nn/nn.sml b/tests/pure-nn/nn.sml new file mode 100644 index 000000000..4053a4d5d --- /dev/null +++ b/tests/pure-nn/nn.sml @@ -0,0 +1,420 @@ +structure NN : +sig + type t + type 'a seq = 'a PureSeq.t + + type point = Geometry2D.point + + (* makeTree leafSize points *) + val makeTree : int -> point seq -> t + + (* allNearestNeighbors grain quadtree *) + val allNearestNeighbors : int -> t -> int seq +end = +struct + + structure A = Array + structure AS = ArraySlice + structure V = Vector + structure VS = VectorSlice + val unsafeCast: 'a array -> 'a vector = VectorExtra.unsafeFromArray + + type 'a seq = 'a VS.slice + structure G = Geometry2D + type point = G.point + + fun par4 (a, b, c, d) = + let + val ((ar, br), (cr, dr)) = + ForkJoin.par (fn _ => ForkJoin.par (a, b), + fn _ => ForkJoin.par (c, d)) + in + (ar, br, cr, dr) + end + + datatype tree = + Leaf of { anchor : point + , width : real + , vertices : int seq (* indices of original point seq *) + } + | Node of { anchor : point + , width : real + , count : int + , children : tree seq + } + + type t = tree * point seq + + fun count t = + case t of + Leaf {vertices, ...} => PureSeq.length vertices + | Node {count, ...} => count + + fun anchor t = + case t of + Leaf {anchor, ...} => anchor + | Node {anchor, ...} => anchor + + fun width t = + case t of + Leaf {width, ...} => width + | Node {width, ...} => width + + fun boxOf t = + case t of + Leaf {anchor=(x,y), width, ...} => (x, y, x+width, y+width) + | Node {anchor=(x,y), width, ...} => (x, y, x+width, y+width) + + fun indexApp grain f t = + let + fun downSweep offset t = + case t of + Leaf {vertices, ...} => + VS.appi (fn (i, v) => f (offset + i, v)) vertices + | Node {children, ...} => + let + fun q i = VS.sub (children, i) + fun qCount i = count (q i) + val offset0 = offset + val offset1 = offset0 + qCount 0 + val offset2 = offset1 + qCount 1 + val offset3 = offset2 + qCount 2 + in + if count t <= grain then + ( downSweep offset0 (q 0) + ; downSweep offset1 (q 1) + ; downSweep offset2 (q 2) + ; downSweep offset3 (q 3) + ) + else + ( par4 + ( fn _ => downSweep offset0 (q 0) + , fn _ => downSweep offset1 (q 1) + , fn _ => downSweep offset2 (q 2) + , fn _ => downSweep offset3 (q 3) + ) + ; () + ) + end + in + downSweep 0 t + end + + fun indexMap grain f t = + let + val result = ForkJoin.alloc (count t) + val _ = indexApp grain (fn (i, v) => A.update (result, i, f (i, v))) t + in + VS.full (unsafeCast result) + end + + fun flatten grain t = indexMap grain (fn (_, v) => v) t + + (* val lowerTime = ref Time.zeroTime + val upperTime = ref Time.zeroTime + fun addTm r t = + if Primitives.numberOfProcessors = 1 then + r := Time.+ (!r, t) + else () + fun clearAndReport r name = + (print (name ^ " " ^ Time.fmt 4 (!r) ^ "\n"); r := Time.zeroTime) *) + + (* Make a tree where all points are in the specified bounding box. *) + fun makeTreeBounded leafSize (verts : point seq) (idx : int Seq.t) ((xLeft, yBot) : G.point) width = + if Seq.length idx <= leafSize then + Leaf { anchor = (xLeft, yBot) + , width = width + , vertices = PureSeq.fromSeq idx + } + else let + val qw = width/2.0 (* quadrant width *) + val center = (xLeft + qw, yBot + qw) + + val ((sorted, offsets), tm) = Util.getTime (fn () => + CountingSort.sort idx (fn i => + G.quadrant center (PureSeq.nth verts (Seq.nth idx i))) 4) + + (* val _ = + if AS.length idx >= 4 * leafSize then + addTm upperTime tm + else + addTm lowerTime tm *) + + fun quadrant i = + let + val start = AS.sub (offsets, i) + val len = AS.sub (offsets, i+1) - start + val childIdx = AS.subslice (sorted, start, SOME len) + val qAnchor = + case i of + 0 => (xLeft + qw, yBot + qw) + | 1 => (xLeft, yBot + qw) + | 2 => (xLeft, yBot) + | _ => (xLeft + qw, yBot) + in + makeTreeBounded leafSize verts childIdx qAnchor qw + end + + (* val children = Seq.tabulate (Perf.grain 1) quadrant 4 *) + val (a, b, c, d) = + if AS.length idx <= 100 then + (quadrant 0, quadrant 1, quadrant 2, quadrant 3) + else + par4 + ( fn _ => quadrant 0 + , fn _ => quadrant 1 + , fn _ => quadrant 2 + , fn _ => quadrant 3 ) + val children = PureSeq.fromList [a,b,c,d] + in + Node { anchor = (xLeft, yBot) + , width = width + , count = AS.length idx + , children = children + } + end + + fun loop (lo, hi) b f = + if (lo >= hi) then b else loop (lo+1, hi) (f (b, lo)) f + + fun reduce grain f b (get, lo, hi) = + if hi - lo <= grain then + loop (lo, hi) b (fn (b, i) => f (b, get i)) + else let + val mid = lo + (hi-lo) div 2 + val (l,r) = ForkJoin.par + ( fn _ => reduce grain f b (get, lo, mid) + , fn _ => reduce grain f b (get, mid, hi) + ) + in + f (l, r) + end + + fun makeTree leafSize (verts : point seq) = + if PureSeq.length verts = 0 then raise Fail "makeTree with 0 points" else + let + (* calculate the bounding box *) + fun maxPt ((x1,y1),(x2,y2)) = (Real.max (x1, x2), Real.max (y1, y2)) + fun minPt ((x1,y1),(x2,y2)) = (Real.min (x1, x2), Real.min (y1, y2)) + fun getPt i = PureSeq.nth verts i + val (xLeft,yBot) = reduce 10000 minPt (Real.posInf, Real.posInf) (getPt, 0, VS.length verts) + val (xRight,yTop) = reduce 10000 maxPt (Real.negInf, Real.negInf) (getPt, 0, VS.length verts) + val width = Real.max (xRight-xLeft, yTop-yBot) + + val idx = Seq.tabulate (fn i => i) (PureSeq.length verts) + val result = makeTreeBounded leafSize verts idx (xLeft, yBot) width + in + (* clearAndReport upperTime "upper sort time"; *) + (* clearAndReport lowerTime "lower sort time"; *) + (result, verts) + end + + (* ======================================================================== *) + + fun constrain (x : real) (lo, hi) = + if x < lo then lo + else if x > hi then hi + else x + + fun distanceToBox (x,y) (xLeft, yBot, xRight, yTop) = + G.distance (x,y) (constrain x (xLeft, xRight), constrain y (yBot, yTop)) + + val dummyBest = (~1, Real.posInf) + + fun nearestNeighbor (t : tree, pts) (pi : int) = + let + fun pt i = PureSeq.nth pts i + + val p = pt pi + + fun refineNearest (qi, (bestPt, bestDist)) = + if pi = qi then (bestPt, bestDist) else + let + val qDist = G.distance p (pt qi) + in + if qDist < bestDist + then (qi, qDist) + else (bestPt, bestDist) + end + + fun search (best as (_, bestDist : real)) t = + if distanceToBox p (boxOf t) > bestDist then best else + case t of + Leaf {vertices, ...} => + VS.foldl refineNearest best vertices + | Node {anchor=(x,y), width, children, ...} => + let + val qw = width/2.0 + val center = (x+qw, y+qw) + + (* search the quadrant that p is in first *) + val heuristicOrder = + case G.quadrant center p of + 0 => [0,1,2,3] + | 1 => [1,0,2,3] + | 2 => [2,1,3,0] + | _ => [3,0,2,1] + + fun child i = VS.sub (children, i) + fun refine (i, best) = search best (child i) + in + List.foldl refine best heuristicOrder + end + + val (best, _) = search dummyBest t + in + best + end + + fun allNearestNeighbors grain (t, pts) = + let + val n = PureSeq.length pts + val idxs = flatten 10000 t + val nn = ForkJoin.alloc n + in + ForkJoin.parfor grain (0, n) (fn i => + let + val j = PureSeq.nth idxs i + in + A.update (nn, j, nearestNeighbor (t, pts) j) + end); + VS.full (unsafeCast nn) + end + +end + +(* ========================================================================== + * Now the main bit + *) + +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" 1000000 +val leafSize = CLA.parseInt "leafSize" 16 +val grain = CLA.parseInt "grain" 100 +val seed = CLA.parseInt "seed" 15210 + +fun genReal i = + let + val x = Word64.fromInt (seed + i) + in + Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 x, 0w1000000))) + / 1000000.0 + end + +fun genPoint i = (genReal (2*i), genReal (2*i + 1)) +val (input, tm) = Util.getTime (fn _ => PureSeq.tabulate genPoint n) +val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +fun nnEx() = + let + val (tree, tm) = Util.getTime (fn _ => NN.makeTree leafSize input) + val _ = print ("built quadtree in " ^ Time.fmt 4 tm ^ "s\n") + + val (nbrs, tm) = Util.getTime (fn _ => NN.allNearestNeighbors grain tree) + val _ = print ("found all neighbors in " ^ Time.fmt 4 tm ^ "s\n") + in + (tree, nbrs) + end + +val (tree, nbrs) = Benchmark.run "running nearest neighbors" nnEx + +(* now input[nbrs[i]] is the closest point to input[i] *) + +(* ========================================================================== + * write image to output + * this only works if all input points are within [0,1) *) + +val filename = CLA.parseString "output" "" +val _ = + if filename <> "" then () + else ( print ("to see output, use -output and -resolution arguments\n" ^ + "for example: nn -N 10000 -output result.ppm -resolution 1000\n") + ; GCStats.report () + ; OS.Process.exit OS.Process.success + ) + +val t0 = Time.now () + +val resolution = CLA.parseInt "resolution" 1000 +val width = resolution +val height = resolution + +val image = + { width = width + , height = height + , data = Seq.tabulate (fn _ => Color.white) (width*height) + } + +fun set (i, j) x = + if 0 <= i andalso i < height andalso + 0 <= j andalso j < width + then ArraySlice.update (#data image, i*width + j, x) + else () + +val r = Real.fromInt resolution +fun px x = Real.floor (x * r) +fun pos (x, y) = (resolution - px x - 1, px y) + +fun horizontalLine i (j0, j1) = + if j1 < j0 then horizontalLine i (j1, j0) + else Util.for (j0, j1) (fn j => set (i, j) Color.red) + +fun sign xx = + case Int.compare (xx, 0) of LESS => ~1 | EQUAL => 0 | GREATER => 1 + +(* Bresenham's line algorithm *) +fun line (x1, y1) (x2, y2) = + let + val w = x2 - x1 + val h = y2 - y1 + val dx1 = sign w + val dy1 = sign h + val (longest, shortest, dx2, dy2) = + if Int.abs w > Int.abs h then + (Int.abs w, Int.abs h, dx1, 0) + else + (Int.abs h, Int.abs w, 0, dy1) + + fun loop i numerator x y = + if i > longest then () else + let + val numerator = numerator + shortest; + in + set (x, y) Color.red; + if numerator >= longest then + loop (i+1) (numerator-longest) (x+dx1) (y+dy1) + else + loop (i+1) numerator (x+dx2) (y+dy2) + end + in + loop 0 (longest div 2) x1 y1 + end + +(* mark all nearest neighbors with straight red lines *) +val t0 = Time.now () + +val _ = ForkJoin.parfor 10000 (0, PureSeq.length input) (fn i => + line (pos (PureSeq.nth input i)) (pos (PureSeq.nth input (PureSeq.nth nbrs i)))) + +(* mark input points as a pixel *) +val _ = + ForkJoin.parfor 10000 (0, PureSeq.length input) (fn i => + let + val (x, y) = pos (PureSeq.nth input i) + fun b spot = set spot Color.black + in + b (x-1, y); + b (x, y-1); + b (x, y); + b (x, y+1); + b (x+1, y) + end) + +val t1 = Time.now () + +val _ = print ("generated image in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") + +val (_, tm) = Util.getTime (fn _ => PPM.write filename image) +val _ = print ("wrote to " ^ filename ^ " in " ^ Time.fmt 4 tm ^ "s\n") + diff --git a/tests/pure-nn/pure-nn.mlb b/tests/pure-nn/pure-nn.mlb new file mode 100644 index 000000000..520fc85c3 --- /dev/null +++ b/tests/pure-nn/pure-nn.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +nn.sml diff --git a/tests/pure-quickhull/Quickhull.sml b/tests/pure-quickhull/Quickhull.sml new file mode 100644 index 000000000..a5e97b415 --- /dev/null +++ b/tests/pure-quickhull/Quickhull.sml @@ -0,0 +1,120 @@ +structure Quickhull : +sig + val hull : (real * real) PureSeq.t -> int PureSeq.t +end = +struct + + structure AS = ArraySlice + structure G = Geometry2D + structure Tree = TreeSeq + + fun hull pts = + let + fun pt i = PureSeq.nth pts i + fun dist p q i = G.Point.triArea (p, q, pt i) + fun max ((i, di), (j, dj)) = + if di > dj then (i, di) else (j, dj) + fun x i = #1 (pt i) + + fun aboveLine p q i = (dist p q i > 0.0) + + fun parHull idxs l r = + if PureSeq.length idxs < 2 then + Tree.fromPureSeq idxs + (* if DS.length idxs <= 2048 then + seqHull idxs l r *) + else + let + val lp = pt l + val rp = pt r + fun d i = dist lp rp i + + (* val idxs = DS.fromArraySeq idxs *) + + val (mid, _) = SeqBasis.reduce 10000 max (~1, Real.negInf) + (0, PureSeq.length idxs) + (fn i => (PureSeq.nth idxs i, d (PureSeq.nth idxs i))) + (* val distances = DS.map (fn i => (i, d i)) idxs + val (mid, _) = DS.reduce max (~1, Real.negInf) distances *) + + val midp = pt mid + + fun flag i = + if aboveLine lp midp i then Split.Left + else if aboveLine midp rp i then Split.Right + else Split.Throwaway + val (left, right) = + Split.parSplit idxs (PureSeq.map flag idxs) + (* (DS.force (DS.map flag idxs)) *) + + fun doLeft () = parHull left l mid + fun doRight () = parHull right mid r + val (leftHull, rightHull) = + if PureSeq.length left + PureSeq.length right <= 2048 + then (doLeft (), doRight ()) + else ForkJoin.par (doLeft, doRight) + in + Tree.append (leftHull, + (Tree.append (Tree.$ mid, rightHull))) + end + + (* val tm = Util.startTiming () *) + + (* val allIdx = DS.tabulate (fn i => i) (Seq.length pts) *) + + (* This is faster than doing two reduces *) + (* val (l, r) = DS.reduce + (fn ((l1, r1), (l2, r2)) => + (if x l1 < x l2 then l1 else l2, + if x r1 > x r2 then r1 else r2)) + (0, 0) + (DS.map (fn i => (i, i)) allIdx) *) + + val (l, r) = SeqBasis.reduce 10000 + (fn ((l1, r1), (l2, r2)) => + (if x l1 < x l2 then l1 else l2, + if x r1 > x r2 then r1 else r2)) + (0, 0) + (0, PureSeq.length pts) + (fn i => (i, i)) + + (* val tm = Util.tick tm "endpoints" *) + + val lp = pt l + val rp = pt r + + fun flag i = + let + val d = dist lp rp i + in + if d > 0.0 then Split.Left + else if d < 0.0 then Split.Right + else Split.Throwaway + end + val (above, below) = + (* Split.parSplit allIdx (DS.force (DS.map flag allIdx)) *) + Split.parSplit + (PureSeq.tabulate (fn i => i) (PureSeq.length pts)) + (PureSeq.tabulate flag (PureSeq.length pts)) + + (* val tm = Util.tick tm "above/below filter" *) + + val (above, below) = ForkJoin.par + (fn _ => parHull above l r, + fn _ => parHull below r l) + + (* val tm = Util.tick tm "quickhull" *) + + val hullt = + Tree.append + (Tree.append (Tree.$ l, above), + Tree.append (Tree.$ r, below)) + + val result = Tree.toPureSeq hullt + + (* val tm = Util.tick tm "flatten" *) + in + result + end + +end diff --git a/tests/pure-quickhull/Split.sml b/tests/pure-quickhull/Split.sml new file mode 100644 index 000000000..1de0a3646 --- /dev/null +++ b/tests/pure-quickhull/Split.sml @@ -0,0 +1,126 @@ +structure Split : +sig + type 'a seq + + (* val inPlace : 'a seq -> ('a -> bool) -> ('a -> bool) -> (int * int) *) + + datatype flag = Left | Right | Throwaway + val parSplit : 'a seq -> flag seq -> 'a seq * 'a seq +end = +struct + + structure A = Array + structure AS = ArraySlice + structure V = Vector + structure VS = VectorSlice + + val unsafeCast: 'a array -> 'a vector = VectorExtra.unsafeFromArray + + type 'a seq = 'a PureSeq.t +(* + fun inPlace s putLeft putRight = + let + val (a, start, n) = AS.base s + fun item i = A.sub (a, i) + fun set i x = A.update (a, i, x) + + fun growLeft ll lm rm rr = + if lm >= rm then (ll, rr) else + let + val x = item lm + in + if putRight x then + growRight ll lm rm rr + else if not (putLeft x) then + growLeft ll (lm+1) rm rr + else + (set ll x; growLeft (ll+1) (lm+1) rm rr) + end + + and growRight ll lm rm rr = + if lm >= rm then (ll, rr) else + let + val x = item (rm-1) + in + if putLeft x then + swapThenContinue ll lm rm rr + else if not (putRight x) then + growRight ll lm (rm-1) rr + else + (set (rr-1) x; growRight ll lm (rm-1) (rr-1)) + end + + and swapThenContinue ll lm rm rr = + let + val tmp = item lm + in + set ll (item (rm-1)); + set (rr-1) tmp; + growLeft (ll+1) (lm+1) (rm-1) (rr-1) + end + + val (ll, rr) = growLeft start start (start+n) (start+n) + in + (ll-start, (start+n)-rr) + end +*) + datatype flag = Left | Right | Throwaway + + fun parSplit s flags = + let + val n = PureSeq.length s + val blockSize = 10000 + val numBlocks = 1 + (n-1) div blockSize + + (* the later scan(s) appears to be faster when split into two separate + * scans, rather than doing a single scan on tuples. *) + + (* val counts = Primitives.alloc numBlocks *) + val countl = ForkJoin.alloc numBlocks + val countr = ForkJoin.alloc numBlocks + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + fun loop (cl, cr) i = + if i >= hi then + (* A.update (counts, b, (cl, cr)) *) + (A.update (countl, b, cl); A.update (countr, b, cr)) + else case PureSeq.nth flags i of + Left => loop (cl+1, cr) (i+1) + | Right => loop (cl, cr+1) (i+1) + | _ => loop (cl, cr) (i+1) + in + loop (0, 0) lo + end) + + (* val (offsets, (totl, totr)) = + Seq.scan (fn ((a,b),(c,d)) => (a+c,b+d)) (0,0) (ArraySlice.full counts) *) + val (offsetsl, totl) = PureSeq.scan op+ 0 (VS.full (unsafeCast countl)) + val (offsetsr, totr) = PureSeq.scan op+ 0 (VS.full (unsafeCast countr)) + + val left = ForkJoin.alloc totl + val right = ForkJoin.alloc totr + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + (* val (offsetl, offsetr) = Seq.nth offsets b *) + val offsetl = PureSeq.nth offsetsl b + val offsetr = PureSeq.nth offsetsr b + fun loop (cl, cr) i = + if i >= hi then () else + case PureSeq.nth flags i of + Left => (A.update (left, offsetl+cl, PureSeq.nth s i); loop (cl+1, cr) (i+1)) + | Right => (A.update (right, offsetr+cr, PureSeq.nth s i); loop (cl, cr+1) (i+1)) + | _ => loop (cl, cr) (i+1) + in + loop (0, 0) lo + end) + in + (VS.full (unsafeCast left), VS.full (unsafeCast right)) + end + +end diff --git a/tests/pure-quickhull/TreeSeq.sml b/tests/pure-quickhull/TreeSeq.sml new file mode 100644 index 000000000..6c0346f71 --- /dev/null +++ b/tests/pure-quickhull/TreeSeq.sml @@ -0,0 +1,54 @@ +structure TreeSeq = +struct + datatype 'a t = + Leaf + | Elem of 'a + | Flat of 'a PureSeq.t + | Node of int * 'a t * 'a t + + type 'a seq = 'a t + type 'a ord = 'a * 'a -> order + datatype 'a listview = NIL | CONS of 'a * 'a seq + datatype 'a treeview = EMPTY | ONE of 'a | PAIR of 'a seq * 'a seq + + exception Range + exception Size + exception NYI + + fun length Leaf = 0 + | length (Elem _) = 1 + | length (Flat s) = PureSeq.length s + | length (Node (n, _, _)) = n + + fun append (t1, t2) = Node (length t1 + length t2, t1, t2) + + fun toPureSeq t = + let + val a = ForkJoin.alloc (length t) + fun put offset t = + case t of + Leaf => () + | Elem x => Array.update (a, offset, x) + | Flat s => PureSeq.foreach s (fn (i, x) => Array.update (a, offset+i, x)) + | Node (n, l, r) => + let + fun left () = put offset l + fun right () = put (offset + length l) r + in + if n <= 4096 then + (left (); right ()) + else + (ForkJoin.par (left, right); ()) + end + in + put 0 t; + PureSeq.fromSeq (ArraySlice.full a) + end + + fun fromPureSeq v = Flat v + + fun empty () = Leaf + fun singleton x = Elem x + val $ = singleton + +end diff --git a/tests/pure-quickhull/main.sml b/tests/pure-quickhull/main.sml new file mode 100644 index 000000000..80ccca51c --- /dev/null +++ b/tests/pure-quickhull/main.sml @@ -0,0 +1,58 @@ +structure CLA = CommandLineArgs + +val resolution = 1000000 +fun randReal seed = + Real.fromInt (Util.hash seed mod resolution) / Real.fromInt resolution + +fun randPt seed = + let + val r = Math.sqrt (randReal (2*seed)) + val theta = randReal (2*seed+1) * 2.0 * Math.pi + in + (1.0 + r * Math.cos(theta), 1.0 + r * Math.sin(theta)) + end + +(* val filename = CLA.parseString "infile" "" *) +val outfile = CLA.parseString "outfile" "" +val n = CLA.parseInt "N" (1000 * 1000 * 100) + +val _ = print ("input size " ^ Int.toString n ^ "\n") + +val (inputPts, tm) = Util.getTime (fn _ => PureSeq.tabulate randPt n) +val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +val result = Benchmark.run "running quickhull" (fn _ => Quickhull.hull inputPts) + +val _ = print ("hull size " ^ Int.toString (PureSeq.length result) ^ "\n") + +fun rtos x = + if x < 0.0 then "-" ^ rtos (~x) + else Real.fmt (StringCvt.FIX (SOME 3)) x +fun pttos (x,y) = + String.concat ["(", rtos x, ",", rtos y, ")"] + +(* fun check result = + let + val correct = Checkhull.check inputPts result + in + print ("correct? " ^ + Checkhull.report inputPts result (Checkhull.check inputPts result) + ^ "\n") + end *) + +val _ = + if outfile = "" then + print ("use -outfile XXX to see result\n") + else + let + val out = TextIO.openOut outfile + fun writeln str = TextIO.output (out, str ^ "\n") + fun dump i = + if i >= PureSeq.length result then () + else (writeln (Int.toString (PureSeq.nth result i)); dump (i+1)) + in + writeln "pbbs_sequenceInt"; + dump 0; + TextIO.closeOut out + end + diff --git a/tests/pure-quickhull/pure-quickhull.mlb b/tests/pure-quickhull/pure-quickhull.mlb new file mode 100644 index 000000000..fecef8c75 --- /dev/null +++ b/tests/pure-quickhull/pure-quickhull.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +TreeSeq.sml +Split.sml +Quickhull.sml +main.sml diff --git a/tests/pure-skyline/CityGen.sml b/tests/pure-skyline/CityGen.sml new file mode 100644 index 000000000..70d0c15a2 --- /dev/null +++ b/tests/pure-skyline/CityGen.sml @@ -0,0 +1,77 @@ +structure CityGen: +sig + + (* (city n x) produces a sequence of n random buildings, seeded by x (any + * integer will do). *) + val city : int -> int -> (int * int * int) PureSeq.t + + (* (cities m n x) produces m cities, each is a sequence of at most n random + * buildings, seeded by x (any integer will do). *) + val cities : int -> int -> int -> (int * int * int) PureSeq.t PureSeq.t +end = +struct + + structure R = FastHashRand + + (* Fisher-Yates shuffle aka Knuth shuffle *) + fun shuffle s r = + let + val n = PureSeq.length s + val data = Array.tabulate (n, PureSeq.nth s) + + fun swapLoop (r, i) = + if i >= n then r + else let + val j = R.boundedInt (i, n) r + val (x, y) = (Array.sub (data, i), Array.sub (data, j)) + in + Array.update (data, i, y); + Array.update (data, j, x); + swapLoop (R.next r, i+1) + end + + val r' = swapLoop (r, 0) + in + (r', PureSeq.tabulate (fn i => Array.sub (data, i)) n) + end + + fun citySeeded n r0 = + let + val (r1, xs) = shuffle (PureSeq.tabulate (fn i => i) (2*n)) r0 + val (_, seeds) = R.splitTab (r1, n) + fun pow b e = if e <= 0 then 1 else b * pow b (e-1) + + fun makeBuilding i = + let + val xpair = (PureSeq.nth xs (2*i), PureSeq.nth xs (2*i + 1)) + val lo = Int.min xpair + val hi = Int.max xpair + val width = hi-lo + val maxHeight = Int.max (1, 2*n div width) + val maxHeight = + if maxHeight >= n then + 1 + pow (Util.log2 maxHeight) 2 + else + maxHeight + pow (Util.log2 maxHeight) 2 + val heightRange = (Int.max (1, maxHeight-(n div 100)), maxHeight+1) + val height = R.boundedInt heightRange (seeds i) + in + (lo, height, hi) + end + in + PureSeq.tabulate makeBuilding n + end + + fun city n x = citySeeded n (R.fromInt x) + + fun cities m n x = + let + val (_, rs) = R.splitTab (R.fromInt x, m) + fun ithCity i = + let val r = rs i + in citySeeded (R.boundedInt (0, n+1) r) (R.next r) + end + in PureSeq.tabulate ithCity m + end + +end diff --git a/tests/pure-skyline/FastHashRand.sml b/tests/pure-skyline/FastHashRand.sml new file mode 100644 index 000000000..205a7737d --- /dev/null +++ b/tests/pure-skyline/FastHashRand.sml @@ -0,0 +1,66 @@ +(* MUCH faster random number generation than DotMix. + * I wonder how good its randomness is? *) +structure FastHashRand = +struct + type rand = Word64.word + + val maxWord = 0wxFFFFFFFFFFFFFFFF : Word64.word + + exception FastHashRand + + fun hashWord w = + let + open Word64 + infix 2 >> infix 2 << infix 2 xorb infix 2 andb + val v = w * 0w3935559000370003845 + 0w2691343689449507681 + val v = v xorb (v >> 0w21) + val v = v xorb (v << 0w37) + val v = v xorb (v >> 0w4) + val v = v * 0w4768777513237032717 + val v = v xorb (v << 0w20) + val v = v xorb (v >> 0w41) + val v = v xorb (v << 0w5) + in + v + end + + fun fromInt x = hashWord (Word64.fromInt x) + + fun next r = hashWord r + + fun split r = (hashWord r, (hashWord (r+0w1), hashWord (r+0w2))) + + fun biasedBool (h, t) r = + let + val scaleFactor = Word64.div (maxWord, Word64.fromInt (h+t)) + in + Word64.<= (r, Word64.* (Word64.fromInt h, scaleFactor)) + end + + fun split3 _ = raise FastHashRand + fun splitTab (r, n) = + (hashWord r, fn i => hashWord (r + Word64.fromInt (i+1))) + + val intp = + case Int.precision of + SOME n => n + | NONE => (print "[ERR] int precision\n"; OS.Process.exit OS.Process.failure) + + val mask = Word64.<< (0w1, Word.fromInt (intp-1)) + + fun int r = + Word64.toIntX (Word64.andb (r, mask) - 0w1) + + fun int r = + Word64.toIntX (Word64.>> (r, Word.fromInt (64-intp+1))) + + fun boundedInt (a, b) r = a + ((int r) mod (b-a)) + + fun bool _ = raise FastHashRand + + fun biasedInt _ _ = raise FastHashRand + fun real _ = raise FastHashRand + fun boundedReal _ _ = raise FastHashRand + fun char _ = raise FastHashRand + fun boundedChar _ _ = raise FastHashRand +end diff --git a/tests/pure-skyline/Skyline.sml b/tests/pure-skyline/Skyline.sml new file mode 100644 index 000000000..0b10b41a1 --- /dev/null +++ b/tests/pure-skyline/Skyline.sml @@ -0,0 +1,56 @@ +structure Skyline = +struct + type 'a seq = 'a PureSeq.t + type skyline = (int * int) PureSeq.t + + fun singleton (l, h, r) = PureSeq.fromList [(l, h), (r, 0)] + + fun combine (sky1, sky2) = + let + val lMarked = PureSeq.map (fn (x, y) => (x, SOME y, NONE)) sky1 + val rMarked = PureSeq.map (fn (x, y) => (x, NONE, SOME y)) sky2 + + fun cmp ((x1, _, _), (x2, _, _)) = Int.compare (x1, x2) + val merged = PureSeq.merge cmp (lMarked, rMarked) + + fun copy (a, b) = case b of SOME _ => b | NONE => a + fun copyFused ((x1, yl1, yr1), (x2, yl2, yr2)) = + (x2, copy (yl1, yl2), copy (yr1, yr2)) + + val allHeights = PureSeq.scanIncl copyFused (0,NONE,NONE) merged + + fun squish (x, y1, y2) = + (x, Int.max (Option.getOpt (y1, 0), Option.getOpt (y2, 0))) + val sky = PureSeq.map squish allHeights + in + sky + end + + fun skyline g bs = + let + fun skyline' bs = + case PureSeq.length bs of + 0 => PureSeq.empty () + | 1 => singleton (PureSeq.nth bs 0) + | n => + let + val half = n div 2 + val sfL = fn _ => skyline' (PureSeq.take bs half) + val sfR = fn _ => skyline' (PureSeq.drop bs half) + in + if PureSeq.length bs <= g then + combine (sfL (), sfR ()) + else + combine (ForkJoin.par (sfL, sfR)) + end + + val sky = skyline' bs + + fun isUnique (i, (x, h)) = + i = 0 orelse let val (_, prevh) = PureSeq.nth sky (i-1) in h <> prevh end + val sky = PureSeq.filterIdx isUnique sky + in + sky + end + +end diff --git a/tests/pure-skyline/main.sml b/tests/pure-skyline/main.sml new file mode 100644 index 000000000..b205cf8e1 --- /dev/null +++ b/tests/pure-skyline/main.sml @@ -0,0 +1,102 @@ +structure CLA = CommandLineArgs +structure Gen = CityGen + +(* +functor S (Sky : SKYLINE where type skyline = (int * int) Seq.t) = +struct + open Sky + fun skyline bs = + case Seq.splitMid bs of + Seq.EMPTY => Seq.empty () + | Seq.ONE b => singleton b + | Seq.PAIR (l, r) => + let + fun sl _ = skyline l + fun sr _ = skyline r + val (l', r') = + if Seq.length bs <= 1000 + then (sl (), sr ()) + else Primitives.par (sl, sr) + in + combine (l', r') + end +end + +structure Stu = S (MkSkyline (structure Seq = Seq)) +structure Ref = S (MkRefSkyline (structure Seq = Seq)) +*) + +fun pairEq ((x1, y1), (x2, y2)) = (x1 = x2 andalso y1 = y2) + +fun skylinesEq (s1, s2) = + PureSeq.length s1 = PureSeq.length s2 andalso + PureSeq.reduce (fn (a,b) => a andalso b) true + (PureSeq.tabulate (fn i => pairEq (PureSeq.nth s1 i, PureSeq.nth s2 i)) (PureSeq.length s1)) + +val size = CLA.parseInt "size" 1000000 +val seed = CLA.parseInt "seed" 15210 +val grain = CLA.parseInt "grain" 1000 +val output = CLA.parseString "output" "" + +(* ensure newline at end of string *) +fun println s = + let + val needsNewline = + String.size s = 0 orelse String.sub (s, String.size s - 1) <> #"\n" + in + print (if needsNewline then s ^ "\n" else s) + end + +val _ = println ("size " ^ Int.toString size) +val _ = println ("seed " ^ Int.toString seed) +val _ = println ("grain " ^ Int.toString grain) + +val (input, tm) = Util.getTime (fn _ => Gen.city size seed) +val _ = println ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +val sky = Benchmark.run "skyline" (fn _ => Skyline.skyline grain input) +val _ = print ("result-len " ^ Int.toString (PureSeq.length sky) ^ "\n") + +val _ = + if output = "" then + print ("use -output XXX.ppm to see result\n") + else + let + val (xMin, _) = PureSeq.nth sky 0 + val (xMax, _) = PureSeq.nth sky (PureSeq.length sky - 1) + val yMax = PureSeq.reduce Int.max 0 (PureSeq.map (fn (_,y) => y) sky) + val _ = print ("xMin " ^ Int.toString xMin ^ "\n") + val _ = print ("xMax " ^ Int.toString xMax ^ "\n") + val _ = print ("yMax " ^ Int.toString yMax ^ "\n") + + val width = 1000 + val height = 250 + + val padding = 20 + + fun col x = + padding + width * (x - xMin) div (1 + xMax - xMin) + fun row y = + padding + height - 1 - (height * y div (1 + yMax)) + + val width' = 2*padding + width + val height' = padding + height + val image = Seq.tabulate (fn _ => Color.white) (width' * height') + + val _ = PureSeq.foreach sky (fn (idx, (x, y)) => + if idx >= PureSeq.length sky - 1 then () else + let + val (x', _) = PureSeq.nth sky (idx+1) + + val ihi = row y + val jlo = col x + val jhi = Int.max (col x + 1, col x') + in + Util.for (ihi, height') (fn i => + Util.for (jlo, jhi) (fn j => + ArraySlice.update (image, i*width' + j, Color.black))) + end) + in + PPM.write output {width=width', height=height', data=image}; + print ("wrote output to " ^ output ^ "\n") + end diff --git a/tests/pure-skyline/pure-skyline.mlb b/tests/pure-skyline/pure-skyline.mlb new file mode 100644 index 000000000..00f8c074a --- /dev/null +++ b/tests/pure-skyline/pure-skyline.mlb @@ -0,0 +1,6 @@ +../mpllib/sources.$(COMPAT).mlb +FastHashRand.sml +CityGen.sml +Skyline.sml +main.sml + diff --git a/tests/quickhull/MkOptSplit.sml b/tests/quickhull/MkOptSplit.sml new file mode 100644 index 000000000..0a68264ca --- /dev/null +++ b/tests/quickhull/MkOptSplit.sml @@ -0,0 +1,123 @@ +functor MkSplit (Seq: SEQUENCE) : +sig + type 'a seq = 'a Seq.t + type 'a aseq = 'a ArraySequence.t + + datatype flag = Left | Right | Throwaway + val parSplit: 'a seq -> flag seq -> 'a aseq * 'a aseq +end = +struct + + structure A = Array + structure AS = ArraySlice + structure ASeq = ArraySequence + + type 'a seq = 'a Seq.t + type 'a aseq = 'a ASeq.t + + fun inPlace (s: 'a ASeq.t) putLeft putRight = + let + val (a, start, n) = AS.base s + fun item i = Array.sub (a, i) + fun set i x = Array.update (a, i, x) + + fun growLeft ll lm rm rr = + if lm >= rm then (ll, rr) else + let + val x = item lm + in + if putRight x then + growRight ll lm rm rr + else if not (putLeft x) then + growLeft ll (lm+1) rm rr + else + (set ll x; growLeft (ll+1) (lm+1) rm rr) + end + + and growRight ll lm rm rr = + if lm >= rm then (ll, rr) else + let + val x = item (rm-1) + in + if putLeft x then + swapThenContinue ll lm rm rr + else if not (putRight x) then + growRight ll lm (rm-1) rr + else + (set (rr-1) x; growRight ll lm (rm-1) (rr-1)) + end + + and swapThenContinue ll lm rm rr = + let + val tmp = item lm + in + set ll (item (rm-1)); + set (rr-1) tmp; + growLeft (ll+1) (lm+1) (rm-1) (rr-1) + end + + val (ll, rr) = growLeft start start (start+n) (start+n) + in + (ll-start, (start+n)-rr) + end + + datatype flag = Left | Right | Throwaway + + fun parSplit s flags = + let + val n = Seq.length s + val blockSize = 10000 + val numBlocks = 1 + (n-1) div blockSize + + (* the later scan(s) appears to be faster when split into two separate + * scans, rather than doing a single scan on tuples. *) + + (* val counts = Primitives.alloc numBlocks *) + val countl = ForkJoin.alloc numBlocks + val countr = ForkJoin.alloc numBlocks + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + fun loop (cl, cr) i = + if i >= hi then + (* Array.update (counts, b, (cl, cr)) *) + (Array.update (countl, b, cl); Array.update (countr, b, cr)) + else case Seq.nth flags i of + Left => loop (cl+1, cr) (i+1) + | Right => loop (cl, cr+1) (i+1) + | _ => loop (cl, cr) (i+1) + in + loop (0, 0) lo + end) + + (* val (offsets, (totl, totr)) = + Seq.scan (fn ((a,b),(c,d)) => (a+c,b+d)) (0,0) (ArraySlice.full counts) *) + val (offsetsl, totl) = ASeq.scan op+ 0 (ArraySlice.full countl) + val (offsetsr, totr) = ASeq.scan op+ 0 (ArraySlice.full countr) + + val left = ForkJoin.alloc totl + val right = ForkJoin.alloc totr + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + (* val (offsetl, offsetr) = Seq.nth offsets b *) + val offsetl = ASeq.nth offsetsl b + val offsetr = ASeq.nth offsetsr b + fun loop (cl, cr) i = + if i >= hi then () else + case Seq.nth flags i of + Left => (Array.update (left, offsetl+cl, Seq.nth s i); loop (cl+1, cr) (i+1)) + | Right => (Array.update (right, offsetr+cr, Seq.nth s i); loop (cl, cr+1) (i+1)) + | _ => loop (cl, cr) (i+1) + in + loop (0, 0) lo + end) + in + (ArraySlice.full left, ArraySlice.full right) + end + +end diff --git a/tests/quickhull/MkPurishSplit.sml b/tests/quickhull/MkPurishSplit.sml new file mode 100644 index 000000000..00fabf674 --- /dev/null +++ b/tests/quickhull/MkPurishSplit.sml @@ -0,0 +1,46 @@ +functor MkPurishSplit (Seq: SEQUENCE) : +sig + type 'a seq = 'a Seq.t + type 'a aseq = 'a ArraySequence.t + + datatype flag = Left | Right | Throwaway + val parSplit: 'a seq -> flag seq -> 'a aseq * 'a aseq +end = +struct + + structure A = Array + structure AS = ArraySlice + structure ASeq = ArraySequence + + type 'a seq = 'a Seq.t + type 'a aseq = 'a ASeq.t + + datatype flag = Left | Right | Throwaway + + fun parSplit s flags = + let + fun countFlag i = + case Seq.nth flags i of + Left => (1, 0) + | Right => (0, 1) + | Throwaway => (0, 0) + + fun add ((a, b), (c, d)) = (a+c, b+d) + + val n = Seq.length s + val (offsets, (tl, tr)) = Seq.scan add (0, 0) (Seq.tabulate countFlag n) + + val left = ForkJoin.alloc tl + val right = ForkJoin.alloc tr + in + Seq.applyIdx offsets (fn (i, (offl, offr)) => + case Seq.nth flags i of + Left => Array.update (left, offl, Seq.nth s i) + | Right => Array.update (right, offr, Seq.nth s i) + | _ => () + ); + + (AS.full left, AS.full right) + end + +end diff --git a/tests/quickhull/MkQuickhull.sml b/tests/quickhull/MkQuickhull.sml new file mode 100644 index 000000000..c1bccd7a3 --- /dev/null +++ b/tests/quickhull/MkQuickhull.sml @@ -0,0 +1,109 @@ +functor MkQuickhull (Seq: SEQUENCE): +sig + type 'a aseq = 'a ArraySequence.t + val hull: (real * real) aseq -> int aseq +end = +struct + + structure AS = ArraySlice + structure ASeq = ArraySequence + type 'a aseq = 'a ASeq.t + + structure G = Geometry2D + structure Tree = TreeSeq + + structure Split = MkSplit (Seq) + + fun hull pts = + let + fun pt i = ASeq.nth pts i + fun dist p q i = G.Point.triArea (p, q, pt i) + fun max ((i, di), (j, dj)) = + if di > dj then (i, di) else (j, dj) + fun x i = #1 (pt i) + + fun aboveLine p q i = (dist p q i > 0.0) + + fun parHull idxs l r = + if ASeq.length idxs < 2 then + Tree.fromArraySeq idxs + else + let + val lp = pt l + val rp = pt r + fun d i = dist lp rp i + + val idxs = Seq.fromArraySeq idxs + + val distances = Seq.map (fn i => (i, d i)) idxs + val (mid, _) = Seq.reduce max (~1, Real.negInf) distances + + val midp = pt mid + + fun flag i = + if aboveLine lp midp i then Split.Left + else if aboveLine midp rp i then Split.Right + else Split.Throwaway + val (left, right) = + Split.parSplit idxs (Seq.force (Seq.map flag idxs)) + + fun doLeft () = parHull left l mid + fun doRight () = parHull right mid r + val (leftHull, rightHull) = + if ASeq.length left + ASeq.length right <= 2048 + then (doLeft (), doRight ()) + else ForkJoin.par (doLeft, doRight) + in + Tree.append (leftHull, + (Tree.append (Tree.$ mid, rightHull))) + end + + (* val tm = Util.startTiming () *) + + val allIdx = Seq.tabulate (fn i => i) (ASeq.length pts) + + (* This is faster than doing two reduces *) + val (l, r) = Seq.reduce + (fn ((l1, r1), (l2, r2)) => + (if x l1 < x l2 then l1 else l2, + if x r1 > x r2 then r1 else r2)) + (0, 0) + (Seq.map (fn i => (i, i)) allIdx) + + (* val tm = Util.tick tm "endpoints" *) + + val lp = pt l + val rp = pt r + + fun flag i = + let + val d = dist lp rp i + in + if d > 0.0 then Split.Left + else if d < 0.0 then Split.Right + else Split.Throwaway + end + val (above, below) = + Split.parSplit allIdx (Seq.force (Seq.map flag allIdx)) + + (* val tm = Util.tick tm "above/below filter" *) + + val (above, below) = ForkJoin.par + (fn _ => parHull above l r, + fn _ => parHull below r l) + + (* val tm = Util.tick tm "quickhull" *) + + val hullt = + Tree.append + (Tree.append (Tree.$ l, above), + Tree.append (Tree.$ r, below)) + + val result = Tree.toArraySeq hullt + + (* val tm = Util.tick tm "flatten" *) + in + result + end + +end diff --git a/tests/quickhull/ParseFile.sml b/tests/quickhull/ParseFile.sml new file mode 100644 index 000000000..c7eb4e101 --- /dev/null +++ b/tests/quickhull/ParseFile.sml @@ -0,0 +1,190 @@ +(** SAM_NOTE: copy/pasted... some repetition here with Parse. *) +structure ParseFile = +struct + + structure RF = ReadFile + structure Seq = ArraySequence + structure DS = OldDelayedSeq + + fun tokens (f: char -> bool) (cs: char Seq.t) : (char DS.t) DS.t = + let + val n = Seq.length cs + val s = DS.tabulate (Seq.nth cs) n + val indices = DS.tabulate (fn i => i) (n+1) + fun check i = + if (i = n) then not (f(DS.nth s (n-1))) + else if (i = 0) then not (f(DS.nth s 0)) + else let val i1 = f (DS.nth s i) + val i2 = f (DS.nth s (i-1)) + in (i1 andalso not i2) orelse (i2 andalso not i1) end + val ids = DS.filter check indices + val res = DS.tabulate (fn i => + let val (start, e) = (DS.nth ids (2*i), DS.nth ids (2*i+1)) + in DS.tabulate (fn i => Seq.nth cs (start+i)) (e - start) + end) + ((DS.length ids) div 2) + in + res + end + + fun eqStr str (chars : char DS.t) = + let + val n = String.size str + fun checkFrom i = + i >= n orelse + (String.sub (str, i) = DS.nth chars i andalso checkFrom (i+1)) + in + DS.length chars = n + andalso + checkFrom 0 + end + + fun parseDigit char = + let + val code = Char.ord char + val code0 = Char.ord #"0" + val code9 = Char.ord #"9" + in + if code < code0 orelse code9 < code then + NONE + else + SOME (code - code0) + end + + (* This implementation doesn't work with mpl :( + * Need to fix the basis library... *) + (* + fun parseReal chars = + let + val str = CharVector.tabulate (DS.length chars, DS.nth chars) + in + Real.fromString str + end + *) + + fun parseInt (chars : char DS.t) = + let + val n = DS.length chars + fun c i = DS.nth chars i + + fun build x i = + if i >= n then SOME x else + case c i of + #"," => build x (i+1) + | #"_" => build x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => build (x * 10 + dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1) (build 0 1) + else if (c 0 = #"+") then + build 0 1 + else + build 0 0 + end + + fun parseReal (chars : char DS.t) = + let + val n = DS.length chars + fun c i = DS.nth chars i + + fun buildAfterE x i = + let + val chars' = DS.subseq chars (i, n-i) + in + Option.map (fn e => x * Math.pow (10.0, Real.fromInt e)) + (parseInt chars') + end + + fun buildAfterPoint m x i = + if i >= n then SOME x else + case c i of + #"," => buildAfterPoint m x (i+1) + | #"_" => buildAfterPoint m x (i+1) + | #"." => NONE + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildAfterPoint (m * 0.1) (x + m * (Real.fromInt dig)) (i+1) + + fun buildBeforePoint x i = + if i >= n then SOME x else + case c i of + #"," => buildBeforePoint x (i+1) + | #"_" => buildBeforePoint x (i+1) + | #"." => buildAfterPoint 0.1 x (i+1) + | #"e" => buildAfterE x (i+1) + | #"E" => buildAfterE x (i+1) + | cc => + case parseDigit cc of + NONE => NONE + | SOME dig => buildBeforePoint (x * 10.0 + Real.fromInt dig) (i+1) + in + if n = 0 then NONE + else if (c 0 = #"-" orelse c 0 = #"~") then + Option.map (fn x => x * ~1.0) (buildBeforePoint 0.0 1) + else + buildBeforePoint 0.0 0 + end + + fun readSequencePoint2d filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequencePoint2d" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun r i = Option.valOf (parseReal (tok (1 + i))) + + fun pt i = + (r (2*i), r (2*i+1)) + handle e => raise Fail ("error parsing point " ^ Int.toString i ^ " (" ^ exnMessage e ^ ")") + + val result = Seq.tabulate pt (n div 2) + in + result + end + + fun readSequenceInt filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequenceInt" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun p i = + Option.valOf (parseInt (tok (1 + i))) + handle e => raise Fail ("error parsing integer " ^ Int.toString i) + in + Seq.tabulate p n + end + + fun readSequenceReal filename = + let + val toks = tokens Char.isSpace (RF.contentsSeq filename) + fun tok i = DS.nth toks i + val _ = + if eqStr "pbbs_sequenceDouble" (tok 0) then () + else raise Fail (filename ^ " wrong file type") + + val n = DS.length toks - 1 + + fun p i = + Option.valOf (parseReal (tok (1 + i))) + handle e => raise Fail ("error parsing double value " ^ Int.toString i) + in + Seq.tabulate p n + end + +end diff --git a/tests/quickhull/Quickhull.sml b/tests/quickhull/Quickhull.sml new file mode 100644 index 000000000..fbe8f83d0 --- /dev/null +++ b/tests/quickhull/Quickhull.sml @@ -0,0 +1,119 @@ +structure Quickhull : +sig + val hull : (real * real) Seq.t -> int Seq.t +end = +struct + + structure AS = ArraySlice + structure G = Geometry2D + structure Tree = TreeSeq + + fun hull pts = + let + fun pt i = Seq.nth pts i + fun dist p q i = G.Point.triArea (p, q, pt i) + fun max ((i, di), (j, dj)) = + if di > dj then (i, di) else (j, dj) + fun x i = #1 (pt i) + + fun aboveLine p q i = (dist p q i > 0.0) + + fun parHull idxs l r = + if Seq.length idxs < 2 then + Tree.fromArraySeq idxs + (* if DS.length idxs <= 2048 then + seqHull idxs l r *) + else + let + val lp = pt l + val rp = pt r + fun d i = dist lp rp i + + (* val idxs = DS.fromArraySeq idxs *) + + val (mid, _) = SeqBasis.reduce 10000 max (~1, Real.negInf) + (0, Seq.length idxs) (fn i => (Seq.nth idxs i, d (Seq.nth idxs i))) + (* val distances = DS.map (fn i => (i, d i)) idxs + val (mid, _) = DS.reduce max (~1, Real.negInf) distances *) + + val midp = pt mid + + fun flag i = + if aboveLine lp midp i then Split.Left + else if aboveLine midp rp i then Split.Right + else Split.Throwaway + val (left, right) = + Split.parSplit idxs (Seq.map flag idxs) + (* (DS.force (DS.map flag idxs)) *) + + fun doLeft () = parHull left l mid + fun doRight () = parHull right mid r + val (leftHull, rightHull) = + if Seq.length left + Seq.length right <= 2048 + then (doLeft (), doRight ()) + else ForkJoin.par (doLeft, doRight) + in + Tree.append (leftHull, + (Tree.append (Tree.$ mid, rightHull))) + end + + (* val tm = Util.startTiming () *) + + (* val allIdx = DS.tabulate (fn i => i) (Seq.length pts) *) + + (* This is faster than doing two reduces *) + (* val (l, r) = DS.reduce + (fn ((l1, r1), (l2, r2)) => + (if x l1 < x l2 then l1 else l2, + if x r1 > x r2 then r1 else r2)) + (0, 0) + (DS.map (fn i => (i, i)) allIdx) *) + + val (l, r) = SeqBasis.reduce 10000 + (fn ((l1, r1), (l2, r2)) => + (if x l1 < x l2 then l1 else l2, + if x r1 > x r2 then r1 else r2)) + (0, 0) + (0, Seq.length pts) + (fn i => (i, i)) + + (* val tm = Util.tick tm "endpoints" *) + + val lp = pt l + val rp = pt r + + fun flag i = + let + val d = dist lp rp i + in + if d > 0.0 then Split.Left + else if d < 0.0 then Split.Right + else Split.Throwaway + end + val (above, below) = + (* Split.parSplit allIdx (DS.force (DS.map flag allIdx)) *) + Split.parSplit + (Seq.tabulate (fn i => i) (Seq.length pts)) + (Seq.tabulate flag (Seq.length pts)) + + (* val tm = Util.tick tm "above/below filter" *) + + val (above, below) = ForkJoin.par + (fn _ => parHull above l r, + fn _ => parHull below r l) + + (* val tm = Util.tick tm "quickhull" *) + + val hullt = + Tree.append + (Tree.append (Tree.$ l, above), + Tree.append (Tree.$ r, below)) + + val result = Tree.toArraySeq hullt + + (* val tm = Util.tick tm "flatten" *) + in + result + end + +end diff --git a/tests/quickhull/Split.sml b/tests/quickhull/Split.sml new file mode 100644 index 000000000..0d3efec8e --- /dev/null +++ b/tests/quickhull/Split.sml @@ -0,0 +1,122 @@ +structure Split : +sig + type 'a seq + + val inPlace : 'a seq -> ('a -> bool) -> ('a -> bool) -> (int * int) + + datatype flag = Left | Right | Throwaway + val parSplit : 'a seq -> flag seq -> 'a seq * 'a seq +end = +struct + + structure A = Array + structure AS = ArraySlice + + type 'a seq = 'a Seq.t + + fun inPlace s putLeft putRight = + let + val (a, start, n) = AS.base s + fun item i = A.sub (a, i) + fun set i x = A.update (a, i, x) + + fun growLeft ll lm rm rr = + if lm >= rm then (ll, rr) else + let + val x = item lm + in + if putRight x then + growRight ll lm rm rr + else if not (putLeft x) then + growLeft ll (lm+1) rm rr + else + (set ll x; growLeft (ll+1) (lm+1) rm rr) + end + + and growRight ll lm rm rr = + if lm >= rm then (ll, rr) else + let + val x = item (rm-1) + in + if putLeft x then + swapThenContinue ll lm rm rr + else if not (putRight x) then + growRight ll lm (rm-1) rr + else + (set (rr-1) x; growRight ll lm (rm-1) (rr-1)) + end + + and swapThenContinue ll lm rm rr = + let + val tmp = item lm + in + set ll (item (rm-1)); + set (rr-1) tmp; + growLeft (ll+1) (lm+1) (rm-1) (rr-1) + end + + val (ll, rr) = growLeft start start (start+n) (start+n) + in + (ll-start, (start+n)-rr) + end + + datatype flag = Left | Right | Throwaway + + fun parSplit s flags = + let + val n = Seq.length s + val blockSize = 10000 + val numBlocks = 1 + (n-1) div blockSize + + (* the later scan(s) appears to be faster when split into two separate + * scans, rather than doing a single scan on tuples. *) + + (* val counts = Primitives.alloc numBlocks *) + val countl = ForkJoin.alloc numBlocks + val countr = ForkJoin.alloc numBlocks + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + fun loop (cl, cr) i = + if i >= hi then + (* A.update (counts, b, (cl, cr)) *) + (A.update (countl, b, cl); A.update (countr, b, cr)) + else case Seq.nth flags i of + Left => loop (cl+1, cr) (i+1) + | Right => loop (cl, cr+1) (i+1) + | _ => loop (cl, cr) (i+1) + in + loop (0, 0) lo + end) + + (* val (offsets, (totl, totr)) = + Seq.scan (fn ((a,b),(c,d)) => (a+c,b+d)) (0,0) (ArraySlice.full counts) *) + val (offsetsl, totl) = Seq.scan op+ 0 (AS.full countl) + val (offsetsr, totr) = Seq.scan op+ 0 (AS.full countr) + + val left = ForkJoin.alloc totl + val right = ForkJoin.alloc totr + + val _ = ForkJoin.parfor 1 (0, numBlocks) (fn b => + let + val lo = b * blockSize + val hi = Int.min (lo + blockSize, n) + (* val (offsetl, offsetr) = Seq.nth offsets b *) + val offsetl = Seq.nth offsetsl b + val offsetr = Seq.nth offsetsr b + fun loop (cl, cr) i = + if i >= hi then () else + case Seq.nth flags i of + Left => (A.update (left, offsetl+cl, Seq.nth s i); loop (cl+1, cr) (i+1)) + | Right => (A.update (right, offsetr+cr, Seq.nth s i); loop (cl, cr+1) (i+1)) + | _ => loop (cl, cr) (i+1) + in + loop (0, 0) lo + end) + in + (AS.full left, AS.full right) + end + +end diff --git a/tests/quickhull/TreeSeq.sml b/tests/quickhull/TreeSeq.sml new file mode 100644 index 000000000..766667e71 --- /dev/null +++ b/tests/quickhull/TreeSeq.sml @@ -0,0 +1,54 @@ +structure TreeSeq = +struct + datatype 'a t = + Leaf + | Elem of 'a + | Flat of 'a Seq.t + | Node of int * 'a t * 'a t + + type 'a seq = 'a t + type 'a ord = 'a * 'a -> order + datatype 'a listview = NIL | CONS of 'a * 'a seq + datatype 'a treeview = EMPTY | ONE of 'a | PAIR of 'a seq * 'a seq + + exception Range + exception Size + exception NYI + + fun length Leaf = 0 + | length (Elem _) = 1 + | length (Flat s) = Seq.length s + | length (Node (n, _, _)) = n + + fun append (t1, t2) = Node (length t1 + length t2, t1, t2) + + fun toArraySeq t = + let + val a = ForkJoin.alloc (length t) + fun put offset t = + case t of + Leaf => () + | Elem x => Array.update (a, offset, x) + | Flat s => Seq.foreach s (fn (i, x) => Array.update (a, offset+i, x)) + | Node (n, l, r) => + let + fun left () = put offset l + fun right () = put (offset + length l) r + in + if n <= 4096 then + (left (); right ()) + else + (ForkJoin.par (left, right); ()) + end + in + put 0 t; + ArraySlice.full a + end + + fun fromArraySeq a = Flat a + + fun empty () = Leaf + fun singleton x = Elem x + val $ = singleton + +end diff --git a/tests/quickhull/main.sml b/tests/quickhull/main.sml new file mode 100644 index 000000000..f007de715 --- /dev/null +++ b/tests/quickhull/main.sml @@ -0,0 +1,86 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure Quickhull = MkQuickhull(OldDelayedSeq) + +val resolution = 1000000 +fun randReal seed = + Real.fromInt (Util.hash seed mod resolution) / Real.fromInt resolution + +fun randPt seed = + let + val r = Math.sqrt (randReal (2*seed)) + val theta = randReal (2*seed+1) * 2.0 * Math.pi + in + (1.0 + r * Math.cos(theta), 1.0 + r * Math.sin(theta)) + end + +val filename = CLA.parseString "infile" "" +val outfile = CLA.parseString "outfile" "" +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val doCheck = CLA.parseFlag "check" + +(* This silly thing helps ensure good placement, by + * forcing points to be reallocated more adjacent. + * It's a no-op, but gives us as much as 2x time + * improvement (!) + *) +fun swap pts = Seq.map (fn (x, y) => (y, x)) pts +fun compactify pts = swap (swap pts) + +val inputPts = + case filename of + "" => Seq.tabulate randPt n + | _ => compactify (ParseFile.readSequencePoint2d filename) + +val n = Seq.length inputPts + +fun task () = + Quickhull.hull inputPts + +fun rtos x = + if x < 0.0 then "-" ^ rtos (~x) + else Real.fmt (StringCvt.FIX (SOME 3)) x +fun pttos (x,y) = + String.concat ["(", rtos x, ",", rtos y, ")"] + +(* +fun check result = + if not doCheck then () else + let + val correct = Checkhull.check inputPts result + in + print ("correct? " ^ + Checkhull.report inputPts result (Checkhull.check inputPts result) + ^ "\n") + end +*) + +(* val _ = + (writeln "pbbs_sequencePoint2d"; dump inputPts 0; OS.Process.exit OS.Process.success) *) + +val result = Benchmark.run "quickhull" task +val _ = print ("hull size " ^ Int.toString (Seq.length result) ^ "\n") +(* val _ = check result *) + +val _ = + if outfile = "" then () else + let + val out = TextIO.openOut outfile + fun writeln str = TextIO.output (out, str ^ "\n") + fun dump i = + if i >= Seq.length result then () + else (writeln (Int.toString (Seq.nth result i)); dump (i+1)) + in + writeln "pbbs_sequenceInt"; + dump 0; + TextIO.closeOut out + end + + +(* fun dumpPt (x, y) = writeln (rtos x ^ " " ^ rtos y) *) +(* fun dump pts i = + if i >= Seq.length pts then () + else (dumpPt (Seq.nth pts i); dump pts (i+1)) *) +(* val hullPts = Seq.map (Seq.nth inputPts) result *) +(* dump hullPts 0 *) diff --git a/tests/quickhull/quickhull.mlb b/tests/quickhull/quickhull.mlb new file mode 100644 index 000000000..1f92f9fd9 --- /dev/null +++ b/tests/quickhull/quickhull.mlb @@ -0,0 +1,18 @@ +../mpllib/sources.$(COMPAT).mlb +ParseFile.sml +TreeSeq.sml + +(* +Split.sml +Quickhull.sml +*) + + +local + MkPurishSplit.sml +in + functor MkSplit = MkPurishSplit +end +MkQuickhull.sml + +main.sml diff --git a/tests/random/random.mlb b/tests/random/random.mlb new file mode 100644 index 000000000..b85468778 --- /dev/null +++ b/tests/random/random.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +random.sml diff --git a/tests/random/random.sml b/tests/random/random.sml new file mode 100644 index 000000000..117694042 --- /dev/null +++ b/tests/random/random.sml @@ -0,0 +1,33 @@ +structure CLA = CommandLineArgs + +val grain = CLA.parseInt "grain" 10000 + +(* build an array in parallel with elements f(i) for each 0 <= i < n *) +fun tabulate (n, f) = + let + val arr = ForkJoin.alloc n + in + ForkJoin.parfor grain (0, n) (fn i => Array.update (arr, i, f i)); + arr + end + +(* generate the ith element with a hash function *) +fun gen seed i = Util.hash64 (Word64.xorb (Word64.fromInt i, seed)) + +(* ========================================================================== + * parse command-line arguments and run + *) + +val n = CLA.parseInt "N" (1000 * 1000 * 1000) +val seed = CLA.parseInt "seed" 0 + +val _ = print ("tabulate " ^ Int.toString n ^ " pseudo-random 64-bit words\n") +val _ = print ("seed " ^ Int.toString seed ^ "\n") + +val seed' = Util.hash64 (Word64.fromInt seed) + +val result = Benchmark.run "tabulating" (fn _ => tabulate (n, gen seed')) + +fun str x = Word64.fmt StringCvt.HEX x +val _ = print ("result " ^ Util.summarizeArray 3 str result ^ "\n") + diff --git a/tests/range-tree/RangeTree.sml b/tests/range-tree/RangeTree.sml new file mode 100644 index 000000000..4ee1dc749 --- /dev/null +++ b/tests/range-tree/RangeTree.sml @@ -0,0 +1,59 @@ +signature RANGE_TREE = +sig + type rt + type point = int * int + type weight = int + + val build : (point * weight) Seq.t -> int -> rt + val query : rt -> point -> point -> weight + val print : rt -> unit +end + +structure RangeTree : RANGE_TREE = +struct + type point = int * int + type weight = int + + structure IRTree : Aug = + struct + type key = point + type value = weight + type aug = weight + val compare = fn (p1 : point, p2 : point) => Int.compare (#2 p1, #2 p2) + val g = fn (x, y) => y + val f = fn (x, y) => x + y + val id = 0 + val balance = WB 0.28 + fun debug (k, v, a) = " " + end + + structure InnerRangeTree = PAM(IRTree) + + structure ORTree : Aug = + struct + type key = point + type value = weight + type aug = InnerRangeTree.am + val compare = fn (p1 : key, p2 : key) => Int.compare (#1 p1, #1 p2) + val g = fn (x, y) => InnerRangeTree.singleton x y + val f = fn (x, y) => InnerRangeTree.union x y (Int.+) + val id = InnerRangeTree.empty () + val balance = WB 0.28 + fun debug (k, v, a) = (InnerRangeTree.print_tree a ""; Int.toString v) + end + + structure OuterRangeTree = PAM(ORTree) + type rt = OuterRangeTree.am + + fun build s n = OuterRangeTree.build s 0 n + + fun query r p1 p2 = + let + fun g' ri = InnerRangeTree.aug_range ri p1 p2 + in + OuterRangeTree.aug_project g' (Int.+) r p1 p2 + end + + fun print r = OuterRangeTree.print_tree r " " +end + diff --git a/tests/range-tree/main.sml b/tests/range-tree/main.sml new file mode 100644 index 000000000..bfdf82065 --- /dev/null +++ b/tests/range-tree/main.sml @@ -0,0 +1,103 @@ +structure CLA = CommandLineArgs + +val q = CLA.parseInt "q" 10000000 +val n = CLA.parseInt "n" 10000000 + +val max_size = 2147483647 + +fun randRange i j seed = + i + Word64.toInt + (Word64.mod (Util.hash64 (Word64.fromInt seed), Word64.fromInt (j - i))) + +fun randPt seed = + let + val p = randRange 0 max_size seed + in + (randRange 0 max_size seed, randRange 0 max_size (seed+1)) + end + +(* copied from PAM: range_utils.h *) +fun generate_points n seed = + let + fun rand_coordinate i = randRange 0 max_size i + val rand_numbers = Seq.tabulate rand_coordinate (3*n) + val get = Seq.nth rand_numbers + val points = Seq.tabulate (fn i => ((get i, get (i + n)), get (i + 2*n))) n + in + points + end + +val (tree, tm) = Util.getTime (fn _ => + RangeTree.build (generate_points n 0) n) +val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +fun query i = + let + val p1 = randPt (4*i) + val p2 = randPt (4*i + 2) + in + RangeTree.query tree p1 p2 + end + +fun bench () = SeqBasis.tabulate 100 (0, q) query +val result = Benchmark.run "querying range tree" bench +val _ = Util.summarizeArray 10 Int.toString result + +(* + +fun run_rounds f r = + let + fun round_rec i diff = + if i = 0 then diff + else + let + val (t0, t1, _) = f() + val new_diff = Time.- (t1, t0) + val _ = print ("round " ^ (Int.toString (r - i + 1)) ^ " in " ^ Time.fmt 4 (new_diff) ^ "s\n") + in + round_rec (i - 1) (Time.+ (diff, new_diff)) + end + in + round_rec r Time.zeroTime + end + +fun eval_build_range_tree n = + let + val points = generate_points n 0 + val t0 = Time.now () + val rt = RangeTree.build points n + val t1 = Time.now () + in + (t0, t1, rt) + end + +fun eval_queries_range_tree rt q = + let + val max_size = 2147483647 + val pl = generate_points q 0 + val pr = generate_points q 0 + val t0 = Time.now() + val r = SeqBasis.tabulate 100 (0, q) (fn i => RangeTree.query rt (#1 (Seq.nth pl i)) (#1 (Seq.nth pr i))) + val t1 = Time.now() + in + (t0, t1, 0) + end + +val query_size = CommandLineArgs.parseInt "q" 10000000 +val size = CommandLineArgs.parseInt "n" 10000000 +val rep = CommandLineArgs.parseInt "repeat" 1 + +val diff = + if query_size = 0 then + run_rounds (fn _ => eval_build_range_tree size) rep + else + let + val curr = eval_queries_range_tree (#3 (eval_build_range_tree size)) + in + run_rounds (fn _ => curr query_size) rep + end + +val _ = print ("total " ^ Time.fmt 4 diff ^ "s\n") +val avg = Time.toReal diff / (Real.fromInt rep) +val _ = print ("average " ^ Real.fmt (StringCvt.FIX (SOME 4)) avg ^ "s\n") +*) diff --git a/tests/range-tree/range-tree.mlb b/tests/range-tree/range-tree.mlb new file mode 100644 index 000000000..fa61df8c2 --- /dev/null +++ b/tests/range-tree/range-tree.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +RangeTree.sml +main.sml diff --git a/tests/raytracer/main.sml b/tests/raytracer/main.sml new file mode 100644 index 000000000..0e0dba2a4 --- /dev/null +++ b/tests/raytracer/main.sml @@ -0,0 +1,429 @@ +(* Author: Troels Henriksen, https://sigkill.dk/ *) + +(* A ray tracer that fires one ray per pixel and only supports +coloured, reflective spheres. It parallelises two things + + 0. The construction of a BVH for accelerating ray lookups + (divide-and-conquer task parallelism) + + 1. The parallel loop across all of the pixels to be computed (data + parallelism, albeit potentially poorly load balanced) + +*) + +type vec3 = {x: real, y: real, z: real} + +local + fun vf f (v1: vec3) (v2: vec3) = + {x= f (#x v1, #x v2), + y= f (#y v1, #y v2), + z= f (#z v1, #z v2)} +in + +val vec_add = vf (op+) +val vec_sub = vf (op-) +val vec_mul = vf (op* ) +val vec_div = vf (op/) + +fun scale s {x,y,z} = {x=s*x, y=s*y, z=s*z} : vec3 + +fun dot (v1: vec3) (v2: vec3) = + let val v3 = vec_mul v1 v2 + in #x v3 + #y v3 + #z v3 end + +fun norm v = Math.sqrt (dot v v) + +fun normalise v = scale (1.0 / norm v) v + +fun cross {x=x1, y=y1, z=z1} {x=x2, y=y2, z=z2} = + {x=y1*z2-z1*y2, y=z1*x2-x1*z2, z=x1*y2-y1*x2} : vec3 + +end + +type aabb = { min: vec3, max: vec3 } + +fun min x y : real = + if x < y then x else y + +fun max x y : real = + if x < y then y else x + +fun enclosing (box0: aabb) (box1: aabb) = + let val small = { x = min (#x (#min box0)) (#x (#min box1)) + , y = min (#y (#min box0)) (#y (#min box1)) + , z = min (#z (#min box0)) (#z (#min box1)) + } + val big = { x = max (#x (#max box0)) (#x (#max box1)) + , y = max (#y (#max box0)) (#y (#max box1)) + , z = max (#z (#max box0)) (#z (#max box1)) + } + in {min=small, max=big} end + +fun centre (aabb: aabb) = + { x = (#x (#min aabb) + (#x (#max aabb) - #x (#min aabb))), + y = (#y (#min aabb) + (#y (#max aabb) - #y (#min aabb))), + z = (#z (#min aabb) + (#z (#max aabb) - #z (#min aabb))) + } + +datatype 'a bvh = bvh_leaf of aabb * 'a + | bvh_split of aabb * 'a bvh * 'a bvh + +fun bvh_aabb (bvh_leaf (box, _)) = box + | bvh_aabb (bvh_split (box, _, _)) = box + +(* Couldn't find a sorting function in MLtons stdlib - this is from Rosetta Code. *) +local + fun merge cmp ([], ys) = ys + | merge cmp (xs, []) = xs + | merge cmp (xs as x::xs', ys as y::ys') = + case cmp (x, y) of + GREATER => y :: merge cmp (xs, ys') + | _ => x :: merge cmp (xs', ys) + fun sort cmp [] = [] + | sort cmp [x] = [x] + | sort cmp xs = + let + val ys = List.take (xs, length xs div 2) + val zs = List.drop (xs, length xs div 2) + in + merge cmp (sort cmp ys, sort cmp zs) + end +in +fun mk_bvh f all_objs = + let fun mk _ _ [] = raise Fail "mk_bvh: no nodes" + | mk _ _ [x] = bvh_leaf(f x, x) + | mk d n xs = + let val axis = case d mod 3 of 0 => #x + | 1 => #y + | _ => #z + fun cmp (x, y) = + Real.compare(axis(centre(f x)), + axis(centre(f y))) + val xs_sorted = sort cmp xs + val xs_left = List.take(xs_sorted, n div 2) + val xs_right = List.drop(xs_sorted, n div 2) + fun do_left () = mk (d+1) (n div 2) xs_left + fun do_right () = mk (d+1) (n-(n div 2)) xs_right + val (left, right) = + if n < 100 + then (do_left(), do_right()) + else ForkJoin.par (do_left, do_right) + val box = enclosing (bvh_aabb left) (bvh_aabb right) + in bvh_split (box, left, right) end + in mk 0 (length all_objs) all_objs end +end + +type pos = vec3 +type dir = vec3 +type colour = vec3 + +val black : vec3 = {x=0.0, y=0.0, z=0.0} +val white : vec3 = {x=1.0, y=1.0, z=1.0} + +type ray = {origin: pos, dir: dir} + +fun point_at_param (ray: ray) t = + vec_add (#origin ray) (scale t (#dir ray)) + +type hit = { t: real + , p: pos + , normal: dir + , colour: colour + } + +type sphere = { pos: pos + , colour: colour + , radius: real + } + +fun sphere_aabb {pos, colour=_, radius} = + {min = vec_sub pos {x=radius, y=radius, z=radius}, + max = vec_add pos {x=radius, y=radius, z=radius}} + +fun sphere_hit {pos, colour, radius} r t_min t_max : hit option = + let val oc = vec_sub (#origin r) pos + val a = dot (#dir r) (#dir r) + val b = dot oc (#dir r) + val c = dot oc oc - radius*radius + val discriminant = b*b - a*c + fun try temp = + if temp < t_max andalso temp > t_min + then SOME { t = temp + , p = point_at_param r temp + , normal = scale (1.0/radius) + (vec_sub (point_at_param r temp) pos) + , colour = colour + } + else NONE + in if discriminant <= 0.0 + then NONE + else case try ((~b - Math.sqrt(b*b-a*c))/a) of + SOME hit => SOME hit + | NONE => try ((~b + Math.sqrt(b*b-a*c))/a) + end + +fun aabb_hit aabb ({origin, dir}: ray) tmin0 tmax0 = + let fun iter min' max' origin' dir' tmin' tmax' = + let val invD = 1.0 / dir' + val t0 = (min' - origin') * invD + val t1 = (max' - origin') * invD + val (t0', t1') = if invD < 0.0 then (t1, t0) else (t0, t1) + val tmin'' = max t0' tmin' + val tmax'' = min t1' tmax' + in (tmin'', tmax'') end + val (tmin1, tmax1) = + iter + (#x (#min aabb)) (#x (#max aabb)) + (#x origin) (#x dir) + tmin0 tmax0 + in if tmax1 <= tmin1 then false + else let val (tmin2, tmax2) = + iter (#y (#min aabb)) (#y (#max aabb)) + (#y origin) (#y dir) + tmin1 tmax1 + in if tmax2 <= tmin2 then false + else let val (tmin3, tmax3) = + iter (#z (#min aabb)) (#z (#max aabb)) + (#z origin) (#z dir) + tmin2 tmax2 + in not (tmax3 <= tmin3) end + end + end + +type objs = sphere bvh + +fun objs_hit (bvh_leaf (_, s)) r t_min t_max = + sphere_hit s r t_min t_max + | objs_hit (bvh_split (box, left, right)) r t_min t_max = + if not (aabb_hit box r t_min t_max) + then NONE + else case objs_hit left r t_min t_max of + SOME h => (case objs_hit right r t_min (#t h) of + NONE => SOME h + | SOME h' => SOME h') + | NONE => objs_hit right r t_min t_max + +type camera = { origin: pos + , llc: pos + , horizontal: dir + , vertical: dir + } + +fun camera lookfrom lookat vup vfov aspect = + let val theta = vfov * Math.pi / 180.0 + val half_height = Math.tan (theta / 2.0) + val half_width = aspect * half_height + val origin = lookfrom + val w = normalise (vec_sub lookfrom lookat) + val u = normalise (cross vup w) + val v = cross w u + in { origin = lookfrom + , llc = vec_sub + (vec_sub (vec_sub origin (scale half_width u)) + (scale half_height v)) w + , horizontal = scale (2.0*half_width) u + , vertical = scale (2.0*half_height) v + } + end + +fun get_ray (cam: camera) s t : ray= + { origin = #origin cam + , dir = vec_sub (vec_add (vec_add (#llc cam) (scale s (#horizontal cam))) + (scale t (#vertical cam))) + (#origin cam) + } + +fun reflect v n = + vec_sub v (scale (2.0 * dot v n) n) + +fun scatter (r: ray) (hit: hit) = + let val reflected = + reflect (normalise (#dir r)) (#normal hit) + val scattered = {origin = #p hit, dir = reflected} + in if dot (#dir scattered) (#normal hit) > 0.0 + then SOME (scattered, #colour hit) + else NONE + end + +fun ray_colour objs r depth = + case objs_hit objs r 0.001 1000000000.0 of + SOME hit => (case scatter r hit of + SOME (scattered, attenuation) => + if depth < 50 + then vec_mul attenuation (ray_colour objs scattered (depth+1)) + else black + | NONE => black) + | NONE => let val unit_dir = normalise (#dir r) + val t = 0.5 * (#y unit_dir + 1.0) + val bg = {x=0.5, y=0.7, z=1.0} + in vec_add (scale (1.0-t) white) (scale t bg) + end + +fun trace_ray objs width height cam j i : colour = + let val u = real i / real width + val v = real j / real height + val ray = get_ray cam u v + in ray_colour objs ray 0 end + +type pixel = int * int * int + +fun colour_to_pixel {x=r,y=g,z=b} = + let val ir = trunc (255.99 * r) + val ig = trunc (255.99 * g) + val ib = trunc (255.99 * b) + in (ir, ig, ib) end + +type image = { pixels: pixel Array.array + , height: int + , width: int} + +fun image2ppm out ({pixels, height, width}: image) = + let fun onPixel (r,g,b) = + TextIO.output(out, + Int.toString r ^ " " ^ + Int.toString g ^ " " ^ + Int.toString b ^ "\n") + in TextIO.output(out, + "P3\n" ^ + Int.toString width ^ " " ^ Int.toString height ^ "\n" ^ + "255\n") + before Array.app onPixel pixels + end + +fun image2ppm6 out ({pixels, height, width}: image) = + let + fun onPixel (r,g,b) = + TextIO.output(out, String.implode (List.map Char.chr [r,g,b])) + in TextIO.output(out, + "P6\n" ^ + Int.toString width ^ " " ^ Int.toString height ^ "\n" ^ + "255\n") + before Array.app onPixel pixels + end + +fun render objs width height cam : image = + let val pixels = ForkJoin.alloc (height*width) + fun pixel l = + let val i = l mod width + val j = height - l div width + in Array.update (pixels, + l, + colour_to_pixel (trace_ray objs width height cam j i)) end + val _ = ForkJoin.parfor 256 (0,height*width) pixel + in {width = width, + height = height, + pixels = pixels + } + end + +type scene = { camLookFrom: pos + , camLookAt: pos + , camFov: real + , spheres: sphere list + } + +fun from_scene width height (scene: scene) : objs * camera = + (mk_bvh sphere_aabb (#spheres scene), + camera (#camLookFrom scene) (#camLookAt scene) {x=0.0, y=1.0, z=0.0} + (#camFov scene) (real width/real height)) + +fun tabulate_2d m n f = + List.concat (List.tabulate (m, fn j => List.tabulate (n, fn i => f (j, i)))) + +val rgbbox : scene = + let val n = 10 + val k = 60.0 + + val leftwall = + tabulate_2d n n (fn (y, z) => + { pos={x=(~k/2.0), + y=(~k/2.0 + (k/real n) * real y), + z=(~k/2.0 + (k/real n) * real z)} + , colour={x=1.0, y=0.0, z=0.0} + , radius = (k/(real n*2.0)) + }) + + val midwall = + tabulate_2d n n (fn (x,y) => + { pos={x=(~k/2.0 + (k/real n) * real x), + y=(~k/2.0 + (k/real n) * real y), + z=(~k/2.0)} + , colour={x=1.0, y=1.0, z=0.0} + , radius = (k/(real n*2.0))}) + + val rightwall = + tabulate_2d n n (fn (y,z) => + { pos={x=(k/2.0), + y=(~k/2.0 + (k/real n) * real y), + z=(~k/2.0 + (k/real n) * real z)} + , colour={x=0.0, y=0.0, z=1.0} + , radius = (k/(real n*2.0)) + }) + + + val bottom = + tabulate_2d n n (fn (x,z) => + { pos={x=(~k/2.0 + (k/real n) * real x), + y=(~k/2.0), + z=(~k/2.0 + (k/real n) * real z)} + , colour={x=1.0, y=1.0, z=1.0} + , radius = (k/(real n*2.0)) + }) + + + in { spheres = leftwall @ midwall @ rightwall @ bottom + , camLookFrom = {x=0.0, y=30.0, z=30.0} + , camLookAt = {x=0.0, y= ~1.0, z= ~1.0} + , camFov = 75.0 + } + end + +val irreg : scene = + let val n = 100 + val k = 600.0 + val bottom = + tabulate_2d n n (fn (x,z) => + { pos={x=(~k/2.0 + (k/real n) * real x), + y=0.0, + z=(~k/2.0 + (k/real n) * real z)} + , colour = white + , radius = k/(real n * 2.0) + }) + in { spheres = bottom + , camLookFrom = {x=0.0, y=12.0, z=30.0} + , camLookAt = {x=0.0, y=10.0, z= ~1.0} + , camFov = 75.0 } + end + +structure CLA = CommandLineArgs + +val height = CLA.parseInt "m" 200 +val width = CLA.parseInt "n" 200 +val f = CLA.parseString "f" "" +val dop6 = CLA.parseFlag "ppm6" +val scene_name = CLA.parseString "s" "rgbbox" +val scene = case scene_name of + "rgbbox" => rgbbox + | "irreg" => irreg + | s => raise Fail ("No such scene: " ^ s) +val rep = case (Int.fromString (CLA.parseString "repeat" "1")) of + SOME(a) => a + | NONE => 1 + +val _ = print ("Using scene '" ^ scene_name ^ "' (-s to switch)\n") + +val ((objs, cam), tm1) = Util.getTime(fn _ => from_scene width height scene) +val _ = print ("Scene BVH construction in " ^ Time.fmt 4 tm1 ^ "s\n") + +val result = Benchmark.run "rendering" (fn _ => render objs width height cam) + +val writeImage = if dop6 then image2ppm6 else image2ppm + +val _ = if f <> "" then + let val out = TextIO.openOut f + in print ("Writing image to " ^ f ^ ".\n") + before writeImage out (result) + before TextIO.closeOut out + end + else print ("-f not passed, so not writing image to file.\n") + diff --git a/tests/raytracer/raytracer.mlb b/tests/raytracer/raytracer.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/raytracer/raytracer.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/reverb/main.sml b/tests/reverb/main.sml new file mode 100644 index 000000000..aad622a52 --- /dev/null +++ b/tests/reverb/main.sml @@ -0,0 +1,26 @@ +structure CLA = CommandLineArgs + + + +val infile = + case CLA.positional () of + [x] => x + | _ => Util.die ("[ERR] usage: reverb INPUT_FILE [-output OUTPUT_FILE]\n") + +val outfile = CLA.parseString "output" "" + +val (snd, tm) = Util.getTime (fn _ => NewWaveIO.readSound infile) +val _ = print ("read sound in " ^ Time.fmt 4 tm ^ "s\n") + +val rsnd = Benchmark.run "reverberating" (fn _ => Signal.reverb snd) + +val _ = + if outfile = "" then + print ("use -output file.wav to hear results\n") + else + let + val (_, tm) = Util.getTime (fn _ => NewWaveIO.writeSound rsnd outfile) + in + print ("wrote output in " ^ Time.fmt 4 tm ^ "s\n") + end + diff --git a/tests/reverb/reverb.mlb b/tests/reverb/reverb.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/reverb/reverb.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/samplesort/main.sml b/tests/samplesort/main.sml new file mode 100644 index 000000000..b59ed73a1 --- /dev/null +++ b/tests/samplesort/main.sml @@ -0,0 +1,16 @@ +structure CLA = CommandLineArgs + +val n = CLA.parseInt "N" (100 * 1000 * 1000) +val _ = print ("N " ^ Int.toString n ^ "\n") + +val _ = print ("generating " ^ Int.toString n ^ " random integers\n") + +fun elem i = + Word64.toInt (Word64.mod (Util.hash64 (Word64.fromInt i), Word64.fromInt n)) +val input = ArraySlice.full (SeqBasis.tabulate 10000 (0, n) elem) + +val result = + Benchmark.run "running samplesort" (fn _ => SampleSort.sort Int.compare input) + +val _ = print ("result " ^ Util.summarizeArraySlice 8 Int.toString result ^ "\n") + diff --git a/tests/samplesort/samplesort.mlb b/tests/samplesort/samplesort.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/samplesort/samplesort.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/seam-carve-index/README b/tests/seam-carve-index/README new file mode 100644 index 000000000..4a6f232cc --- /dev/null +++ b/tests/seam-carve-index/README @@ -0,0 +1,7 @@ +This benchmark computes a seam-carving index, which is the order in which +pixels are removed. This allows us to efficiently generate any intermediate +image. + +So, whereas the "seam-carve" benchmark really only removes one seam +(and this can be iterated to remove many seams), this benchmark is designed +to carve out many seams. diff --git a/tests/seam-carve-index/SCI.sml b/tests/seam-carve-index/SCI.sml new file mode 100644 index 000000000..a0ce09346 --- /dev/null +++ b/tests/seam-carve-index/SCI.sml @@ -0,0 +1,192 @@ +structure SCI: +sig + type image = PPM.image + type seam = int Seq.t + + (* `makeSeamCarveIndex n img` removes `n` seams and returns + * a mapping X that indicates the order in which pixels are removed. + * + * For a pixel at (i,j): + * - if not removed, then X[i*width + j] = -1 + * - otherwise, removed in seam number X[i*width + j] + * + * So, for an image of height H, there will be H pixels that are marked 0 + * and H other pixels that are marked 1, etc. + *) + val makeSeamCarveIndex: int -> image -> int Seq.t +end = +struct + + type image = PPM.image + type seam = int Seq.t + + val blockWidth = CommandLineArgs.parseInt "block-width" 80 + val _ = print ("block-width " ^ Int.toString blockWidth ^ "\n") + val _ = + if blockWidth mod 2 = 0 then () + else Util.die ("block-width must be even!") + + (* This is copied/adapted from ../seam-carve/SC.sml. See that file for + * explanation of the algorithm. *) + fun triangularBlockedWriteAllMinSeams width height energy minSeamEnergies = + let + fun M (i, j) = + if j < 0 orelse j >= width then Real.posInf + else Array.sub (minSeamEnergies, i*width + j) + fun setM (i, j) = + let + val x = + if i = 0 then 0.0 + else energy (i, j) + + Real.min (M (i-1, j), Real.min (M (i-1, j-1), M (i-1, j+1))) + in + Array.update (minSeamEnergies, i*width + j, x) + end + + val blockHeight = blockWidth div 2 + val numBlocks = 1 + (width - 1) div blockWidth + + fun upperTriangle i jMid = + Util.for (0, Int.min (height-i, blockHeight)) (fn k => + let + val lo = Int.max (0, jMid-blockHeight+k) + val hi = Int.min (width, jMid+blockHeight-k) + in + Util.for (lo, hi) (fn j => setM (i+k, j)) + end) + + fun lowerTriangle i jMid = + Util.for (0, Int.min (height-i, blockHeight)) (fn k => + let + val lo = Int.max (0, jMid-k-1) + val hi = Int.min (width, jMid+k+1) + in + Util.for (lo, hi) (fn j => setM (i+k, j)) + end) + + fun setStripStartingAt i = + ( ForkJoin.parfor 1 (0, numBlocks) (fn b => + upperTriangle i (b * blockWidth + blockHeight)) + ; ForkJoin.parfor 1 (0, numBlocks+1) (fn b => + lowerTriangle (i+1) (b * blockWidth)) + ) + + fun loop i = + if i >= height then () else + ( setStripStartingAt i + ; loop (i + blockHeight + 1) + ) + in + loop 0 + end + + (* ====================================================================== *) + + fun isolateMinSeam width height M = + let + fun idxMin2 ((j1, m1), (j2, m2)) = + if m1 > m2 then (j2, m2) else (j1, m1) + fun idxMin3 (a, b, c) = idxMin2 (a, idxMin2 (b, c)) + + (* the index of the minimum seam in the last row *) + val (jMin, _) = + SeqBasis.reduce 1000 + idxMin2 (~1, Real.posInf) (0, width) (fn j => (j, M (height-1, j))) + + val seam = ForkJoin.alloc height + + fun computeSeamBackwards (i, j) = + if i = 0 then + Array.update (seam, 0, j) + else + let + val (j', _) = idxMin3 + ( (j, M (i-1, j )) + , (j-1, M (i-1, j-1)) + , (j+1, M (i-1, j+1)) + ) + in + Array.update (seam, i, j); + computeSeamBackwards (i-1, j') + end + in + computeSeamBackwards (height-1, jMin); + ArraySlice.full seam + end + + (* ====================================================================== *) + + structure VSIM = VerticalSeamIndexMap + + fun makeSeamCarveIndex numSeamsToRemove image = + let + val N = #width image * #height image + + (* This buffer will be reused throughout *) + val minSeamEnergies = ForkJoin.alloc N + + fun pixel idx (i, j) = PPM.elem image (VSIM.remap idx (i, j)) + + (* =========================================== + * computing the energy of all pixels + * (gradient values) + *) + + fun d p1 p2 = Color.distance (p1, p2) + + fun energy idx (i, j) = + let + val (h, w) = VSIM.domain idx + in + if j = w-1 then Real.posInf + else if i = h - 1 then 0.0 + else let + val p = pixel idx (i, j) + val dx = d p (pixel idx (i, j+1)) + val dy = d p (pixel idx (i+1, j)) + in + Math.sqrt (dx + dy) + end + end + + (* ============================================ + * loop to remove seams + *) + + val X = ForkJoin.alloc N + val _ = ForkJoin.parfor 4000 (0, N) (fn i => Array.update (X, i, ~1)) + fun setX (i, j) x = Array.update (X, i*(#width image) + j, x) + + val idx = VSIM.new (#height image, #width image) + + fun loop numSeamsRemoved = + if numSeamsRemoved >= numSeamsToRemove then () else + let + val currentWidth = #width image - numSeamsRemoved + val _ = triangularBlockedWriteAllMinSeams + currentWidth + (#height image) + (energy idx) + minSeamEnergies (* results written here *) + + fun M (i, j) = + if j < 0 orelse j >= currentWidth then Real.posInf + else Array.sub (minSeamEnergies, i*currentWidth + j) + + val seam = isolateMinSeam currentWidth (#height image) M + in + Seq.foreach seam (fn (i, j) => + setX (VSIM.remap idx (i, j)) numSeamsRemoved); + + VSIM.carve idx seam; + + loop (numSeamsRemoved+1) + end + + in + loop 0; + + ArraySlice.full X + end + +end diff --git a/tests/seam-carve-index/VerticalSeamIndexMap.sml b/tests/seam-carve-index/VerticalSeamIndexMap.sml new file mode 100644 index 000000000..2b331c655 --- /dev/null +++ b/tests/seam-carve-index/VerticalSeamIndexMap.sml @@ -0,0 +1,80 @@ +structure VerticalSeamIndexMap :> +sig + type t + type seam = int Seq.t + + (* `new (height, width)` *) + val new: (int * int) -> t + + (* Remaps from some (H, W) into (H', W'). For vertical seams, we always + * have H = H'. This signature could be reused for horizontal seams, though. + *) + val domain: t -> (int * int) + val range: t -> (int * int) + + (* Remap an index (i, j) to some (i', j'), where i is a row index and j + * is a column index. With vertical seams, i = i'. + *) + val remap: t -> (int * int) -> (int * int) + + (* Remove the given seam. + * Causes all (i, j) on the right of the seam to be remapped to (i, j+1). + *) + val carve: t -> seam -> unit +end = +struct + + structure AS = ArraySlice + + type t = {displacement: int Seq.t, domain: (int * int) ref, range: int * int} + type seam = int Seq.t + + fun new (height, width) = + { displacement = AS.full (SeqBasis.tabulate 4000 (0, width*height) (fn _ => 0)) + , domain = ref (height, width) + , range = (height, width) + } + + fun domain ({domain = ref d, ...}: t) = d + fun range ({range=r, ...}: t) = r + + fun remap ({displacement, range=(h, w), ...}: t) (i, j) = + (i, j + Seq.nth displacement (i*w + j)) + + (* fun carve ({displacement, domain=(h, w), range=r}: t) seam = + let + in + { domain = (h, w-1) + , range = r + , displacement = displacement + } + end *) + + fun carve ({displacement, domain=(d as ref (h, w)), range=(_, w0)}: t) seam = + ( d := (h, w-1) + ; ForkJoin.parfor 1 (0, h) (fn i => + let + val s = Seq.nth seam i + in + Util.for (s+1, w) (fn j => + ArraySlice.update (displacement, i*w0 + j - 1, + 1 + Seq.nth displacement (i*w0 + j))) + end) + ) + + (* { domain = (h, w-1) + , range = (h, w0) + , displacement = AS.full (SeqBasis.tabulate 1000 (0, w0*h) (fn k => + let + val i = k div w0 + val j = k mod w0 + val s = Seq.nth seam i + in + if j < s then + Seq.nth displacement (i*w0 + j) + else + 1 + Seq.nth displacement (i*w0 + j) + end)) + } *) + +end diff --git a/tests/seam-carve-index/main.sml b/tests/seam-carve-index/main.sml new file mode 100644 index 000000000..d9efb5142 --- /dev/null +++ b/tests/seam-carve-index/main.sml @@ -0,0 +1,122 @@ +structure CLA = CommandLineArgs + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val numSeams = CLA.parseInt "num-seams" 100 +val _ = print ("num-seams " ^ Int.toString numSeams ^ "\n") + +val (image, tm) = Util.getTime (fn _ => PPM.read filename) +val _ = print ("read image in " ^ Time.fmt 4 tm ^ "s\n") + +val w = #width image +val h = #height image + +val _ = print ("height " ^ Int.toString h ^ "\n") +val _ = print ("width " ^ Int.toString w ^ "\n") + +val _ = + if numSeams >= 0 andalso numSeams <= w then () + else + Util.die ("cannot remove " ^ Int.toString numSeams + ^ " seams from image of width " ^ Int.toString w ^ "\n") + +val X = Benchmark.run "seam carving" + (fn _ => SCI.makeSeamCarveIndex numSeams image) + +val outfile = CLA.parseString "output" "" + +val _ = + if outfile = "" then + print ("use -output XXX.gif to see result\n") + else + let + val ((palette, indices), tm) = Util.getTime (fn _ => + let + val palette = GIF.Palette.summarize [Color.black, Color.red] 128 image + in + (palette, #remap palette image) + end) + + val _ = print ("remapped color palette in " ^ Time.fmt 4 tm ^ "s\n") + fun getIdx (i, j) = Seq.nth indices (i*w + j) + + val redIdx = GIF.Palette.remapColor palette Color.red + val blackIdx = GIF.Palette.remapColor palette Color.black + + fun removeSeams count = + let + val data = ForkJoin.alloc (w * h) + fun set (i, j) x = Array.update (data, i*w + j, x) + + (* compact row i from index j, writing the result at index k *) + fun compactRow i j k = + if j >= w then + Util.for (k, w) (fn kk => set (i, kk) blackIdx) + else + let + val xx = Seq.nth X (i*w + j) + in + if xx = ~1 orelse xx > count then + ( set (i, k) (getIdx (i, j)) + ; compactRow i (j+1) (k+1) + ) + else if xx = count then + ( set (i, k) redIdx + ; compactRow i (j+1) (k+1) + ) + else + compactRow i (j+1) k + end + in + ForkJoin.parfor 1 (0, h) (fn i => compactRow i 0 0); + ArraySlice.full data + end + + val (images, tm) = Util.getTime (fn _ => + ArraySlice.full (SeqBasis.tabulate 1 (0, numSeams+1) removeSeams)) + val _ = print ("generated images in " ^ Time.fmt 4 tm ^ "s\n") + + val (_, tm) = Util.getTime (fn _ => + GIF.writeMany outfile 10 palette + { width = w + , height = h + , numImages = numSeams+1 + , getImage = Seq.nth images + }) + in + print ("wrote to " ^ outfile ^ " in " ^ Time.fmt 4 tm ^ "s\n") + end + +(* val _ = + if outfile = "" then + print ("use -output XXX to see result\n") + else + let + fun colorSeam i = + Color.hsv { h = 100.0 * (Real.fromInt i / Real.fromInt numSeams) + , s = 1.0 + , v = 1.0 + } + + val carved = + { width = w + , height = h + , data = ArraySlice.full (SeqBasis.tabulate 4000 (0, w * h) (fn k => + let + val i = k div w + val j = k mod w + in + if Seq.nth X k < 0 then + PPM.elem image (i, j) + else + colorSeam (Seq.nth X k) + end)) + } + val (_, tm) = Util.getTime (fn _ => PPM.write outfile carved) + in + print ("wrote output in " ^ Time.fmt 4 tm ^ "s\n") + end *) + diff --git a/tests/seam-carve-index/seam-carve-index.mlb b/tests/seam-carve-index/seam-carve-index.mlb new file mode 100644 index 000000000..a7c6b4334 --- /dev/null +++ b/tests/seam-carve-index/seam-carve-index.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +VerticalSeamIndexMap.sml +SCI.sml +main.sml diff --git a/tests/seam-carve/SC.sml b/tests/seam-carve/SC.sml new file mode 100644 index 000000000..993f82487 --- /dev/null +++ b/tests/seam-carve/SC.sml @@ -0,0 +1,287 @@ +structure SC: +sig + type image = PPM.image + type seam = int Seq.t + + val minSeam: image -> seam + val paintSeam: image -> seam -> PPM.pixel -> image + + val carve: image -> seam -> image + + val removeSeams: int -> image -> image +end = +struct + + structure AS = ArraySlice + + type image = PPM.image + type seam = int Seq.t + + (* ====================================================================== + * seam finding algorithms to solve the equation + * M(i,j) = E(i,j) + min(M(i-1,j), M(i-1,j-1), M(i-1,j+1)) + *) + + (* row-wise bottom-up DP goes by increasing i *) + fun rowWiseAllMinSeams width height (energy: int -> int -> real): real Seq.t = + let + val minSeamEnergies = ForkJoin.alloc (width * height) + fun M i j = + if j < 0 orelse j >= width then Real.posInf + else Array.sub (minSeamEnergies, i*width + j) + fun setM i j = + let + val x = + if i = 0 then 0.0 + else energy i j + + Real.min (M (i-1) j, Real.min (M (i-1) (j-1), M (i-1) (j+1))) + in + Array.update (minSeamEnergies, i*width + j, x) + end + + fun computeMinSeamEnergies i = + if i >= height then () + else ( ForkJoin.parfor 1000 (0, width) (setM i) + ; computeMinSeamEnergies (i+1) + ) + + val _ = computeMinSeamEnergies 0 + in + AS.full minSeamEnergies + end + + (* I tuned this a little bit. For an image approximately 1000 pixels + * wide, this only gives us about 12x possible speedup. But any smaller + * and the grains are too small! *) + val blockWidth = CommandLineArgs.parseInt "block-width" 80 + val _ = print ("block-width " ^ Int.toString blockWidth ^ "\n") + val _ = + if blockWidth mod 2 = 0 then () + else Util.die ("block-width must be even!") + + (* Triangular-blocked bottom-up DP does fancy triangular strategy, to + * improve granularity. + * + * Imaging breaking up the image into a bunch of strips, where each strip + * is then divided into triangles. If each triangle is processed sequentially + * row-wise from top to bottom, then we can compute a strip in parallel + * by first doing all of upper triangles (#), and then doing all of the lower + * triangles (.): + * + * +------------------------------------+ + * strip 1 -> |\####/\####/\####/\####/\####/\####/| + * |.\##/..\##/..\##/..\##/..\##/..\##/.| + * |..\/....\/....\/....\/....\/....\/..| + * strip 2 -> |\####/\####/\####/\####/\####/\####/| + * |.\##/..\##/..\##/..\##/..\##/..\##/.| + * |..\/....\/....\/....\/....\/....\/..| + * strip 3 -> | | + *) + fun triangularBlockedAllMinSeams width height energy = + let + val minSeamEnergies = ForkJoin.alloc (width * height) + fun M i j = + if j < 0 orelse j >= width then Real.posInf + else Array.sub (minSeamEnergies, i*width + j) + fun setM i j = + let + val x = + if i = 0 then 0.0 + else energy i j + + Real.min (M (i-1) j, Real.min (M (i-1) (j-1), M (i-1) (j+1))) + in + Array.update (minSeamEnergies, i*width + j, x) + end + + val blockHeight = blockWidth div 2 + val numBlocks = 1 + (width - 1) div blockWidth + + (* Fill in a triangle starting at row i, centered at jMid, with + * the fat end at top and small end at bottom. + * + * For example with blockWidth 6: + * jMid + * | + * i -- X X X X X X + * X X X X + * X X + *) + fun upperTriangle i jMid = + Util.for (0, Int.min (height-i, blockHeight)) (fn k => + let + val lo = Int.max (0, jMid-blockHeight+k) + val hi = Int.min (width, jMid+blockHeight-k) + in + Util.for (lo, hi) (fn j => setM (i+k) j) + end) + + (* The other way around. For example with blockWidth 6: + * jMid + * | + * i -- X X + * X X X X + * X X X X X X + *) + fun lowerTriangle i jMid = + Util.for (0, Int.min (height-i, blockHeight)) (fn k => + let + val lo = Int.max (0, jMid-k-1) + val hi = Int.min (width, jMid+k+1) + in + Util.for (lo, hi) (fn j => setM (i+k) j) + end) + + (* This sets rows [i, i + blockHeight]. + * Note that this includes the row i+blockHeight; i.e. the number of + * rows set is blockHeight+1 + *) + fun setStripStartingAt i = + ( ForkJoin.parfor 1 (0, numBlocks) (fn b => + upperTriangle i (b * blockWidth + blockHeight)) + ; ForkJoin.parfor 1 (0, numBlocks+1) (fn b => + lowerTriangle (i+1) (b * blockWidth)) + ) + + fun computeMinSeamEnergies i = + if i >= height then () else + ( setStripStartingAt i + ; computeMinSeamEnergies (i + blockHeight + 1) + ) + + val _ = computeMinSeamEnergies 0 + in + AS.full minSeamEnergies + end + + (* ====================================================================== + * find the min seam + * can choose for the allMinSeams algorithm: + * 1. rowWiseAllMinSeams + * 2. triangularBlockedAllMinSeams + *) + + fun minSeam' allMinSeams image = + let + val height = #height image + val width = #width image + fun pixel i j = PPM.elem image (i, j) + + (* =========================================== + * compute the energy of all pixels + * (gradient values) + *) + + fun c x = Real.fromInt (Word8.toInt x) / 255.0 + fun sq (x: real) = x * x + fun d {red=r1, green=g1, blue=b1} {red=r2, green=g2, blue=b2} = + sq (c r2 - c r1) + sq (c g2 - c g1) + sq (c b2 - c b1) + + fun computeEnergy i j = + if j = width-1 then Real.posInf + else if i = height - 1 then 0.0 + else let + val dx = d (pixel i j) (pixel i (j+1)) + val dy = d (pixel i j) (pixel (i+1) j) + in + Math.sqrt (dx + dy) + end + + val energies = + AS.full (SeqBasis.tabulate 4000 (0, width*height) (fn k => + computeEnergy (k div width) (k mod width))) + fun energy i j = + Seq.nth energies (i*width + j) + + (* =========================================== + * compute the min seam energies + *) + + val MM = allMinSeams width height energy + fun M i j = + if j < 0 orelse j >= width then Real.posInf + else Seq.nth MM (i*width + j) + + (* =========================================== + * isolate the minimum seam + *) + + fun idxMin2 ((j1, m1), (j2, m2)) = + if m1 > m2 then (j2, m2) else (j1, m1) + fun idxMin3 (a, b, c) = idxMin2 (a, idxMin2 (b, c)) + + (* the index of the minimum seam in the last row *) + val (jMin, _) = + SeqBasis.reduce 1000 + idxMin2 (~1, Real.posInf) (0, width) (fn j => (j, M (height-1) j)) + + fun computeSeamBackwards seam (i, j) = + if i = 0 then j::seam else + let + val (j', _) = idxMin3 + ( (j, M (i-1) j ) + , (j-1, M (i-1) (j-1)) + , (j+1, M (i-1) (j+1)) + ) + in + computeSeamBackwards (j::seam) (i-1, j') + end + in + Seq.fromList (computeSeamBackwards [] (height-1, jMin)) + end + + (* val minSeam = minSeam' rowWiseAllMinSeams *) + val minSeam = minSeam' triangularBlockedAllMinSeams + + (* ====================================================================== + * utilities: carving, painting seams, etc. + *) + + fun carve image seam = + let + val height = #height image + val width = #width image + fun pixel i j = PPM.elem image (i, j) + + fun newElem k = + let + val i = k div (width-1) + val j = k mod (width-1) + val r = Seq.nth seam i + in + if j < r then pixel i j else pixel i (j+1) + end + in + { width = width-1 + , height = height + , data = AS.full (SeqBasis.tabulate 4000 (0, (width-1)*height) newElem) + } + end + + fun paintSeam image seam seamColor = + let + val height = #height image + val width = #width image + fun pixel i j = PPM.elem image (i, j) + + fun newElem k = + let + val i = k div width + val j = k mod width + val r = Seq.nth seam i + in + if j = r then seamColor else pixel i j + end + in + { width = width + , height = height + , data = Seq.tabulate newElem (height * width) + } + end + + fun removeSeams n image = + if n = 0 then + image + else + removeSeams (n-1) (carve image (minSeam image)) + +end diff --git a/tests/seam-carve/main.sml b/tests/seam-carve/main.sml new file mode 100644 index 000000000..ff28b8e0e --- /dev/null +++ b/tests/seam-carve/main.sml @@ -0,0 +1,29 @@ +structure CLA = CommandLineArgs + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val numSeams = CLA.parseInt "num-seams" 100 +val _ = print ("num-seams " ^ Int.toString numSeams ^ "\n") + +val (image, tm) = Util.getTime (fn _ => PPM.read filename) +val _ = print ("read image in " ^ Time.fmt 4 tm ^ "s\n") + +val carved = Benchmark.run "seam carving" (fn _ => SC.removeSeams numSeams image) + +val outfile = CLA.parseString "output" "" +val _ = + if outfile = "" then + print ("use -output XXX to see result\n") + else + let + (* val red = {red=0w255, green=0w0, blue=0w0} + val (_, tm) = Util.getTime (fn _ => + PPM.write outfile (SC.paintSeam image seam red)) *) + val (_, tm) = Util.getTime (fn _ => PPM.write outfile carved) + in + print ("wrote output in " ^ Time.fmt 4 tm ^ "s\n") + end + diff --git a/tests/seam-carve/seam-carve.mlb b/tests/seam-carve/seam-carve.mlb new file mode 100644 index 000000000..b078b011a --- /dev/null +++ b/tests/seam-carve/seam-carve.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +SC.sml +main.sml diff --git a/tests/shuf/main.sml b/tests/shuf/main.sml new file mode 100644 index 000000000..ec9a2ef4b --- /dev/null +++ b/tests/shuf/main.sml @@ -0,0 +1,47 @@ +structure CLA = CommandLineArgs +val seed = CLA.parseInt "seed" 15210 +val outfile = CLA.parseString "o" "" + +val filename = + case CLA.positional () of + [f] => f + | _ => Util.die ("usage: shuf [-o OUTPUT_FILE] [-seed N] INPUT_FILE") + +val (contents, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filename) +val _ = print ("read file in " ^ Time.fmt 4 tm ^ "s\n") + + +fun shuf () = + let + val (lines, tm) = Util.getTime (fn _ => + Tokenize.tokens (fn c => c = #"\n") contents) + val _ = print ("tokenized in " ^ Time.fmt 4 tm ^ "s\n") + + val indices = Seq.tabulate (fn i => i) (Seq.length lines) + val perm = Shuffle.shuffle indices seed + in + Seq.tabulate (fn i => Seq.nth lines (Seq.nth perm i)) (Seq.length lines) + end + +val result = Benchmark.run "shuffle" shuf + +fun dump () = + let + val f = TextIO.openOut outfile + in + Util.for (0, Seq.length result) (fn i => + ( TextIO.output (f, Seq.nth result i) + ; TextIO.output1 (f, #"\n") + )); + TextIO.closeOut f + end + +val _ = + if outfile = "" then + print ("no output specified; use -o OUTPUT_FILE to see results\n") + else + let + val ((), tm) = Util.getTime dump + in + print ("wrote to " ^ outfile ^ " in " ^ Time.fmt 4 tm ^ "s\n") + end diff --git a/tests/shuf/shuf.mlb b/tests/shuf/shuf.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/shuf/shuf.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/skyline/CityGen.sml b/tests/skyline/CityGen.sml new file mode 100644 index 000000000..d88c2285d --- /dev/null +++ b/tests/skyline/CityGen.sml @@ -0,0 +1,77 @@ +structure CityGen: +sig + + (* (city n x) produces a sequence of n random buildings, seeded by x (any + * integer will do). *) + val city : int -> int -> (int * int * int) Seq.t + + (* (cities m n x) produces m cities, each is a sequence of at most n random + * buildings, seeded by x (any integer will do). *) + val cities : int -> int -> int -> (int * int * int) Seq.t Seq.t +end = +struct + + structure R = FastHashRand + + (* Fisher-Yates shuffle aka Knuth shuffle *) + fun shuffle s r = + let + val n = Seq.length s + val data = Array.tabulate (n, Seq.nth s) + + fun swapLoop (r, i) = + if i >= n then r + else let + val j = R.boundedInt (i, n) r + val (x, y) = (Array.sub (data, i), Array.sub (data, j)) + in + Array.update (data, i, y); + Array.update (data, j, x); + swapLoop (R.next r, i+1) + end + + val r' = swapLoop (r, 0) + in + (r', Seq.tabulate (fn i => Array.sub (data, i)) n) + end + + fun citySeeded n r0 = + let + val (r1, xs) = shuffle (Seq.tabulate (fn i => i) (2*n)) r0 + val (_, seeds) = R.splitTab (r1, n) + fun pow b e = if e <= 0 then 1 else b * pow b (e-1) + + fun makeBuilding i = + let + val xpair = (Seq.nth xs (2*i), Seq.nth xs (2*i + 1)) + val lo = Int.min xpair + val hi = Int.max xpair + val width = hi-lo + val maxHeight = Int.max (1, 2*n div width) + val maxHeight = + if maxHeight >= n then + 1 + pow (Util.log2 maxHeight) 2 + else + maxHeight + pow (Util.log2 maxHeight) 2 + val heightRange = (Int.max (1, maxHeight-(n div 100)), maxHeight+1) + val height = R.boundedInt heightRange (seeds i) + in + (lo, height, hi) + end + in + Seq.tabulate makeBuilding n + end + + fun city n x = citySeeded n (R.fromInt x) + + fun cities m n x = + let + val (_, rs) = R.splitTab (R.fromInt x, m) + fun ithCity i = + let val r = rs i + in citySeeded (R.boundedInt (0, n+1) r) (R.next r) + end + in Seq.tabulate ithCity m + end + +end diff --git a/tests/skyline/FastHashRand.sml b/tests/skyline/FastHashRand.sml new file mode 100644 index 000000000..205a7737d --- /dev/null +++ b/tests/skyline/FastHashRand.sml @@ -0,0 +1,66 @@ +(* MUCH faster random number generation than DotMix. + * I wonder how good its randomness is? *) +structure FastHashRand = +struct + type rand = Word64.word + + val maxWord = 0wxFFFFFFFFFFFFFFFF : Word64.word + + exception FastHashRand + + fun hashWord w = + let + open Word64 + infix 2 >> infix 2 << infix 2 xorb infix 2 andb + val v = w * 0w3935559000370003845 + 0w2691343689449507681 + val v = v xorb (v >> 0w21) + val v = v xorb (v << 0w37) + val v = v xorb (v >> 0w4) + val v = v * 0w4768777513237032717 + val v = v xorb (v << 0w20) + val v = v xorb (v >> 0w41) + val v = v xorb (v << 0w5) + in + v + end + + fun fromInt x = hashWord (Word64.fromInt x) + + fun next r = hashWord r + + fun split r = (hashWord r, (hashWord (r+0w1), hashWord (r+0w2))) + + fun biasedBool (h, t) r = + let + val scaleFactor = Word64.div (maxWord, Word64.fromInt (h+t)) + in + Word64.<= (r, Word64.* (Word64.fromInt h, scaleFactor)) + end + + fun split3 _ = raise FastHashRand + fun splitTab (r, n) = + (hashWord r, fn i => hashWord (r + Word64.fromInt (i+1))) + + val intp = + case Int.precision of + SOME n => n + | NONE => (print "[ERR] int precision\n"; OS.Process.exit OS.Process.failure) + + val mask = Word64.<< (0w1, Word.fromInt (intp-1)) + + fun int r = + Word64.toIntX (Word64.andb (r, mask) - 0w1) + + fun int r = + Word64.toIntX (Word64.>> (r, Word.fromInt (64-intp+1))) + + fun boundedInt (a, b) r = a + ((int r) mod (b-a)) + + fun bool _ = raise FastHashRand + + fun biasedInt _ _ = raise FastHashRand + fun real _ = raise FastHashRand + fun boundedReal _ _ = raise FastHashRand + fun char _ = raise FastHashRand + fun boundedChar _ _ = raise FastHashRand +end diff --git a/tests/skyline/Skyline.sml b/tests/skyline/Skyline.sml new file mode 100644 index 000000000..d871c1382 --- /dev/null +++ b/tests/skyline/Skyline.sml @@ -0,0 +1,60 @@ +structure Skyline = +struct + type 'a seq = 'a Seq.t + type skyline = (int * int) Seq.t + + fun singleton (l, h, r) = Seq.fromList [(l, h), (r, 0)] + + fun combine (sky1, sky2) = + let + val lMarked = Seq.map (fn (x, y) => (x, SOME y, NONE)) sky1 + val rMarked = Seq.map (fn (x, y) => (x, NONE, SOME y)) sky2 + + fun cmp ((x1, _, _), (x2, _, _)) = Int.compare (x1, x2) + val merged = Merge.merge cmp (lMarked, rMarked) + + fun copy (a, b) = case b of SOME _ => b | NONE => a + fun copyFused ((x1, yl1, yr1), (x2, yl2, yr2)) = + (x2, copy (yl1, yl2), copy (yr1, yr2)) + + val allHeights = Seq.scanIncl copyFused (0,NONE,NONE) merged + + fun squish (x, y1, y2) = + (x, Int.max (Option.getOpt (y1, 0), Option.getOpt (y2, 0))) + val sky = Seq.map squish allHeights + + (*fun isUnique (i, (x, h)) = + i = 0 orelse let val (_, prevh) = Seq.nth sky (i-1) in h <> prevh end*) + (*val sky = Seq.filterIdx isUnique sky*) + in + sky + end + + fun skyline g bs = + let + fun skyline' bs = + case Seq.length bs of + 0 => Seq.empty () + | 1 => singleton (Seq.nth bs 0) + | n => + let + val half = n div 2 + val sfL = fn _ => skyline' (Seq.take bs half) + val sfR = fn _ => skyline' (Seq.drop bs half) + in + if Seq.length bs <= g then + combine (sfL (), sfR ()) + else + combine (ForkJoin.par (sfL, sfR)) + end + + val sky = skyline' bs + + fun isUnique (i, (x, h)) = + i = 0 orelse let val (_, prevh) = Seq.nth sky (i-1) in h <> prevh end + val sky = Seq.filterIdx isUnique sky + in + sky + end + +end diff --git a/tests/skyline/main.sml b/tests/skyline/main.sml new file mode 100644 index 000000000..13810c918 --- /dev/null +++ b/tests/skyline/main.sml @@ -0,0 +1,102 @@ +structure CLA = CommandLineArgs +structure Gen = CityGen + +(* +functor S (Sky : SKYLINE where type skyline = (int * int) Seq.t) = +struct + open Sky + fun skyline bs = + case Seq.splitMid bs of + Seq.EMPTY => Seq.empty () + | Seq.ONE b => singleton b + | Seq.PAIR (l, r) => + let + fun sl _ = skyline l + fun sr _ = skyline r + val (l', r') = + if Seq.length bs <= 1000 + then (sl (), sr ()) + else Primitives.par (sl, sr) + in + combine (l', r') + end +end + +structure Stu = S (MkSkyline (structure Seq = Seq)) +structure Ref = S (MkRefSkyline (structure Seq = Seq)) +*) + +fun pairEq ((x1, y1), (x2, y2)) = (x1 = x2 andalso y1 = y2) + +fun skylinesEq (s1, s2) = + Seq.length s1 = Seq.length s2 andalso + Seq.reduce (fn (a,b) => a andalso b) true + (Seq.tabulate (fn i => pairEq (Seq.nth s1 i, Seq.nth s2 i)) (Seq.length s1)) + +val size = CLA.parseInt "size" 1000000 +val seed = CLA.parseInt "seed" 15210 +val grain = CLA.parseInt "grain" 1000 +val output = CLA.parseString "output" "" + +(* ensure newline at end of string *) +fun println s = + let + val needsNewline = + String.size s = 0 orelse String.sub (s, String.size s - 1) <> #"\n" + in + print (if needsNewline then s ^ "\n" else s) + end + +val _ = println ("size " ^ Int.toString size) +val _ = println ("seed " ^ Int.toString seed) +val _ = println ("grain " ^ Int.toString grain) + +val (input, tm) = Util.getTime (fn _ => Gen.city size seed) +val _ = println ("generated input in " ^ Time.fmt 4 tm ^ "s\n") + +val sky = Benchmark.run "skyline" (fn _ => Skyline.skyline grain input) +val _ = print ("result-len " ^ Int.toString (Seq.length sky) ^ "\n") + +val _ = + if output = "" then + print ("use -output XXX.ppm to see result\n") + else + let + val (xMin, _) = Seq.nth sky 0 + val (xMax, _) = Seq.nth sky (Seq.length sky - 1) + val yMax = Seq.reduce Int.max 0 (Seq.map (fn (_,y) => y) sky) + val _ = print ("xMin " ^ Int.toString xMin ^ "\n") + val _ = print ("xMax " ^ Int.toString xMax ^ "\n") + val _ = print ("yMax " ^ Int.toString yMax ^ "\n") + + val width = 1000 + val height = 250 + + val padding = 20 + + fun col x = + padding + width * (x - xMin) div (1 + xMax - xMin) + fun row y = + padding + height - 1 - (height * y div (1 + yMax)) + + val width' = 2*padding + width + val height' = padding + height + val image = Seq.tabulate (fn _ => Color.white) (width' * height') + + val _ = Seq.foreach sky (fn (idx, (x, y)) => + if idx >= Seq.length sky - 1 then () else + let + val (x', _) = Seq.nth sky (idx+1) + + val ihi = row y + val jlo = col x + val jhi = Int.max (col x + 1, col x') + in + Util.for (ihi, height') (fn i => + Util.for (jlo, jhi) (fn j => + ArraySlice.update (image, i*width' + j, Color.black))) + end) + in + PPM.write output {width=width', height=height', data=image}; + print ("wrote output to " ^ output ^ "\n") + end diff --git a/tests/skyline/skyline.mlb b/tests/skyline/skyline.mlb new file mode 100644 index 000000000..00f8c074a --- /dev/null +++ b/tests/skyline/skyline.mlb @@ -0,0 +1,6 @@ +../mpllib/sources.$(COMPAT).mlb +FastHashRand.sml +CityGen.sml +Skyline.sml +main.sml + diff --git a/tests/spanner/Spanner.sml b/tests/spanner/Spanner.sml new file mode 100644 index 000000000..4b13b08ba --- /dev/null +++ b/tests/spanner/Spanner.sml @@ -0,0 +1,41 @@ +structure Spanner = +struct + type 'a seq = 'a Seq.t + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + structure AS = ArraySlice + + type vertex = G.vertex + + fun spanner g k = + let + val n = G.numVertices g + val b = (Math.ln (Real.fromInt n))/(Real.fromInt (2 * k)) + val (clusters, parents) = LDD.ldd g b + fun is_center i = if i = (Seq.nth clusters i) then 1 else 0 + val compact_clusters = SeqBasis.scan 10000 Int.+ 0 (0, n) is_center + val num_clusters = Array.sub (compact_clusters, n) + val _ = print ("number of clusters = " ^ (Int.toString (num_clusters)) ^ "\n") + fun center i = Array.sub (compact_clusters, (Seq.nth clusters i)) + val intra_edges = Seq.tabulate (fn i => (i, Seq.nth parents i)) n + val hash_sim = Seq.tabulate (fn i => NONE) (num_clusters*num_clusters) + fun icu u = + let + val cu = center u + fun add_edge i = + let + val ci = center i + val indexi = cu*num_clusters + ci + in + if (cu < ci) then AS.update (hash_sim, indexi, SOME(u, i)) + else () + end + in + Seq.foreach (G.neighbors g u) (fn (i, si) => add_edge si) + end + val _ = ForkJoin.parfor 10000 (0, n) (fn i => icu i) + val inter_edges = AS.full(SeqBasis.tabFilter 10000 (0, num_clusters*num_clusters) (Seq.nth hash_sim)) + in + Seq.append (intra_edges, inter_edges) + end +end \ No newline at end of file diff --git a/tests/spanner/main.sml b/tests/spanner/main.sml new file mode 100644 index 000000000..2065354be --- /dev/null +++ b/tests/spanner/main.sml @@ -0,0 +1,73 @@ +structure CLA = CommandLineArgs +structure G = AdjacencyGraph(Int) + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + + +val k = (CommandLineArgs.parseInt "k" 3) + +val P = Benchmark.run "running spanner: " (fn _ => Spanner.spanner graph k) +(* val _ = print ("num-triangles = " ^ (Int.toString P) ^ "\n") *) +(* val _ = LDD.check_ldd graph (#1 P) (#2 P) *) +(* val _ = Benchmark.run "running connectivity" (fn _ => LDD.connectivity graph b) *) +(* +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + +val _ = GCStats.report () *) diff --git a/tests/spanner/spanner.mlb b/tests/spanner/spanner.mlb new file mode 100644 index 000000000..66aa6e19e --- /dev/null +++ b/tests/spanner/spanner.mlb @@ -0,0 +1,4 @@ +../mpllib/sources.$(COMPAT).mlb +../ldd/LDD.sml +Spanner.sml +main.sml diff --git a/tests/sparse-mxv-opt/SparseMxV.sml b/tests/sparse-mxv-opt/SparseMxV.sml new file mode 100644 index 000000000..c6b277163 --- /dev/null +++ b/tests/sparse-mxv-opt/SparseMxV.sml @@ -0,0 +1,14 @@ +structure SparseMxV = +struct + + fun sparseMxV (mat: (int * real) Seq.t Seq.t) (vec: real Seq.t) = + let + fun f (i,x) = (Seq.nth vec i) * x + fun rowSum r = + SeqBasis.reduce 5000 op+ 0.0 (0, Seq.length r) (fn i => f(Seq.nth r i)) + in + ArraySlice.full (SeqBasis.tabulate 100 (0, Seq.length mat) (fn i => + rowSum (Seq.nth mat i))) + end + +end diff --git a/tests/sparse-mxv-opt/main.sml b/tests/sparse-mxv-opt/main.sml new file mode 100644 index 000000000..64e971937 --- /dev/null +++ b/tests/sparse-mxv-opt/main.sml @@ -0,0 +1,37 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence +structure DS = DelayedSeq + +structure M = SparseMxV + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val doCheck = CLA.parseFlag "check" + +val rowLen = 100 +val numRows = n div rowLen +val vec = Seq.tabulate (fn i => 1.0) numRows +fun gen i j = + ((Util.hash (i * rowLen + j) mod numRows), 1.0) +val mat = Seq.tabulate (fn i => Seq.tabulate (gen i) rowLen) numRows + +fun task () = + M.sparseMxV mat vec + +fun check result = + if not doCheck then () else + let + fun closeEnough (a, b) = Real.< (Real.abs (a - b), 0.000001) + val correct = + DS.reduce (fn (a, b) => a andalso b) true + (DS.tabulate + (fn i => closeEnough (Seq.nth result i, Real.fromInt rowLen)) + numRows) + in + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + end + +val result = Benchmark.run "sparse-mxv" task +val _ = check result diff --git a/tests/sparse-mxv-opt/sparse-mxv-opt.mlb b/tests/sparse-mxv-opt/sparse-mxv-opt.mlb new file mode 100644 index 000000000..eded4f9db --- /dev/null +++ b/tests/sparse-mxv-opt/sparse-mxv-opt.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +SparseMxV.sml +main.sml diff --git a/tests/sparse-mxv/MkMXV.sml b/tests/sparse-mxv/MkMXV.sml new file mode 100644 index 000000000..d69d4af94 --- /dev/null +++ b/tests/sparse-mxv/MkMXV.sml @@ -0,0 +1,16 @@ +functor MkMXV (Seq : SEQUENCE) = +struct + + structure ASeq = ArraySequence + type 'a seq = 'a ASeq.t + + fun sparseMxV (mat : (int * real) seq seq) (vec : real seq) = + let + fun f (i,x) = (ASeq.nth vec i) * x + fun rowSum r = + Seq.reduce op+ 0.0 (Seq.map f (Seq.fromArraySeq r)) + in + Seq.toArraySeq (Seq.map rowSum (Seq.fromArraySeq mat)) + end + +end diff --git a/tests/sparse-mxv/main.sml b/tests/sparse-mxv/main.sml new file mode 100644 index 000000000..2c2c514b4 --- /dev/null +++ b/tests/sparse-mxv/main.sml @@ -0,0 +1,37 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence +structure DS = DelayedSeq + +structure M = MkMXV (DelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val doCheck = CLA.parseFlag "check" + +val rowLen = 100 +val numRows = n div rowLen +val vec = Seq.tabulate (fn i => 1.0) numRows +fun gen i j = + ((Util.hash (i * rowLen + j) mod numRows), 1.0) +val mat = Seq.tabulate (fn i => Seq.tabulate (gen i) rowLen) numRows + +fun task () = + M.sparseMxV mat vec + +fun check result = + if not doCheck then () else + let + fun closeEnough (a, b) = Real.< (Real.abs (a - b), 0.000001) + val correct = + DS.reduce (fn (a, b) => a andalso b) true + (DS.tabulate + (fn i => closeEnough (Seq.nth result i, Real.fromInt rowLen)) + numRows) + in + if correct then + print ("correct? yes\n") + else + print ("correct? no\n") + end + +val result = Benchmark.run "sparse-mxv" task +val _ = check result diff --git a/tests/sparse-mxv/sparse-mxv.mlb b/tests/sparse-mxv/sparse-mxv.mlb new file mode 100644 index 000000000..6b60b1438 --- /dev/null +++ b/tests/sparse-mxv/sparse-mxv.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MkMXV.sml +main.sml diff --git a/tests/subset-sum/SubsetSumTiled.sml b/tests/subset-sum/SubsetSumTiled.sml new file mode 100644 index 000000000..199e6a2d3 --- /dev/null +++ b/tests/subset-sum/SubsetSumTiled.sml @@ -0,0 +1,100 @@ +structure SubsetSumTiled: +sig + val subset_sum: {unsafe_skip_table_set: bool} + -> int Seq.t * int + -> int Seq.t option +end = +struct + + structure Table: + sig + type t + val new: {unsafe_skip_table_set: bool} -> int * int -> t + val set: t -> int * int -> bool -> unit + val get: t -> int * int -> bool + end = + struct + datatype t = T of {num_rows: int, num_cols: int, data: Word8.word array} + + fun new {unsafe_skip_table_set} (num_rows, num_cols) = + let + val data = ForkJoin.alloc (num_rows * num_cols) + in + if unsafe_skip_table_set then + () + else + ForkJoin.parfor 1000 (0, num_rows * num_cols) (fn i => + Array.update (data, i, 0w0 : Word8.word)); + T {num_rows = num_rows, num_cols = num_cols, data = data} + end + + fun set (T {num_rows, num_cols, data}) (r, c) b = + Array.update (data, r * num_cols + c, if b then 0w1 else 0w0) + + fun get (T {num_rows, num_cols, data}) (r, c) = + Array.sub (data, r * num_cols + c) = 0w1 + end + + + fun subset_sum {unsafe_skip_table_set} (bag: int Seq.t, goal: int) : + int Seq.t option = + let + val n = Seq.length bag + + val table = + Table.new {unsafe_skip_table_set = unsafe_skip_table_set} + (1 + n, 1 + goal) + fun get (r, c) = Table.get table (r, c) + fun set (r, c) b = + Table.set table (r, c) b + + fun do_node (i, j) = + if j = 0 then set (i, j) true + else if i >= n then set (i, j) false + else if Seq.nth bag i > j then set (i, j) (get (i + 1, j)) + else set (i, j) (get (i + 1, j) orelse get (i + 1, j - Seq.nth bag i)) + + fun do_tile (i_lo, i_hi, j_lo, j_hi) = + let + val i_sz = i_hi - i_lo + val j_sz = j_hi - j_lo + in + if i_sz * j_sz <= 1000 then + Util.forBackwards (i_lo, i_hi) (fn i => + Util.for (j_lo, j_hi) (fn j => do_node (i, j))) + else if i_sz = 1 then + ForkJoin.parfor 1000 (j_lo, j_hi) (fn j => do_node (i_lo, j)) + else if j_sz = 1 then + (* no parallelism is possible within a single column *) + Util.forBackwards (i_lo, i_hi) (fn i => do_node (i, j_lo)) + else + let + val i_mid = i_lo + i_sz div 2 + val j_mid = j_lo + j_sz div 2 + in + do_tile (i_mid, i_hi, j_lo, j_mid); + ForkJoin.par + ( fn () => do_tile (i_lo, i_mid, j_lo, j_mid) + , fn () => do_tile (i_mid, i_hi, j_mid, j_hi) + ); + do_tile (i_lo, i_mid, j_mid, j_hi) + end + end + + fun reconstruct_path acc (i, j) = + if j = 0 then + Seq.fromRevList acc + else + let + val x = Seq.nth bag i + in + if get (i + 1, j) then reconstruct_path acc (i + 1, j) + else reconstruct_path (x :: acc) (i + 1, j - x) + end + in + do_tile (0, n + 1, 0, goal + 1); + + if get (0, goal) then SOME (reconstruct_path [] (0, goal)) else NONE + end + +end diff --git a/tests/subset-sum/main.sml b/tests/subset-sum/main.sml new file mode 100644 index 000000000..e0dc55038 --- /dev/null +++ b/tests/subset-sum/main.sml @@ -0,0 +1,36 @@ +structure CLA = CommandLineArgs + +val bag_str = CLA.parseString "bag" "3,2,3,1,2,1,5,10,10000000,30,10000" +val goal = CLA.parseInt "goal" 10010021 +val unsafe_skip_table_set = CLA.parseFlag "unsafe_skip_table_set" + +val bag = + Seq.fromList (List.map (valOf o Int.fromString) + (String.tokens (fn c => c = #",") bag_str)) + handle _ => Util.die ("parsing -bag ... failed") + +val _ = + if Util.all (0, Seq.length bag) (fn i => Seq.nth bag i > 0) then () + else Util.die ("bag elements must be all >0") + +val _ = if goal >= 0 then () else Util.die ("goal must be >=0") + +val bag_str = let val s = Seq.toString Int.toString bag + in String.substring (s, 1, String.size s - 2) + end +val _ = print ("bag " ^ bag_str ^ "\n") +val _ = print ("goal " ^ Int.toString goal ^ "\n") +val _ = print + ("unsafe_skip_table_set? " ^ (if unsafe_skip_table_set then "yes" else "no") + ^ "\n") + +val result = Benchmark.run "subset-sum" (fn () => + SubsetSumTiled.subset_sum {unsafe_skip_table_set = unsafe_skip_table_set} + (bag, goal)) + +val out_str = + case result of + NONE => "NONE" + | SOME x => "SOME " ^ Seq.toString Int.toString x + +val _ = print ("result " ^ out_str ^ "\n") diff --git a/tests/subset-sum/subset-sum.mlb b/tests/subset-sum/subset-sum.mlb new file mode 100644 index 000000000..a96518275 --- /dev/null +++ b/tests/subset-sum/subset-sum.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +SubsetSumTiled.sml +main.sml \ No newline at end of file diff --git a/tests/suffix-array/AS.sml b/tests/suffix-array/AS.sml new file mode 100644 index 000000000..b3a4705ce --- /dev/null +++ b/tests/suffix-array/AS.sml @@ -0,0 +1,8 @@ +structure AS = +struct + open ArraySlice + open Seq + + val GRAIN = 4096 + val ASupdate = ArraySlice.update +end diff --git a/tests/suffix-array/BruteForce.sml b/tests/suffix-array/BruteForce.sml new file mode 100644 index 000000000..6352310b5 --- /dev/null +++ b/tests/suffix-array/BruteForce.sml @@ -0,0 +1,29 @@ +(* Author: Lawrence Wang (lawrenc2@andrew.cmu.edu, github.com/larry98) + *) + +structure BruteForceSuffixArray :> +sig + val makeSuffixArray : string -> int Seq.t +end = +struct + + fun makeSuffixArray str = + let + val n = String.size str + val sa = AS.tabulate (fn i => i) n + fun cmp k (i, j) = + if i = j then EQUAL + else if i + k >= n then LESS + else if j + k >= n then GREATER + else + let + val c1 = String.sub (str, i + k) + val c2 = String.sub (str, j + k) + in + Char.compare (c1, c2) + end + in + RadixSort.quicksort sa cmp n + end + +end diff --git a/tests/suffix-array/PrefixDoubling.sml b/tests/suffix-array/PrefixDoubling.sml new file mode 100644 index 000000000..686f28606 --- /dev/null +++ b/tests/suffix-array/PrefixDoubling.sml @@ -0,0 +1,256 @@ +(* Author: Lawrence Wang (lawrenc2@andrew.cmu.edu, github.com/larry98) + * + * Based on the prefix doubling approach used by Manbar & Meyers and Larson & + * Sudakane. The general idea is to repeatedly sort the suffixes by their first + * k characters for k = 1, 2, 4, 8, ... This can be done efficiently by keeping + * track of the "ranks" (or groups, buckets, inverse suffix array, etc) of each + * suffix on each round. + * + * Our algorithm maintains sets of "active groups", which are groups of + * suffixes whose k-prefixes are equal. Each of the active groups are processed + * in parallel, and the processing consists of sorting the suffixes in each + * group by their 2k-prefixes, which may create more active groups for the + * next round. We only maintain active groups of size greater than 1, which + * is an optimization made in Larson & Sudakane. This effectively skips over + * the parts of the suffix array which are already in their sorted positions. + * + * Larson & Sudakane also perform additional optimizations, such as using a + * modified version of 3-way pivot quicksort that updates the group numbers + * as part of the sorting routine (essentially it assigns the group number + * when processing the EQUAL partition). We do not implement this optimization + * as our algorithm uses DualPivotQuickSort. + *) +structure PrefixDoublingSuffixArray :> +sig + val makeSuffixArray : string -> int Seq.t +end = +struct + + val GRAIN = 10000 + + structure AS = + struct + open ArraySlice + open Seq + val ASupdate = ArraySlice.update + + fun filter p s = + full (SeqBasis.filter GRAIN (0, length s) (nth s) (p o nth s)) + + fun scanIncl f b s = + let + val a = SeqBasis.scan GRAIN f b (0, length s) (nth s) + in + slice (a, 1, NONE) + end + end + + fun appG grain f s = + ForkJoin.parfor grain (0, AS.length s) (fn i => f (AS.nth s i)) + + fun initCountingSort str = + let + val n = String.size str + + fun bucket i = Char.ord (String.sub (str, i)) + val (sa, offsets) = + CountingSort.sort (AS.tabulate (fn i => i) n) bucket 256 + val offsets = + AS.full (SeqBasis.filter GRAIN (0, AS.length offsets) (AS.nth offsets) + (fn i => i = 0 orelse AS.nth offsets i <> AS.nth offsets (i-1))) + val numGroups = AS.length offsets + val groupNum = ArraySlice.full (ForkJoin.alloc n) + + fun makeGroup offsets n i = + let + val off = AS.nth offsets i + val len = if i = AS.length offsets - 1 then n - off + else AS.nth offsets (i + 1) - off + in + (off, len) + end + + fun updateGroupNum (off, len) = + ForkJoin.parfor GRAIN (off, off + len) (fn j => + AS.ASupdate (groupNum, AS.nth sa j, off) + ) + + val groups = AS.tabulate (makeGroup offsets n) numGroups + val () = appG 1 updateGroupNum groups + in + (sa, groupNum, groups, 1) + end + + fun initSampleSort str = + let + val n = String.size str + + fun pack i = + let + val charToWord = Word64.fromInt o Char.ord + fun getChar i = if i >= n then Char.minChar else String.sub (str, i) + val orb = Word64.orb + val << = Word64.<< + infix 2 << infix 2 orb + val v = charToWord (String.sub (str, i)) << 0w56 + val v = v orb (charToWord (getChar (i + 1)) << 0w48) + val v = v orb (charToWord (getChar (i + 2)) << 0w40) + val v = v orb (charToWord (getChar (i + 3)) << 0w32) + in + v orb (Word64.fromInt i) + end + val words = SampleSort.sort Word64.compare (AS.tabulate pack n) + val idxMask = 0w4294967295 + + val sa = AS.map (fn w => Word64.toInt (Word64.andb (w, idxMask))) words + val groupNum = ArraySlice.full (ForkJoin.alloc n) + + fun eq w1 w2 = Word64.>> (w1, 0w32) = Word64.>> (w2, 0w32) + fun f i = + if i > 0 andalso eq (AS.nth words i) (AS.nth words (i - 1)) + then 0 + else i + val offsets = AS.scanIncl Int.max 0 (AS.tabulate f n) + val maxOffset = AS.nth offsets (n - 1) + val groups = AS.tabulate (fn i => (0, 0)) n + val () = ForkJoin.parfor GRAIN (1, n) (fn i => + let + val off1 = AS.nth offsets (i - 1) + val off2 = AS.nth offsets i + in + AS.ASupdate (groupNum, AS.nth sa i, off2); + if off1 = off2 then () + else AS.ASupdate (groups, i - 1, (off1, off2 - off1)) + end + ) + val () = AS.ASupdate (groupNum, AS.nth sa 0, AS.nth offsets 0) + val () = AS.ASupdate (groups, n - 1, (maxOffset, n - maxOffset)) + in + (sa, groupNum, groups, 4) + end + + fun makeSuffixArray str = + let + val n = String.size str + val (sa, groupNum, groups, k) = initSampleSort str + + fun isActive (off, len) = len > 1 + val activeGroups = AS.filter isActive groups + + val ranks = ArraySlice.full (ForkJoin.alloc n) + val aux = ArraySlice.full (ForkJoin.alloc n) + + fun loop activeGroups k = + if AS.length activeGroups = 0 then () + else if k > n then () + else + let + fun cmp (i, j) = + let + val x = AS.nth ranks i + val y = AS.nth ranks j + in + if x = ~1 andalso y = ~1 then EQUAL + else if x = ~1 then LESS + else if y = ~1 then GREATER + else Int.compare (x, y) + end + + fun sortGroup group = + Quicksort.sortInPlaceG n cmp (AS.subseq sa group) + + fun expandGroupSeq s (off, len) = + let + fun loop i numGroups start = + if i = off + len then ( + AS.ASupdate (s, numGroups, (start, i - start)); + numGroups + 1 + ) else if cmp (AS.nth sa i, AS.nth sa (i - 1)) <> EQUAL then ( + AS.ASupdate (groupNum, AS.nth sa i, i); + AS.ASupdate (s, numGroups, (start, i - start)); + loop (i + 1) (numGroups + 1) i + ) else ( + AS.ASupdate (groupNum, AS.nth sa i, start); + loop (i + 1) numGroups start + ) + val numGroups = loop (off + 1) 0 off + val () = AS.ASupdate (groupNum, AS.nth sa off, off) + in + Util.for (numGroups, len) (fn i => + AS.ASupdate (s, i, (0, 0)) + ) + end + + fun expandGroupPar s (off, len) = + let + fun f i = + if i = 0 then off + else + case cmp (AS.nth sa (off + i), AS.nth sa (off + i - 1)) of + EQUAL => 0 + | _ => off + i + val names = AS.scanIncl Int.max 0 (AS.tabulate f len) + val maxName = AS.nth names (len - 1) + in + ( + ForkJoin.parfor GRAIN (1, len) (fn i => ( + let + val name = AS.nth names i + val name' = AS.nth names (i - 1) + in + AS.ASupdate (groupNum, AS.nth sa (off + i), name); + if AS.nth names i = off + i andalso i > 0 then + AS.ASupdate (s, i - 1, (name', off + i - name')) + else AS.ASupdate (s, i - 1, (0, 0)) + end + )); + AS.ASupdate (groupNum, AS.nth sa off, AS.nth names 0); + AS.ASupdate (s, len - 1, (maxName, off + len - maxName)) + ) + end + + val seqExpand = + AS.length activeGroups > Concurrency.numberOfProcessors + fun expandGroup s (off, len) = + if len <= GRAIN orelse seqExpand then expandGroupSeq s (off, len) + else expandGroupPar s (off, len) + + val groupLens = AS.map #2 activeGroups + val (groupStarts, maxGroups) = AS.scan (op +) 0 groupLens + val avgLen = maxGroups div (AS.length activeGroups) + (* TODO: tune grain size *) + val grain = if avgLen >= 256 then 1 + else if avgLen >= 64 then 32 + else if avgLen >= 16 then 64 + else 4096 + + val () = print ("grain is " ^ (Int.toString grain) ^ "\n") + val () = print ("avgLen is " ^ (Int.toString avgLen) ^ "\n") + val () = print ("numGroups is " ^ (Int.toString (AS.length activeGroups)) ^ "\n") + + (* Its faster to copy all of the ranks instead of just those in + active groups *) + val () = ForkJoin.parfor GRAIN (0, n) (fn i => + let val x = if i + k >= n then ~1 else AS.nth groupNum (i + k) + in AS.ASupdate (ranks, i, x) end + ) + + val newGroups = AS.take aux maxGroups + val () = ForkJoin.parfor grain (0, AS.length activeGroups) (fn i => + let + val group = AS.nth activeGroups i + val start = AS.nth groupStarts i + val s = AS.subseq newGroups (start, #2 group) + in + (sortGroup group; expandGroup s group) + end + ) + in + loop (AS.filter isActive newGroups) (2 * k) + end + val () = loop activeGroups k + in + sa + end + +end diff --git a/tests/suffix-array/main.sml b/tests/suffix-array/main.sml new file mode 100644 index 000000000..fb08bfaa3 --- /dev/null +++ b/tests/suffix-array/main.sml @@ -0,0 +1,104 @@ +structure CLA = CommandLineArgs +(* structure Seq = ArraySequence *) + +val str = CLA.parseString "str" "" +(* val algo = CLA.parseString "algo" "" *) +val check = CLA.parseFlag "check" +val benchmark = CLA.parseFlag "benchmark" +val benchSize = CLA.parseInt "N" 10000000 +val printResult = CLA.parseFlag "print" +val filename = CLA.parseString "file" "" +val rep = case (Int.fromString (CLA.parseString "repeat" "1")) of + SOME(a) => a + | NONE => 1 + +fun load filename = + ReadFile.contents filename + (* let val str = Util.readFile filename + in CharVector.tabulate (Array.length str, fn i => Array.sub (str, i)) end *) + +val str = if filename <> "" then load filename else str + +val maker = + PrefixDoublingSuffixArray.makeSuffixArray + (*if algo = "DC3" then DC3SuffixArray.makeSuffixArray + else if algo = "PD" then PrefixDoublingSuffixArray.makeSuffixArray + else if algo = "BF" then BruteForceSuffixArray.makeSuffixArray + else Util.exit "Unknown algorithm" *) + +(* val _ = MLton.Rusage.measureGC true *) + +(* fun totalGCTime () = + let + val n = Primitives.numberOfProcessors + val time = ref Time.zeroTime + val () = Primitives.for (0, n) (fn i => + (time := Time.+ (!time, Primitives.localGCTimeOfProc i); + time := Time.+ (!time, Primitives.promoTimeOfProc i)) + ) + in + !time + end *) + +fun runTrial str = + let + (* val _ = MLton.GC.collect () *) + (* val gcTime0 = totalGCTime () *) + val t0 = Time.now () + val result = maker str + val t1 = Time.now () + (* val gcTime1 = totalGCTime () *) + val elapsed = Time.toMilliseconds (Time.- (t1, t0)) + (* val gcTimeTotal = Time.toMilliseconds (Time.- (gcTime1, gcTime0)) *) + val gcTimeTotal = 0 + val () = print ("GC: " ^ LargeInt.toString gcTimeTotal ^ " ms\t" + ^ "Total: " ^ LargeInt.toString elapsed ^ " ms\n") + in + result + end + +val result = if str <> "" then runTrial str else Seq.empty () + +val () = + if printResult then + Util.for (0, Seq.length result) (fn i => + print (Int.toString (Seq.nth result i) ^ "\n") + ) + else () + +fun checker str result = + let + val answer = BruteForceSuffixArray.makeSuffixArray str + in + if Seq.equal (op =) (result, answer) + then print "Correct\n" + else print "Incorrect\n" + end + +val _ = if str <> "" andalso check then checker str result else () + +fun runBenchmark () = + let + fun randChar seed = Char.chr (Util.hash seed mod 256) + fun randString n = CharVector.tabulate (n, randChar) + val _ = print ("N " ^ Int.toString benchSize ^ "\n") + val (str, tm1) = Util.getTime (fn _ => randString benchSize) + val _ = print ("generated input in " ^ Time.fmt 4 tm1 ^ "s\n") + + val result = Benchmark.run "running suffix array" (fn _ => maker str) + + val _ = + if not check then () else + let val (_, tm) = + Util.getTime (fn _ => if check then checker str result else ()) + in print ("checking took " ^ Time.fmt 4 tm ^ "s\n") + end + in + () + end + +val _ = if benchmark then runBenchmark () else () + +val _ = + if benchmark then GCStats.report () + else () diff --git a/tests/suffix-array/suffix-array.mlb b/tests/suffix-array/suffix-array.mlb new file mode 100644 index 000000000..91505ac40 --- /dev/null +++ b/tests/suffix-array/suffix-array.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +AS.sml +BruteForce.sml +PrefixDoubling.sml +main.sml diff --git a/tests/tape-delay/main.sml b/tests/tape-delay/main.sml new file mode 100644 index 000000000..2018ae0ec --- /dev/null +++ b/tests/tape-delay/main.sml @@ -0,0 +1,34 @@ +structure CLA = CommandLineArgs + +val infile = + case CLA.positional () of + [x] => x + | _ => Util.die ("[ERR] usage: tape-delay INPUT_FILE [-output OUTPUT_FILE]\n") + +val outfile = CLA.parseString "output" "" + +val delayTime = CLA.parseReal "delay" 0.5 +val decayFactor = CLA.parseReal "decay" 0.2 + +val decaydB = Real.round (20.0 * Math.log10 decayFactor) + +val _ = print ("delay " ^ Real.toString delayTime ^ "s\n") +val _ = print ("decay " ^ Real.toString decayFactor ^ " (" + ^ Int.toString decaydB ^ "dB)\n") + +val (snd, tm) = Util.getTime (fn _ => NewWaveIO.readSound infile) +val _ = print ("read sound in " ^ Time.fmt 4 tm ^ "s\n") + +val esnd = + Benchmark.run "echoing" (fn _ => Signal.delay delayTime decayFactor snd) + +val _ = + if outfile = "" then + print ("use -output file.wav to hear results\n") + else + let + val (_, tm) = Util.getTime (fn _ => NewWaveIO.writeSound esnd outfile) + in + print ("wrote output in " ^ Time.fmt 4 tm ^ "s\n") + end + diff --git a/tests/tape-delay/tape-delay.mlb b/tests/tape-delay/tape-delay.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/tape-delay/tape-delay.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/tinykaboom/TinyKaboom.sml b/tests/tinykaboom/TinyKaboom.sml new file mode 100644 index 000000000..2ca7616e2 --- /dev/null +++ b/tests/tinykaboom/TinyKaboom.sml @@ -0,0 +1,142 @@ +structure TinyKaboom = +struct + +type f32 = f32.real + +val sphere_radius: f32 = 1.5 +val noise_amplitude: f32 = 1.0 + +fun hash n = + let + val x = f32.sin(n)*43758.5453 + in + x-f32.realFloor(x) + end + +type vec3 = vec3.vector + +fun vec3f (x, y, z): vec3 = {x=x,y=y,z=z} + +fun lerp (v0, v1, t) = + v0 + (v1-v0) * f32.max 0.0 (f32.min 1.0 t) + +fun vlerp (v0, v1, t) = + vec3.map2 (fn x => fn y => lerp (x, y, t)) v0 v1 + +fun noise (x: vec3) = + let + val p = {x = f32.realFloor(#x x), y = f32.realFloor(#y x), z = f32.realFloor(#z x)} + val f = {x = #x x - #x p, y = #y x - #y p, z = #z x - #z p} + val f = vec3.scale (vec3.dot (f, vec3.sub ({x=3.0,y=3.0,z=3.0}, vec3.scale 2.0 f))) f + val n = vec3.dot (p, {x=1.0, y=57.0, z=113.0}) + in lerp(lerp(lerp(hash(n + 0.0), hash(n + 1.0), #x f), + lerp(hash(n + 57.0), hash(n + 58.0), #x f), #y f), + lerp(lerp(hash(n + 113.0), hash(n + 114.0), #x f), + lerp(hash(n + 170.0), hash(n + 171.0), #x f), #y f), #z f) + end + +fun rotate v = + vec3f(vec3.dot (vec3f(0.00, 0.80, 0.60), v), + vec3.dot (vec3f(~0.80, 0.36, ~0.48), v), + vec3.dot (vec3f(~0.60, ~0.48, 0.64), v)) + +fun fractal_brownian_motion (x: vec3) = let + val p = rotate x + val f = 0.0 + val f = f + 0.5000*noise p + val p = vec3.scale 2.32 p + val f = f + 0.2500*noise p + val p = vec3.scale 3.03 p + val f = f + 0.1250*noise p + val p = vec3.scale 2.61 p + val f = f + 0.0625*noise p + in f / 0.9375 + end + +fun palette_fire (d: f32): vec3 = let + val yellow = vec3f (1.7, 1.3, 1.0) + val orange = vec3f (1.0, 0.6, 0.0) + val red = vec3f (1.0, 0.0, 0.0) + val darkgray = vec3f (0.2, 0.2, 0.2) + val gray = vec3f (0.4, 0.4, 0.4) + + val x = f32.max 0.0 (f32.min 1.0 d) + in if x < 0.25 then vlerp(gray, darkgray, x*4.0) + else if x < 0.5 then vlerp(darkgray, red, x*4.0-1.0) + else if x < 0.75 then vlerp(red, orange, x*4.0-2.0) + else vlerp(orange, yellow, x*4.0-3.0) + end + +fun signed_distance t p = let + val displacement = ~(fractal_brownian_motion(vec3.scale 3.4 p)) * noise_amplitude + in vec3.norm p - (sphere_radius * f32.sin(t*0.25) + displacement) + end + +fun sq (x: f32) = x * x + +fun loop state continue f = + if continue state then + loop (f state) continue f + else + state + +fun sphere_trace t (orig: vec3, dir: vec3) : (bool * vec3) = let + fun check (i, hit) = (i = 1337, hit) in + if (vec3.dot (orig, orig)) - sq (vec3.dot (orig, dir)) > sq sphere_radius + then (false, orig) + else + check ( + loop (0, orig) (fn (i, _) => i < 64) (fn (i, pos) => + let val d = signed_distance t pos + in if d < 0.0 + then (1337, pos) + else (i + 1, vec3.add (pos, vec3.scale (f32.max (d*0.1) 0.1) dir)) + end) + ) + end + +fun distance_field_normal t pos = let + val eps = 0.1 + val d = signed_distance t pos + val nx = signed_distance t (vec3.add (pos, vec3f(eps, 0.0, 0.0))) - d + val ny = signed_distance t (vec3.add (pos, vec3f(0.0, eps, 0.0))) - d + val nz = signed_distance t (vec3.add (pos, vec3f(0.0, 0.0, eps))) - d + in vec3.normalise (vec3f(nx, ny, nz)) + end + +fun rgb (r: f32) (g: f32) (b: f32) = + let + fun clamp x = f32.min 1.0 (f32.max 0.0 x) + fun ch x = Word8.fromInt (f32.round (clamp x * 255.0)) + in + {red = ch r, green = ch g, blue = ch b} + end + +fun frame (t: f32) (width: int) (height: int): Color.pixel array = let + val fov = f32.pi / 3.0 + fun f j i = let + val dir_x = (f32.fromInt i + 0.5) - f32.fromInt width / 2.0 + val dir_y = ~(f32.fromInt j + 0.5) + f32.fromInt height / 2.0 + val dir_z = ~(f32.fromInt height)/(2.0*f32.tan(fov/2.0)) + val (is_hit, hit) = + sphere_trace t (vec3f(0.0, 0.0, 3.0), + vec3.normalise (vec3f(dir_x, dir_y, dir_z))) + in + if is_hit then let + val noise_level = (sphere_radius - vec3.norm hit)/noise_amplitude + val light_dir = vec3.normalise (vec3.sub (vec3f(10.0, 10.0, 10.0), hit)) + val light_intensity = + f32.max 0.4 (vec3.dot (light_dir, distance_field_normal t hit)) + val {x, y, z} = + vec3.scale light_intensity (palette_fire((noise_level - 0.2)*2.0)) + in rgb x y z + end + else + rgb 0.2 0.7 0.8 + end + in + SeqBasis.tabulate 10 (0, width*height) + (fn k => f (k div width) (k mod width)) + end + +end diff --git a/tests/tinykaboom/f32.sml b/tests/tinykaboom/f32.sml new file mode 100644 index 000000000..5360ef194 --- /dev/null +++ b/tests/tinykaboom/f32.sml @@ -0,0 +1,7 @@ +structure f32 = +struct + open Real32 + open Real32.Math + fun max a b = Real32.max (a, b) + fun min a b = Real32.min (a, b) +end diff --git a/tests/tinykaboom/main.sml b/tests/tinykaboom/main.sml new file mode 100644 index 000000000..9083f54cd --- /dev/null +++ b/tests/tinykaboom/main.sml @@ -0,0 +1,102 @@ +structure CLA = CommandLineArgs + +val fps = CLA.parseInt "fps" 60 +val width = CLA.parseInt "width" 640 +val height = CLA.parseInt "height" 480 +val frames = CLA.parseInt "frames" (10 * fps) +val outfile = CLA.parseString "outfile" "" +(* val frame = CLA.parseInt "frame" 100 *) + +val _ = print ("width " ^ Int.toString width ^ "\n") +val _ = print ("height " ^ Int.toString height ^ "\n") +(* val _ = print ("frame " ^ Int.toString frame ^ "\n") *) +val _ = print ("fps " ^ Int.toString fps ^ "\n") +val _ = print ("frames " ^ Int.toString frames ^ "\n") + +val duration = Real.fromInt frames / Real.fromInt fps + +val _ = print ("(" ^ Real.fmt (StringCvt.FIX (SOME 2)) duration ^ " seconds)\n") + +fun bench () = + let + val _ = print ("generating frames...\n") + val (images, tm) = Util.getTime (fn _ => + SeqBasis.tabulate 1 (0, frames) (fn frame => + { width = width + , height = height + , data = + ArraySlice.full + (TinyKaboom.frame (f32.fromInt frame / f32.fromInt fps) width height) + })) + val _ = print ("generated all frames in " ^ Time.fmt 4 tm ^ "s\n") + val perFrame = Time.fromReal (Time.toReal tm / Real.fromInt frames) + val _ = print ("average time per frame: " ^ Time.fmt 4 perFrame ^ "s\n") + in + images + end + +val images = Benchmark.run "tinykaboom" bench + +val _ = + if outfile = "" then + print ("no output file specified; use -outfile XXX.gif to see result\n") + else + let + val _ = print ("generating palette...\n") + (* val palette = GIF.Palette.summarize [Color.white, Color.black] 256 + { width = width + , height = height + , data = ArraySlice.full (TinyKaboom.frame 5.1667 640 480) + } *) + + fun sampleColor i = + let + val k = Util.hash i + val frame = (k div (width*height)) mod frames + val idx = k mod (width*height) + in + Seq.nth (#data (Array.sub (images, frame))) idx + end + + val palette = GIF.Palette.summarizeBySampling [Color.white, Color.black] 256 + sampleColor + + val blowUpFactor = CLA.parseInt "blowup" 1 + val _ = print ("blowup " ^ Int.toString blowUpFactor ^ "\n") + + fun blowUpImage (image as {width, height, data}) = + if blowUpFactor = 1 then image else + let + val width' = blowUpFactor * width + val height' = blowUpFactor * height + val output = ForkJoin.alloc (width' * height') + val _ = + ForkJoin.parfor 1 (0, height) (fn i => + ForkJoin.parfor (1000 div blowUpFactor) (0, width) (fn j => + let + val c = Seq.nth data (i*width + j) + in + Util.for (0, blowUpFactor) (fn di => + Util.for (0, blowUpFactor) (fn dj => + Array.update (output, (i*blowUpFactor+di)*width' + (j*blowUpFactor+dj), c))) + end)) + in + { width = width' + , height = height' + , data = ArraySlice.full output + } + end + + val _ = print ("writing to " ^ outfile ^"...\n") + val msBetween = Real.round ((1.0 / Real.fromInt fps) * 100.0) + val (_, tm) = Util.getTime (fn _ => + GIF.writeMany outfile msBetween palette + { width = blowUpFactor * width + , height = blowUpFactor * height + , numImages = frames + , getImage = fn i => #remap palette (blowUpImage (Array.sub (images, i))) + }) + val _ = print ("wrote all frames in " ^ Time.fmt 4 tm ^ "s\n") + in + () + end diff --git a/tests/tinykaboom/tinykaboom.mlb b/tests/tinykaboom/tinykaboom.mlb new file mode 100644 index 000000000..5c55ee69c --- /dev/null +++ b/tests/tinykaboom/tinykaboom.mlb @@ -0,0 +1,5 @@ +../mpllib/sources.$(COMPAT).mlb +f32.sml +vec3.sml +TinyKaboom.sml +main.sml diff --git a/tests/tinykaboom/vec3.sml b/tests/tinykaboom/vec3.sml new file mode 100644 index 000000000..5c6399baf --- /dev/null +++ b/tests/tinykaboom/vec3.sml @@ -0,0 +1,20 @@ +structure vec3 = +struct + type f32 = f32.real + + type vector = {x: f32, y: f32, z: f32} + fun add (a: vector, b: vector) = + {x = #x a + #x b, y = #y a + #y b, z = #z a + #z b} + fun sub (a: vector, b: vector) = + {x = #x a - #x b, y = #y a - #y b, z = #z a - #z b} + fun dot (a: vector, b: vector) = + (#x a * #x b) + (#y a * #y b) + (#z a * #z b) + fun scale s ({x,y,z}: vector) = + {x = s*x, y = s*y, z = s*z} + fun map2 f (a: vector) (b: vector) = + {x = f (#x a) (#x b), y = f (#y a) (#y b), z = f (#z a) (#z b)} + fun norm a = + f32.sqrt (dot (a, a)) + fun normalise (v: vector): vector = + scale (1.0 / norm v) v +end diff --git a/tests/to-gif/main.sml b/tests/to-gif/main.sml new file mode 100644 index 000000000..5bf573277 --- /dev/null +++ b/tests/to-gif/main.sml @@ -0,0 +1,39 @@ +structure CLA = CommandLineArgs + +val (input, output) = + case CLA.positional () of + [input, output] => (input, output) + | _ => Util.die "missing filename" + +val (image, tm) = Util.getTime (fn _ => PPM.read input) +val _ = print ("read image in " ^ Time.fmt 4 tm ^ "s\n") + +(* +val w = #width image +val h = #height image + +fun noisy i = + let + val data = Seq.map (fn x => x) (#data image) + in + (* spit on 10% of all pixels *) + Util.for (0, Seq.length data div 10) (fn j => + let + val k = Util.hash (i * Seq.length data + j) mod Seq.length data + in + ArraySlice.update (data, k, Color.red) + end); + {width = w, height = h, data = data} + end + +val (_, tm) = Util.getTime (fn _ => + GIF.writeMany output { width = w + , height = h + , numImages = 10 + , getImage = noisy + }) +*) + +val (_, tm) = Util.getTime (fn _ => GIF.write output image) + +val _ = print ("wrote " ^ output ^ " in " ^ Time.fmt 4 tm ^ "s\n") diff --git a/tests/to-gif/to-gif.mlb b/tests/to-gif/to-gif.mlb new file mode 100644 index 000000000..eb664c9d2 --- /dev/null +++ b/tests/to-gif/to-gif.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +main.sml diff --git a/tests/tokens/tokens.mlb b/tests/tokens/tokens.mlb new file mode 100644 index 000000000..b0046f5d6 --- /dev/null +++ b/tests/tokens/tokens.mlb @@ -0,0 +1,2 @@ +../mpllib/sources.$(COMPAT).mlb +tokens.sml diff --git a/tests/tokens/tokens.sml b/tests/tokens/tokens.sml new file mode 100644 index 000000000..b1afcabf8 --- /dev/null +++ b/tests/tokens/tokens.sml @@ -0,0 +1,48 @@ +structure CLA = CommandLineArgs + +fun usage () = + let + val msg = + "usage: tokens [--verbose] [--no-output] FILE\n" + in + TextIO.output (TextIO.stdErr, msg); + OS.Process.exit OS.Process.failure + end + +val filename = + case CLA.positional () of + [x] => x + | _ => usage () + +val beVerbose = CLA.parseFlag "verbose" +val noOutput = CLA.parseFlag "no-output" + +fun vprint str = + if not beVerbose then () + else TextIO.output (TextIO.stdErr, str) + +val (contents, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filename) +val _ = vprint ("read file in " ^ Time.fmt 4 tm ^ "s\n") + +val tokens = + Benchmark.run "tokenizing" (fn _ => Tokenize.tokensSeq Char.isSpace contents) + +val _ = vprint ("number of tokens " ^ Int.toString (Seq.length tokens) ^ "\n") + +fun put c = TextIO.output1 (TextIO.stdOut, c) +fun putToken token = + Util.for (0, Seq.length token) (put o Seq.nth token) + +val _ = + if noOutput then () + else + let + val (_, tm) = Util.getTime (fn _ => + ArraySlice.app (fn token => (putToken token; put #"\n")) tokens) + in + vprint ("output in " ^ Time.fmt 4 tm ^ "s\n") + end + +val _ = + if beVerbose then GCStats.report () + else () diff --git a/tests/triangle-count/TriangleCount.sml b/tests/triangle-count/TriangleCount.sml new file mode 100644 index 000000000..cf9bd7482 --- /dev/null +++ b/tests/triangle-count/TriangleCount.sml @@ -0,0 +1,125 @@ +structure TriangleCount = +struct + type 'a seq = 'a Seq.t + + structure G = AdjacencyGraph(Int) + structure V = G.Vertex + structure AS = ArraySlice + + type vertex = G.vertex + exception Assert + + (* assumes l <= r and all elements accessible in [l, r) *) + (* ensures that all elements left of the returned index are lesser *) + fun bin_search k s (l, r) = + if l = r then (l, false) + else if (r - l) = 1 then (l, (Seq.nth s l) = k) + else + let + val mid = l + Int.div (r - l - 1, 2) + in + case Int.compare (k, Seq.nth s mid) of + EQUAL => (mid, true) + | LESS => bin_search k s (l, mid) + | GREATER => bin_search k s (mid + 1, r) + end + + fun intersection_count s s' gran = + let + fun countseq1 s1 s2 = + let + val (n1, n2) = (Seq.length s1, Seq.length s2) + fun helper l1 l2 acc = + if (n1 <= l1) orelse (n2 <= l2) then acc + else + case Int.compare (Seq.nth s1 l1, Seq.nth s2 l2) of + EQUAL => helper (l1 + 1) (l2 + 1) (acc + 1) + | LESS => helper (l1 + 1) l2 acc + | GREATER => helper l1 (l2 + 1) acc + in + helper 0 0 0 + end + + fun countseq2 s1 s2 = + let + val (n1, n2) = (Seq.length s1, Seq.length s2) + fun helper l acc = + if l >= n1 then acc + else + let + val k = Seq.nth s1 l + val (idx, found) = bin_search k s2 (0, n2) + val bump = if found then 1 else 0 + in + helper (l + 1) (acc + bump) + end + in + if n2 = 0 then 0 + else helper 0 0 + end + + fun subs s i j = Seq.subseq s (i, j - i) + fun countpar s1 s2 = + let + val (n1, n2) = (Seq.length s1, Seq.length s2) + val nR = n1 + n2 + in + if nR < gran then countseq1 s1 s2 + else if n2 < n1 then countpar s2 s1 + else if n1 < Int.div (gran, 64) then countseq2 s1 s2 + else + let + val mid1 = Int.div (n1, 2) + val k1 = Seq.nth s1 mid1 + val (mid2, found) = bin_search k1 s2 (0, n2) + val bump = if found then 1 else 0 + val (l, r) = ForkJoin.par (fn _ => countpar (subs s1 0 mid1) (subs s2 0 (mid2 + 1 - bump)), + fn _ => countpar (subs s1 (mid1 + 1) n1) (subs s2 (mid2 + bump) n2)) + in + l + bump + r + end + end + val r = countpar s s' + in + r + end + + (* get common vertices greater than min_elt *) + fun intersection_count_thresh s s' gran min_elt = + let + val (n1, n2) = (Seq.length s, Seq.length s') + val (k1, _) = bin_search min_elt s (0, n1) + val (k2, _) = bin_search min_elt s' (0, n2) + fun subs s i j = Seq.subseq s (i, j - i) + in + if (k1 = n1) orelse (k2 = n2) then 0 + else intersection_count (subs s k1 n1) (subs s' k2 n2) gran + end + + fun triangle_count g = + let + fun count u = + let + val ngbrs = G.neighbors g u + val num_ngbrs = Seq.length ngbrs + val (idx, _) = bin_search u ngbrs (0, num_ngbrs) + val ngbrs = Seq.subseq ngbrs (idx, num_ngbrs - idx) + val num_ngbrs = Seq.length ngbrs + fun helpi i = + let + val v = Seq.nth ngbrs i + in + if u < v then + intersection_count_thresh ngbrs (G.neighbors g v) 10000 v + else + 0 + end + val r = SeqBasis.reduce 100 Int.+ 0 (0, num_ngbrs) helpi + in + r + end + (* val tr_counts = Seq.tabulate count (G.numVertices g) *) + in + SeqBasis.reduce 100 op+ 0 (0, G.numVertices g) count + end +end diff --git a/tests/triangle-count/main.sml b/tests/triangle-count/main.sml new file mode 100644 index 000000000..479b92471 --- /dev/null +++ b/tests/triangle-count/main.sml @@ -0,0 +1,71 @@ +structure CLA = CommandLineArgs +structure G = AdjacencyGraph(Int) + +val source = CLA.parseInt "source" 0 +val doCheck = CLA.parseFlag "check" + +(* +val N = CLA.parseInt "N" 10000000 +val D = CLA.parseInt "D" 10 + +val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) +val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") +*) + +val filename = + case CLA.positional () of + [x] => x + | _ => Util.die "missing filename" + +val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) +val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") +val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") + +val (_, tm) = Util.getTime (fn _ => + if G.parityCheck graph then () + else TextIO.output (TextIO.stdErr, + "WARNING: parity check failed; graph might not be symmetric " ^ + "or might have duplicate- or self-edges\n")) +val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") + +val P = Benchmark.run "running tc: " (fn _ => TriangleCount.triangle_count graph) +val _ = print ("num-triangles = " ^ (Int.toString P) ^ "\n") + +(* val _ = LDD.check_ldd graph (#1 P) (#2 P) *) +(* val _ = Benchmark.run "running connectivity" (fn _ => LDD.connectivity graph b) *) +(* +val numVisited = + SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) + (fn i => if Seq.nth P i >= 0 then 1 else 0) +val _ = print ("visited " ^ Int.toString numVisited ^ "\n") + +fun numHops P hops v = + if hops > Seq.length P then ~2 + else if Seq.nth P v = ~1 then ~1 + else if Seq.nth P v = v then hops + else numHops P (hops+1) (Seq.nth P v) + +val maxHops = + SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) +val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") + +fun check () = + let + val (P', serialTime) = + Util.getTime (fn _ => SerialBFS.bfs graph source) + + val correct = + Seq.length P = Seq.length P' + andalso + SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) + (fn i => numHops P 0 i = numHops P' 0 i) + in + print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); + print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") + end + +val _ = if doCheck then check () else () + +val _ = GCStats.report () *) diff --git a/tests/triangle-count/triangle-count.mlb b/tests/triangle-count/triangle-count.mlb new file mode 100644 index 000000000..ffb73081d --- /dev/null +++ b/tests/triangle-count/triangle-count.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +TriangleCount.sml +main.sml diff --git a/tests/wc-opt/WC.sml b/tests/wc-opt/WC.sml new file mode 100644 index 000000000..dc7931f30 --- /dev/null +++ b/tests/wc-opt/WC.sml @@ -0,0 +1,51 @@ +structure WC : +sig + type 'a seq = 'a ArraySequence.t + + (* returns (num lines, num words, num characters) *) + val wc: char seq -> (int * int * int) +end = +struct + + structure ASeq = ArraySequence + type 'a seq = 'a ASeq.t + + fun wc seq = + let + (* + val (a, i, n) = ArraySlice.base seq + val _ = if i = 0 then () else raise Fail "uh oh" + fun nth i = Array.sub (a, i) + *) + fun nth i = ASeq.nth seq i + (* Create a delayed sequence of pairs of integers: + * the first is 1 if it is line break, 0 otherwise; + * the second is 1 if the start of a word, 0 otherwise. + *) + fun isSpace a = (a = #"\n" orelse a = #"\t" orelse a = #" ") + (*val isSpace = Char.isSpace*) + fun f i = + let + val si = nth i + val wordStart = + if (i = 0 orelse isSpace (nth (i-1))) andalso + not (isSpace si) + then 1 else 0 + val lineBreak = if si = #"\n" then 1 else 0 + in + (lineBreak, wordStart) + end + (* val x = Seq.tabulate f (ASeq.length seq) + val (lines, words) = + Seq.reduce (fn ((lb1, ws1), (lb2, ws2)) => (lb1 + lb2, ws1 + ws2)) (0, 0) x *) + val (lines, words) = + SeqBasis.reduce 5000 + (fn ((lb1, ws1), (lb2, ws2)) => (lb1 + lb2, ws1 + ws2)) + (0, 0) + (0, ASeq.length seq) + f + in + (lines, words, ASeq.length seq) + end + +end diff --git a/tests/wc-opt/main.sml b/tests/wc-opt/main.sml new file mode 100644 index 000000000..46cb3a2f2 --- /dev/null +++ b/tests/wc-opt/main.sml @@ -0,0 +1,31 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val filePath = CLA.parseString "infile" "" + +val source = + if filePath = "" then + (*Seq.tabulate (fn _ => #" ") n*) + Seq.tabulate (fn i => Char.chr (Util.hash i mod 255)) n + else + let + val (source, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filePath) + val _ = print ("loadtime " ^ Time.fmt 3 tm ^ "\n") + in + source + end + +fun task () = + WC.wc source + +fun check (lines, words, bytes) = + let + in + print ("correct? checker for wc not implemented yet\n") + end + +val (nl, nw, nb) = Benchmark.run "wc" task +val _ = print ("lines " ^ Int.toString nl ^ "\n") +val _ = print ("words " ^ Int.toString nw ^ "\n") +val _ = print ("chars " ^ Int.toString nb ^ "\n") diff --git a/tests/wc-opt/wc-opt.mlb b/tests/wc-opt/wc-opt.mlb new file mode 100644 index 000000000..7ac8e8c4d --- /dev/null +++ b/tests/wc-opt/wc-opt.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +WC.sml +main.sml diff --git a/tests/wc/MkWC.sml b/tests/wc/MkWC.sml new file mode 100644 index 000000000..609c4066e --- /dev/null +++ b/tests/wc/MkWC.sml @@ -0,0 +1,45 @@ +functor MkWC (Seq : SEQUENCE) : +sig + type 'a seq = 'a ArraySequence.t + + (* returns (num lines, num words, num characters) *) + val wc: char seq -> (int * int * int) +end = +struct + + structure ASeq = ArraySequence + type 'a seq = 'a ASeq.t + + fun wc seq = + let + (* + val (a, i, n) = ArraySlice.base seq + val _ = if i = 0 then () else raise Fail "uh oh" + fun nth i = Array.sub (a, i) + *) + fun nth i = ASeq.nth seq i + (* Create a delayed sequence of pairs of integers: + * the first is 1 if it is line break, 0 otherwise; + * the second is 1 if the start of a word, 0 otherwise. + *) + fun isSpace a = (a = #"\n" orelse a = #"\t" orelse a = #" ") + (*val isSpace = Char.isSpace*) + fun f i = + let + val si = nth i + val wordStart = + if (i = 0 orelse isSpace (nth (i-1))) andalso + not (isSpace si) + then 1 else 0 + val lineBreak = if si = #"\n" then 1 else 0 + in + (lineBreak, wordStart) + end + val x = Seq.tabulate f (ASeq.length seq) + val (lines, words) = + Seq.reduce (fn ((lb1, ws1), (lb2, ws2)) => (lb1 + lb2, ws1 + ws2)) (0, 0) x + in + (lines, words, ASeq.length seq) + end + +end diff --git a/tests/wc/main.sml b/tests/wc/main.sml new file mode 100644 index 000000000..4edddfe4a --- /dev/null +++ b/tests/wc/main.sml @@ -0,0 +1,33 @@ +structure CLA = CommandLineArgs +structure Seq = ArraySequence + +structure WC = MkWC(DelayedSeq) + +val n = CLA.parseInt "n" (1000 * 1000 * 100) +val filePath = CLA.parseString "infile" "" + +val source = + if filePath = "" then + (*Seq.tabulate (fn _ => #" ") n*) + Seq.tabulate (fn i => Char.chr (Util.hash i mod 255)) n + else + let + val (source, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filePath) + val _ = print ("loadtime " ^ Time.fmt 3 tm ^ "\n") + in + source + end + +fun task () = + WC.wc source + +fun check (lines, words, bytes) = + let + in + print ("correct? checker for wc not implemented yet\n") + end + +val (nl, nw, nb) = Benchmark.run "wc" task +val _ = print ("lines " ^ Int.toString nl ^ "\n") +val _ = print ("words " ^ Int.toString nw ^ "\n") +val _ = print ("chars " ^ Int.toString nb ^ "\n") diff --git a/tests/wc/wc.mlb b/tests/wc/wc.mlb new file mode 100644 index 000000000..2110c6e90 --- /dev/null +++ b/tests/wc/wc.mlb @@ -0,0 +1,3 @@ +../mpllib/sources.$(COMPAT).mlb +MkWC.sml +main.sml From b02e6126742fadf60e696c16d4a9225738fd3b8c Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 14:00:42 -0400 Subject: [PATCH 04/18] add brief readme --- tests/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..dadce24cb --- /dev/null +++ b/tests/README.md @@ -0,0 +1,2 @@ +The `mpllib` is a copy of the parallel utilities in `github.com/mpllang/mpllib`. +All other directories hold tests. From aa3b791310272ac55035d31c87e35a24df159ad9 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 14:56:39 -0400 Subject: [PATCH 05/18] move actual test scripts to bench child directory --- tests/{ => bench}/bfs-delayed/MkBFS.sml | 0 tests/{ => bench}/bfs-delayed/SerialBFS.sml | 0 tests/{ => bench}/bfs-delayed/bfs-delayed.mlb | 0 tests/{ => bench}/bfs-delayed/main.sml | 0 tests/{ => bench}/bfs-det-dedup/Dedup.sml | 0 tests/{ => bench}/bfs-det-dedup/DedupBFS.sml | 0 tests/{ => bench}/bfs-det-dedup/OffsetSearch.sml | 0 tests/{ => bench}/bfs-det-dedup/SerialBFS.sml | 0 tests/{ => bench}/bfs-det-dedup/bfs-det-dedup.mlb | 0 tests/{ => bench}/bfs-det-dedup/main.sml | 0 tests/{ => bench}/bfs-det-priority/OffsetSearch.sml | 0 tests/{ => bench}/bfs-det-priority/PriorityBFS.sml | 0 tests/{ => bench}/bfs-det-priority/SerialBFS.sml | 0 .../bfs-det-priority/bfs-det-priority.mlb | 0 tests/{ => bench}/bfs-det-priority/main.sml | 0 .../bfs-tree-entangled-fixed/NondetBFS.sml | 0 .../bfs-tree-entangled-fixed/OffsetSearch.sml | 0 .../bfs-tree-entangled-fixed/SerialBFS.sml | 0 .../bfs-tree-entangled-fixed.mlb | 0 tests/{ => bench}/bfs-tree-entangled-fixed/main.sml | 0 tests/{ => bench}/bfs-tree-entangled/NondetBFS.sml | 0 .../{ => bench}/bfs-tree-entangled/OffsetSearch.sml | 0 tests/{ => bench}/bfs-tree-entangled/SerialBFS.sml | 0 .../bfs-tree-entangled/bfs-tree-entangled.mlb | 0 tests/{ => bench}/bfs-tree-entangled/main.sml | 0 tests/{ => bench}/bfs/NondetBFS.sml | 0 tests/{ => bench}/bfs/OffsetSearch.sml | 0 tests/{ => bench}/bfs/SerialBFS.sml | 0 tests/{ => bench}/bfs/bfs.mlb | 0 tests/{ => bench}/bfs/main.sml | 0 tests/{ => bench}/bignum-add-opt/Add.sml | 0 tests/{ => bench}/bignum-add-opt/bignum-add-opt.mlb | 0 tests/{ => bench}/bignum-add-opt/main.sml | 0 tests/{ => bench}/bignum-add/Bignum.sml | 0 tests/{ => bench}/bignum-add/MkAdd.sml | 0 tests/{ => bench}/bignum-add/SequentialAdd.sml | 0 tests/{ => bench}/bignum-add/bignum-add.mlb | 0 tests/{ => bench}/bignum-add/main.sml | 0 tests/{ => bench}/centrality/BC.sml | 0 tests/{ => bench}/centrality/OffsetSearch.sml | 0 tests/{ => bench}/centrality/centrality.mlb | 0 tests/{ => bench}/centrality/main.sml | 0 tests/{ => bench}/collect/CollectHash.sml | 0 tests/{ => bench}/collect/CollectSort.sml | 0 tests/{ => bench}/collect/HashTable.sml | 0 tests/{ => bench}/collect/KEY.sml | 0 tests/{ => bench}/collect/VALUE.sml | 0 tests/{ => bench}/collect/collect.mlb | 0 tests/{ => bench}/collect/main.sml | 0 tests/{ => bench}/connectivity/Connectivity.sml | 0 tests/{ => bench}/connectivity/connectivity.mlb | 0 tests/{ => bench}/connectivity/main.sml | 0 .../dedup-entangled-fixed/NondetDedup.sml | 0 .../dedup-entangled-fixed/dedup-entangled-fixed.mlb | 0 tests/{ => bench}/dedup-entangled/NondetDedup.sml | 0 .../{ => bench}/dedup-entangled/dedup-entangled.mlb | 0 tests/{ => bench}/dedup/dedup.mlb | 0 tests/{ => bench}/dedup/dedup.sml | 0 .../delaunay-animation/DelaunayTriangulation.sml | 0 tests/{ => bench}/delaunay-animation/Split.sml | 0 .../delaunay-animation/delaunay-animation.mlb | 0 tests/{ => bench}/delaunay-animation/main.sml | 0 tests/{ => bench}/delaunay-animation/test.sml | 0 .../delaunay-mostly-pure/DelaunayTriangulation.sml | 0 .../delaunay-mostly-pure/delaunay-mostly-pure.mlb | 0 tests/{ => bench}/delaunay-mostly-pure/main.sml | 0 tests/{ => bench}/delaunay-mostly-pure/test.sml | 0 .../DelaunayTriangulationTopDown.sml | 0 tests/{ => bench}/delaunay-top-down/HashTable.sml | 0 tests/{ => bench}/delaunay-top-down/README.md | 0 .../delaunay-top-down/delaunay-top-down.mlb | 0 tests/{ => bench}/delaunay-top-down/main.sml | 0 .../{ => bench}/delaunay/DelaunayTriangulation.sml | 0 tests/{ => bench}/delaunay/Split.sml | 0 tests/{ => bench}/delaunay/delaunay.mlb | 0 tests/{ => bench}/delaunay/main.sml | 0 tests/{ => bench}/delaunay/test.sml | 0 tests/{ => bench}/dense-matmul/dense-matmul.mlb | 0 tests/{ => bench}/dense-matmul/main.sml | 0 tests/{ => bench}/fib/fib.mlb | 0 tests/{ => bench}/fib/fib.sml | 0 tests/{ => bench}/flatten/AllBSFlatten.sml | 0 tests/{ => bench}/flatten/BinarySearch.sml | 0 tests/{ => bench}/flatten/BlockedAllBSFlatten.sml | 0 tests/{ => bench}/flatten/ExpandFlatten.sml | 0 tests/{ => bench}/flatten/FullExpandPow2Flatten.sml | 0 tests/{ => bench}/flatten/MultiBlockedBSFlatten.sml | 0 tests/{ => bench}/flatten/SimpleBlockedFlatten.sml | 0 tests/{ => bench}/flatten/SimpleExpandFlatten.sml | 0 tests/{ => bench}/flatten/flatten.mlb | 0 tests/{ => bench}/flatten/flatten.sml | 0 tests/{ => bench}/gif-encode/main.sml | 0 tests/{ => bench}/graphio/graphio.mlb | 0 tests/{ => bench}/graphio/main.sml | 0 tests/{ => bench}/grep-old/Grep.sml | 0 tests/{ => bench}/grep-old/grep-old.mlb | 0 tests/{ => bench}/grep-old/main.sml | 0 tests/{ => bench}/grep/grep.mlb | 0 tests/{ => bench}/grep/main.sml | 0 tests/{ => bench}/high-frag/high-frag.mlb | 0 tests/{ => bench}/high-frag/main.sml | 0 tests/{ => bench}/integrate-opt/Integrate.sml | 0 tests/{ => bench}/integrate-opt/integrate-opt.mlb | 0 tests/{ => bench}/integrate-opt/main.sml | 0 tests/{ => bench}/integrate/MkIntegrate.sml | 0 tests/{ => bench}/integrate/integrate.mlb | 0 tests/{ => bench}/integrate/main.sml | 0 tests/{ => bench}/interval-tree/IntervalTree.sml | 0 tests/{ => bench}/interval-tree/interval-tree.mlb | 0 tests/{ => bench}/interval-tree/main.sml | 0 tests/{ => bench}/interval-tree/new-main.sml | 0 tests/{ => bench}/linearrec-opt/LinearRec.sml | 0 tests/{ => bench}/linearrec-opt/linearrec-opt.mlb | 0 tests/{ => bench}/linearrec-opt/main.sml | 0 tests/{ => bench}/linearrec/MkLinearRec.sml | 0 tests/{ => bench}/linearrec/linearrec.mlb | 0 tests/{ => bench}/linearrec/main.sml | 0 tests/{ => bench}/linefit-opt/LineFit.sml | 0 tests/{ => bench}/linefit-opt/linefit-opt.mlb | 0 tests/{ => bench}/linefit-opt/main.sml | 0 tests/{ => bench}/linefit/MkLineFit.sml | 0 tests/{ => bench}/linefit/linefit.mlb | 0 tests/{ => bench}/linefit/main.sml | 0 tests/{ => bench}/low-d-decomp/LDD.sml | 0 tests/{ => bench}/low-d-decomp/ldd-alt.sml | 0 tests/{ => bench}/low-d-decomp/low-d-decomp.mlb | 0 tests/{ => bench}/low-d-decomp/main.sml | 0 tests/{ => bench}/max-indep-set/MIS.sml | 0 tests/{ => bench}/max-indep-set/faa.mlton.sml | 0 tests/{ => bench}/max-indep-set/faa.mpl.sml | 0 tests/{ => bench}/max-indep-set/main.sml | 0 tests/{ => bench}/max-indep-set/max-indep-set.mlb | 0 tests/{ => bench}/mcss-opt/MCSS.sml | 0 tests/{ => bench}/mcss-opt/main.sml | 0 tests/{ => bench}/mcss-opt/mcss-opt.mlb | 0 tests/{ => bench}/mcss/MkMapReduceMCSS.sml | 0 tests/{ => bench}/mcss/MkScanMCSS.sml | 0 tests/{ => bench}/mcss/main.sml | 0 tests/{ => bench}/mcss/mcss.mlb | 0 tests/{ => bench}/msort-int32/msort-int32.mlb | 0 tests/{ => bench}/msort-int32/msort.sml | 0 tests/{ => bench}/msort-strings/msort-strings.mlb | 0 tests/{ => bench}/msort-strings/msort.sml | 0 tests/{ => bench}/msort/msort.mlb | 0 tests/{ => bench}/msort/msort.sml | 0 tests/{ => bench}/nearest-nbrs/ParseFile.sml | 0 tests/{ => bench}/nearest-nbrs/main.sml | 0 tests/{ => bench}/nearest-nbrs/nearest-nbrs.mlb | 0 tests/{ => bench}/nqueens-simple/main.sml | 0 tests/{ => bench}/nqueens-simple/nqueens-simple.mlb | 0 tests/{ => bench}/nqueens/nqueens.mlb | 0 tests/{ => bench}/nqueens/nqueens.sml | 0 tests/{ => bench}/ocaml-binarytrees5/main.sml | 0 .../ocaml-binarytrees5/ocaml-binarytrees5.mlb | 0 .../{ => bench}/ocaml-binarytrees5/ocaml-source.ml | 0 tests/{ => bench}/ocaml-game-of-life-pure/main.sml | 0 .../ocaml-game-of-life-pure.mlb | 0 tests/{ => bench}/ocaml-game-of-life/main.sml | 0 .../ocaml-game-of-life/ocaml-game-of-life.mlb | 0 .../{ => bench}/ocaml-game-of-life/ocaml-source.ml | 0 tests/{ => bench}/ocaml-lu-decomp/main.sml | 0 .../{ => bench}/ocaml-lu-decomp/ocaml-lu-decomp.mlb | 0 tests/{ => bench}/ocaml-lu-decomp/ocaml-source.ml | 0 tests/{ => bench}/ocaml-mandelbrot/main.sml | 0 .../ocaml-mandelbrot/ocaml-mandelbrot.mlb | 0 tests/{ => bench}/ocaml-mandelbrot/ocaml-source.ml | 0 tests/{ => bench}/ocaml-nbody-imm/README | 0 tests/{ => bench}/ocaml-nbody-imm/main.sml | 0 .../{ => bench}/ocaml-nbody-imm/ocaml-nbody-imm.mlb | 0 tests/{ => bench}/ocaml-nbody-packed/README | 0 tests/{ => bench}/ocaml-nbody-packed/main.sml | 0 .../ocaml-nbody-packed/ocaml-nbody-packed.mlb | 0 tests/{ => bench}/ocaml-nbody/main.sml | 0 tests/{ => bench}/ocaml-nbody/ocaml-nbody.mlb | 0 tests/{ => bench}/ocaml-nbody/ocaml-source.ml | 0 tests/{ => bench}/palindrome/Pal.sml | 0 tests/{ => bench}/palindrome/main.sml | 0 tests/{ => bench}/palindrome/palindrome.mlb | 0 tests/{ => bench}/parens/MkParens.sml | 0 tests/{ => bench}/parens/main.sml | 0 tests/{ => bench}/parens/parens.mlb | 0 tests/{ => bench}/primes-blocked/primes-blocked.mlb | 0 tests/{ => bench}/primes-blocked/primes-blocked.sml | 0 .../primes-segmented/SegmentedPrimes.sml | 0 tests/{ => bench}/primes-segmented/TreeSeq.sml | 0 tests/{ => bench}/primes-segmented/main.sml | 0 .../primes-segmented/primes-segmented.mlb | 0 .../{ => bench}/primes/check-stack-dir/check-stack | Bin tests/{ => bench}/primes/check-stack-dir/primes.mlb | 0 tests/{ => bench}/primes/check-stack-dir/primes.sml | 0 tests/{ => bench}/primes/primes.mlb | 0 tests/{ => bench}/primes/primes.sml | 0 tests/{ => bench}/primes/safe/primes.mlb | 0 tests/{ => bench}/primes/safe/primes.sml | 0 tests/{ => bench}/pure-msort-int32/msort.sml | 0 .../pure-msort-int32/pure-msort-int32.mlb | 0 tests/{ => bench}/pure-msort-strings/msort.sml | 0 .../pure-msort-strings/pure-msort-strings.mlb | 0 tests/{ => bench}/pure-msort/msort.sml | 0 tests/{ => bench}/pure-msort/pure-msort.mlb | 0 tests/{ => bench}/pure-nn/nn.sml | 0 tests/{ => bench}/pure-nn/pure-nn.mlb | 0 tests/{ => bench}/pure-quickhull/Quickhull.sml | 0 tests/{ => bench}/pure-quickhull/Split.sml | 0 tests/{ => bench}/pure-quickhull/TreeSeq.sml | 0 tests/{ => bench}/pure-quickhull/main.sml | 0 tests/{ => bench}/pure-quickhull/pure-quickhull.mlb | 0 tests/{ => bench}/pure-skyline/CityGen.sml | 0 tests/{ => bench}/pure-skyline/FastHashRand.sml | 0 tests/{ => bench}/pure-skyline/Skyline.sml | 0 tests/{ => bench}/pure-skyline/main.sml | 0 tests/{ => bench}/pure-skyline/pure-skyline.mlb | 0 tests/{ => bench}/quickhull/MkOptSplit.sml | 0 tests/{ => bench}/quickhull/MkPurishSplit.sml | 0 tests/{ => bench}/quickhull/MkQuickhull.sml | 0 tests/{ => bench}/quickhull/ParseFile.sml | 0 tests/{ => bench}/quickhull/Quickhull.sml | 0 tests/{ => bench}/quickhull/Split.sml | 0 tests/{ => bench}/quickhull/TreeSeq.sml | 0 tests/{ => bench}/quickhull/main.sml | 0 tests/{ => bench}/quickhull/quickhull.mlb | 0 tests/{ => bench}/random/random.mlb | 0 tests/{ => bench}/random/random.sml | 0 tests/{ => bench}/range-tree/RangeTree.sml | 0 tests/{ => bench}/range-tree/main.sml | 0 tests/{ => bench}/range-tree/range-tree.mlb | 0 tests/{ => bench}/raytracer/main.sml | 0 tests/{ => bench}/raytracer/raytracer.mlb | 0 tests/{ => bench}/reverb/main.sml | 0 tests/{ => bench}/reverb/reverb.mlb | 0 tests/{ => bench}/samplesort/main.sml | 0 tests/{ => bench}/samplesort/samplesort.mlb | 0 tests/{ => bench}/seam-carve-index/README | 0 tests/{ => bench}/seam-carve-index/SCI.sml | 0 .../seam-carve-index/VerticalSeamIndexMap.sml | 0 tests/{ => bench}/seam-carve-index/main.sml | 0 .../seam-carve-index/seam-carve-index.mlb | 0 tests/{ => bench}/seam-carve/SC.sml | 0 tests/{ => bench}/seam-carve/main.sml | 0 tests/{ => bench}/seam-carve/seam-carve.mlb | 0 tests/{ => bench}/shuf/main.sml | 0 tests/{ => bench}/shuf/shuf.mlb | 0 tests/{ => bench}/skyline/CityGen.sml | 0 tests/{ => bench}/skyline/FastHashRand.sml | 0 tests/{ => bench}/skyline/Skyline.sml | 0 tests/{ => bench}/skyline/main.sml | 0 tests/{ => bench}/skyline/skyline.mlb | 0 tests/{ => bench}/spanner/Spanner.sml | 0 tests/{ => bench}/spanner/main.sml | 0 tests/{ => bench}/spanner/spanner.mlb | 0 tests/{ => bench}/sparse-mxv-opt/SparseMxV.sml | 0 tests/{ => bench}/sparse-mxv-opt/main.sml | 0 tests/{ => bench}/sparse-mxv-opt/sparse-mxv-opt.mlb | 0 tests/{ => bench}/sparse-mxv/MkMXV.sml | 0 tests/{ => bench}/sparse-mxv/main.sml | 0 tests/{ => bench}/sparse-mxv/sparse-mxv.mlb | 0 tests/{ => bench}/subset-sum/SubsetSumTiled.sml | 0 tests/{ => bench}/subset-sum/main.sml | 0 tests/{ => bench}/subset-sum/subset-sum.mlb | 0 tests/{ => bench}/suffix-array/AS.sml | 0 tests/{ => bench}/suffix-array/BruteForce.sml | 0 tests/{ => bench}/suffix-array/PrefixDoubling.sml | 0 tests/{ => bench}/suffix-array/main.sml | 0 tests/{ => bench}/suffix-array/suffix-array.mlb | 0 tests/{ => bench}/tape-delay/main.sml | 0 tests/{ => bench}/tape-delay/tape-delay.mlb | 0 tests/{ => bench}/tinykaboom/TinyKaboom.sml | 0 tests/{ => bench}/tinykaboom/f32.sml | 0 tests/{ => bench}/tinykaboom/main.sml | 0 tests/{ => bench}/tinykaboom/tinykaboom.mlb | 0 tests/{ => bench}/tinykaboom/vec3.sml | 0 tests/{ => bench}/to-gif/main.sml | 0 tests/{ => bench}/to-gif/to-gif.mlb | 0 tests/{ => bench}/tokens/tokens.mlb | 0 tests/{ => bench}/tokens/tokens.sml | 0 tests/{ => bench}/triangle-count/TriangleCount.sml | 0 tests/{ => bench}/triangle-count/main.sml | 0 tests/{ => bench}/triangle-count/triangle-count.mlb | 0 tests/{ => bench}/wc-opt/WC.sml | 0 tests/{ => bench}/wc-opt/main.sml | 0 tests/{ => bench}/wc-opt/wc-opt.mlb | 0 tests/{ => bench}/wc/MkWC.sml | 0 tests/{ => bench}/wc/main.sml | 0 tests/{ => bench}/wc/wc.mlb | 0 284 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => bench}/bfs-delayed/MkBFS.sml (100%) rename tests/{ => bench}/bfs-delayed/SerialBFS.sml (100%) rename tests/{ => bench}/bfs-delayed/bfs-delayed.mlb (100%) rename tests/{ => bench}/bfs-delayed/main.sml (100%) rename tests/{ => bench}/bfs-det-dedup/Dedup.sml (100%) rename tests/{ => bench}/bfs-det-dedup/DedupBFS.sml (100%) rename tests/{ => bench}/bfs-det-dedup/OffsetSearch.sml (100%) rename tests/{ => bench}/bfs-det-dedup/SerialBFS.sml (100%) rename tests/{ => bench}/bfs-det-dedup/bfs-det-dedup.mlb (100%) rename tests/{ => bench}/bfs-det-dedup/main.sml (100%) rename tests/{ => bench}/bfs-det-priority/OffsetSearch.sml (100%) rename tests/{ => bench}/bfs-det-priority/PriorityBFS.sml (100%) rename tests/{ => bench}/bfs-det-priority/SerialBFS.sml (100%) rename tests/{ => bench}/bfs-det-priority/bfs-det-priority.mlb (100%) rename tests/{ => bench}/bfs-det-priority/main.sml (100%) rename tests/{ => bench}/bfs-tree-entangled-fixed/NondetBFS.sml (100%) rename tests/{ => bench}/bfs-tree-entangled-fixed/OffsetSearch.sml (100%) rename tests/{ => bench}/bfs-tree-entangled-fixed/SerialBFS.sml (100%) rename tests/{ => bench}/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb (100%) rename tests/{ => bench}/bfs-tree-entangled-fixed/main.sml (100%) rename tests/{ => bench}/bfs-tree-entangled/NondetBFS.sml (100%) rename tests/{ => bench}/bfs-tree-entangled/OffsetSearch.sml (100%) rename tests/{ => bench}/bfs-tree-entangled/SerialBFS.sml (100%) rename tests/{ => bench}/bfs-tree-entangled/bfs-tree-entangled.mlb (100%) rename tests/{ => bench}/bfs-tree-entangled/main.sml (100%) rename tests/{ => bench}/bfs/NondetBFS.sml (100%) rename tests/{ => bench}/bfs/OffsetSearch.sml (100%) rename tests/{ => bench}/bfs/SerialBFS.sml (100%) rename tests/{ => bench}/bfs/bfs.mlb (100%) rename tests/{ => bench}/bfs/main.sml (100%) rename tests/{ => bench}/bignum-add-opt/Add.sml (100%) rename tests/{ => bench}/bignum-add-opt/bignum-add-opt.mlb (100%) rename tests/{ => bench}/bignum-add-opt/main.sml (100%) rename tests/{ => bench}/bignum-add/Bignum.sml (100%) rename tests/{ => bench}/bignum-add/MkAdd.sml (100%) rename tests/{ => bench}/bignum-add/SequentialAdd.sml (100%) rename tests/{ => bench}/bignum-add/bignum-add.mlb (100%) rename tests/{ => bench}/bignum-add/main.sml (100%) rename tests/{ => bench}/centrality/BC.sml (100%) rename tests/{ => bench}/centrality/OffsetSearch.sml (100%) rename tests/{ => bench}/centrality/centrality.mlb (100%) rename tests/{ => bench}/centrality/main.sml (100%) rename tests/{ => bench}/collect/CollectHash.sml (100%) rename tests/{ => bench}/collect/CollectSort.sml (100%) rename tests/{ => bench}/collect/HashTable.sml (100%) rename tests/{ => bench}/collect/KEY.sml (100%) rename tests/{ => bench}/collect/VALUE.sml (100%) rename tests/{ => bench}/collect/collect.mlb (100%) rename tests/{ => bench}/collect/main.sml (100%) rename tests/{ => bench}/connectivity/Connectivity.sml (100%) rename tests/{ => bench}/connectivity/connectivity.mlb (100%) rename tests/{ => bench}/connectivity/main.sml (100%) rename tests/{ => bench}/dedup-entangled-fixed/NondetDedup.sml (100%) rename tests/{ => bench}/dedup-entangled-fixed/dedup-entangled-fixed.mlb (100%) rename tests/{ => bench}/dedup-entangled/NondetDedup.sml (100%) rename tests/{ => bench}/dedup-entangled/dedup-entangled.mlb (100%) rename tests/{ => bench}/dedup/dedup.mlb (100%) rename tests/{ => bench}/dedup/dedup.sml (100%) rename tests/{ => bench}/delaunay-animation/DelaunayTriangulation.sml (100%) rename tests/{ => bench}/delaunay-animation/Split.sml (100%) rename tests/{ => bench}/delaunay-animation/delaunay-animation.mlb (100%) rename tests/{ => bench}/delaunay-animation/main.sml (100%) rename tests/{ => bench}/delaunay-animation/test.sml (100%) rename tests/{ => bench}/delaunay-mostly-pure/DelaunayTriangulation.sml (100%) rename tests/{ => bench}/delaunay-mostly-pure/delaunay-mostly-pure.mlb (100%) rename tests/{ => bench}/delaunay-mostly-pure/main.sml (100%) rename tests/{ => bench}/delaunay-mostly-pure/test.sml (100%) rename tests/{ => bench}/delaunay-top-down/DelaunayTriangulationTopDown.sml (100%) rename tests/{ => bench}/delaunay-top-down/HashTable.sml (100%) rename tests/{ => bench}/delaunay-top-down/README.md (100%) rename tests/{ => bench}/delaunay-top-down/delaunay-top-down.mlb (100%) rename tests/{ => bench}/delaunay-top-down/main.sml (100%) rename tests/{ => bench}/delaunay/DelaunayTriangulation.sml (100%) rename tests/{ => bench}/delaunay/Split.sml (100%) rename tests/{ => bench}/delaunay/delaunay.mlb (100%) rename tests/{ => bench}/delaunay/main.sml (100%) rename tests/{ => bench}/delaunay/test.sml (100%) rename tests/{ => bench}/dense-matmul/dense-matmul.mlb (100%) rename tests/{ => bench}/dense-matmul/main.sml (100%) rename tests/{ => bench}/fib/fib.mlb (100%) rename tests/{ => bench}/fib/fib.sml (100%) rename tests/{ => bench}/flatten/AllBSFlatten.sml (100%) rename tests/{ => bench}/flatten/BinarySearch.sml (100%) rename tests/{ => bench}/flatten/BlockedAllBSFlatten.sml (100%) rename tests/{ => bench}/flatten/ExpandFlatten.sml (100%) rename tests/{ => bench}/flatten/FullExpandPow2Flatten.sml (100%) rename tests/{ => bench}/flatten/MultiBlockedBSFlatten.sml (100%) rename tests/{ => bench}/flatten/SimpleBlockedFlatten.sml (100%) rename tests/{ => bench}/flatten/SimpleExpandFlatten.sml (100%) rename tests/{ => bench}/flatten/flatten.mlb (100%) rename tests/{ => bench}/flatten/flatten.sml (100%) rename tests/{ => bench}/gif-encode/main.sml (100%) rename tests/{ => bench}/graphio/graphio.mlb (100%) rename tests/{ => bench}/graphio/main.sml (100%) rename tests/{ => bench}/grep-old/Grep.sml (100%) rename tests/{ => bench}/grep-old/grep-old.mlb (100%) rename tests/{ => bench}/grep-old/main.sml (100%) rename tests/{ => bench}/grep/grep.mlb (100%) rename tests/{ => bench}/grep/main.sml (100%) rename tests/{ => bench}/high-frag/high-frag.mlb (100%) rename tests/{ => bench}/high-frag/main.sml (100%) rename tests/{ => bench}/integrate-opt/Integrate.sml (100%) rename tests/{ => bench}/integrate-opt/integrate-opt.mlb (100%) rename tests/{ => bench}/integrate-opt/main.sml (100%) rename tests/{ => bench}/integrate/MkIntegrate.sml (100%) rename tests/{ => bench}/integrate/integrate.mlb (100%) rename tests/{ => bench}/integrate/main.sml (100%) rename tests/{ => bench}/interval-tree/IntervalTree.sml (100%) rename tests/{ => bench}/interval-tree/interval-tree.mlb (100%) rename tests/{ => bench}/interval-tree/main.sml (100%) rename tests/{ => bench}/interval-tree/new-main.sml (100%) rename tests/{ => bench}/linearrec-opt/LinearRec.sml (100%) rename tests/{ => bench}/linearrec-opt/linearrec-opt.mlb (100%) rename tests/{ => bench}/linearrec-opt/main.sml (100%) rename tests/{ => bench}/linearrec/MkLinearRec.sml (100%) rename tests/{ => bench}/linearrec/linearrec.mlb (100%) rename tests/{ => bench}/linearrec/main.sml (100%) rename tests/{ => bench}/linefit-opt/LineFit.sml (100%) rename tests/{ => bench}/linefit-opt/linefit-opt.mlb (100%) rename tests/{ => bench}/linefit-opt/main.sml (100%) rename tests/{ => bench}/linefit/MkLineFit.sml (100%) rename tests/{ => bench}/linefit/linefit.mlb (100%) rename tests/{ => bench}/linefit/main.sml (100%) rename tests/{ => bench}/low-d-decomp/LDD.sml (100%) rename tests/{ => bench}/low-d-decomp/ldd-alt.sml (100%) rename tests/{ => bench}/low-d-decomp/low-d-decomp.mlb (100%) rename tests/{ => bench}/low-d-decomp/main.sml (100%) rename tests/{ => bench}/max-indep-set/MIS.sml (100%) rename tests/{ => bench}/max-indep-set/faa.mlton.sml (100%) rename tests/{ => bench}/max-indep-set/faa.mpl.sml (100%) rename tests/{ => bench}/max-indep-set/main.sml (100%) rename tests/{ => bench}/max-indep-set/max-indep-set.mlb (100%) rename tests/{ => bench}/mcss-opt/MCSS.sml (100%) rename tests/{ => bench}/mcss-opt/main.sml (100%) rename tests/{ => bench}/mcss-opt/mcss-opt.mlb (100%) rename tests/{ => bench}/mcss/MkMapReduceMCSS.sml (100%) rename tests/{ => bench}/mcss/MkScanMCSS.sml (100%) rename tests/{ => bench}/mcss/main.sml (100%) rename tests/{ => bench}/mcss/mcss.mlb (100%) rename tests/{ => bench}/msort-int32/msort-int32.mlb (100%) rename tests/{ => bench}/msort-int32/msort.sml (100%) rename tests/{ => bench}/msort-strings/msort-strings.mlb (100%) rename tests/{ => bench}/msort-strings/msort.sml (100%) rename tests/{ => bench}/msort/msort.mlb (100%) rename tests/{ => bench}/msort/msort.sml (100%) rename tests/{ => bench}/nearest-nbrs/ParseFile.sml (100%) rename tests/{ => bench}/nearest-nbrs/main.sml (100%) rename tests/{ => bench}/nearest-nbrs/nearest-nbrs.mlb (100%) rename tests/{ => bench}/nqueens-simple/main.sml (100%) rename tests/{ => bench}/nqueens-simple/nqueens-simple.mlb (100%) rename tests/{ => bench}/nqueens/nqueens.mlb (100%) rename tests/{ => bench}/nqueens/nqueens.sml (100%) rename tests/{ => bench}/ocaml-binarytrees5/main.sml (100%) rename tests/{ => bench}/ocaml-binarytrees5/ocaml-binarytrees5.mlb (100%) rename tests/{ => bench}/ocaml-binarytrees5/ocaml-source.ml (100%) rename tests/{ => bench}/ocaml-game-of-life-pure/main.sml (100%) rename tests/{ => bench}/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb (100%) rename tests/{ => bench}/ocaml-game-of-life/main.sml (100%) rename tests/{ => bench}/ocaml-game-of-life/ocaml-game-of-life.mlb (100%) rename tests/{ => bench}/ocaml-game-of-life/ocaml-source.ml (100%) rename tests/{ => bench}/ocaml-lu-decomp/main.sml (100%) rename tests/{ => bench}/ocaml-lu-decomp/ocaml-lu-decomp.mlb (100%) rename tests/{ => bench}/ocaml-lu-decomp/ocaml-source.ml (100%) rename tests/{ => bench}/ocaml-mandelbrot/main.sml (100%) rename tests/{ => bench}/ocaml-mandelbrot/ocaml-mandelbrot.mlb (100%) rename tests/{ => bench}/ocaml-mandelbrot/ocaml-source.ml (100%) rename tests/{ => bench}/ocaml-nbody-imm/README (100%) rename tests/{ => bench}/ocaml-nbody-imm/main.sml (100%) rename tests/{ => bench}/ocaml-nbody-imm/ocaml-nbody-imm.mlb (100%) rename tests/{ => bench}/ocaml-nbody-packed/README (100%) rename tests/{ => bench}/ocaml-nbody-packed/main.sml (100%) rename tests/{ => bench}/ocaml-nbody-packed/ocaml-nbody-packed.mlb (100%) rename tests/{ => bench}/ocaml-nbody/main.sml (100%) rename tests/{ => bench}/ocaml-nbody/ocaml-nbody.mlb (100%) rename tests/{ => bench}/ocaml-nbody/ocaml-source.ml (100%) rename tests/{ => bench}/palindrome/Pal.sml (100%) rename tests/{ => bench}/palindrome/main.sml (100%) rename tests/{ => bench}/palindrome/palindrome.mlb (100%) rename tests/{ => bench}/parens/MkParens.sml (100%) rename tests/{ => bench}/parens/main.sml (100%) rename tests/{ => bench}/parens/parens.mlb (100%) rename tests/{ => bench}/primes-blocked/primes-blocked.mlb (100%) rename tests/{ => bench}/primes-blocked/primes-blocked.sml (100%) rename tests/{ => bench}/primes-segmented/SegmentedPrimes.sml (100%) rename tests/{ => bench}/primes-segmented/TreeSeq.sml (100%) rename tests/{ => bench}/primes-segmented/main.sml (100%) rename tests/{ => bench}/primes-segmented/primes-segmented.mlb (100%) rename tests/{ => bench}/primes/check-stack-dir/check-stack (100%) rename tests/{ => bench}/primes/check-stack-dir/primes.mlb (100%) rename tests/{ => bench}/primes/check-stack-dir/primes.sml (100%) rename tests/{ => bench}/primes/primes.mlb (100%) rename tests/{ => bench}/primes/primes.sml (100%) rename tests/{ => bench}/primes/safe/primes.mlb (100%) rename tests/{ => bench}/primes/safe/primes.sml (100%) rename tests/{ => bench}/pure-msort-int32/msort.sml (100%) rename tests/{ => bench}/pure-msort-int32/pure-msort-int32.mlb (100%) rename tests/{ => bench}/pure-msort-strings/msort.sml (100%) rename tests/{ => bench}/pure-msort-strings/pure-msort-strings.mlb (100%) rename tests/{ => bench}/pure-msort/msort.sml (100%) rename tests/{ => bench}/pure-msort/pure-msort.mlb (100%) rename tests/{ => bench}/pure-nn/nn.sml (100%) rename tests/{ => bench}/pure-nn/pure-nn.mlb (100%) rename tests/{ => bench}/pure-quickhull/Quickhull.sml (100%) rename tests/{ => bench}/pure-quickhull/Split.sml (100%) rename tests/{ => bench}/pure-quickhull/TreeSeq.sml (100%) rename tests/{ => bench}/pure-quickhull/main.sml (100%) rename tests/{ => bench}/pure-quickhull/pure-quickhull.mlb (100%) rename tests/{ => bench}/pure-skyline/CityGen.sml (100%) rename tests/{ => bench}/pure-skyline/FastHashRand.sml (100%) rename tests/{ => bench}/pure-skyline/Skyline.sml (100%) rename tests/{ => bench}/pure-skyline/main.sml (100%) rename tests/{ => bench}/pure-skyline/pure-skyline.mlb (100%) rename tests/{ => bench}/quickhull/MkOptSplit.sml (100%) rename tests/{ => bench}/quickhull/MkPurishSplit.sml (100%) rename tests/{ => bench}/quickhull/MkQuickhull.sml (100%) rename tests/{ => bench}/quickhull/ParseFile.sml (100%) rename tests/{ => bench}/quickhull/Quickhull.sml (100%) rename tests/{ => bench}/quickhull/Split.sml (100%) rename tests/{ => bench}/quickhull/TreeSeq.sml (100%) rename tests/{ => bench}/quickhull/main.sml (100%) rename tests/{ => bench}/quickhull/quickhull.mlb (100%) rename tests/{ => bench}/random/random.mlb (100%) rename tests/{ => bench}/random/random.sml (100%) rename tests/{ => bench}/range-tree/RangeTree.sml (100%) rename tests/{ => bench}/range-tree/main.sml (100%) rename tests/{ => bench}/range-tree/range-tree.mlb (100%) rename tests/{ => bench}/raytracer/main.sml (100%) rename tests/{ => bench}/raytracer/raytracer.mlb (100%) rename tests/{ => bench}/reverb/main.sml (100%) rename tests/{ => bench}/reverb/reverb.mlb (100%) rename tests/{ => bench}/samplesort/main.sml (100%) rename tests/{ => bench}/samplesort/samplesort.mlb (100%) rename tests/{ => bench}/seam-carve-index/README (100%) rename tests/{ => bench}/seam-carve-index/SCI.sml (100%) rename tests/{ => bench}/seam-carve-index/VerticalSeamIndexMap.sml (100%) rename tests/{ => bench}/seam-carve-index/main.sml (100%) rename tests/{ => bench}/seam-carve-index/seam-carve-index.mlb (100%) rename tests/{ => bench}/seam-carve/SC.sml (100%) rename tests/{ => bench}/seam-carve/main.sml (100%) rename tests/{ => bench}/seam-carve/seam-carve.mlb (100%) rename tests/{ => bench}/shuf/main.sml (100%) rename tests/{ => bench}/shuf/shuf.mlb (100%) rename tests/{ => bench}/skyline/CityGen.sml (100%) rename tests/{ => bench}/skyline/FastHashRand.sml (100%) rename tests/{ => bench}/skyline/Skyline.sml (100%) rename tests/{ => bench}/skyline/main.sml (100%) rename tests/{ => bench}/skyline/skyline.mlb (100%) rename tests/{ => bench}/spanner/Spanner.sml (100%) rename tests/{ => bench}/spanner/main.sml (100%) rename tests/{ => bench}/spanner/spanner.mlb (100%) rename tests/{ => bench}/sparse-mxv-opt/SparseMxV.sml (100%) rename tests/{ => bench}/sparse-mxv-opt/main.sml (100%) rename tests/{ => bench}/sparse-mxv-opt/sparse-mxv-opt.mlb (100%) rename tests/{ => bench}/sparse-mxv/MkMXV.sml (100%) rename tests/{ => bench}/sparse-mxv/main.sml (100%) rename tests/{ => bench}/sparse-mxv/sparse-mxv.mlb (100%) rename tests/{ => bench}/subset-sum/SubsetSumTiled.sml (100%) rename tests/{ => bench}/subset-sum/main.sml (100%) rename tests/{ => bench}/subset-sum/subset-sum.mlb (100%) rename tests/{ => bench}/suffix-array/AS.sml (100%) rename tests/{ => bench}/suffix-array/BruteForce.sml (100%) rename tests/{ => bench}/suffix-array/PrefixDoubling.sml (100%) rename tests/{ => bench}/suffix-array/main.sml (100%) rename tests/{ => bench}/suffix-array/suffix-array.mlb (100%) rename tests/{ => bench}/tape-delay/main.sml (100%) rename tests/{ => bench}/tape-delay/tape-delay.mlb (100%) rename tests/{ => bench}/tinykaboom/TinyKaboom.sml (100%) rename tests/{ => bench}/tinykaboom/f32.sml (100%) rename tests/{ => bench}/tinykaboom/main.sml (100%) rename tests/{ => bench}/tinykaboom/tinykaboom.mlb (100%) rename tests/{ => bench}/tinykaboom/vec3.sml (100%) rename tests/{ => bench}/to-gif/main.sml (100%) rename tests/{ => bench}/to-gif/to-gif.mlb (100%) rename tests/{ => bench}/tokens/tokens.mlb (100%) rename tests/{ => bench}/tokens/tokens.sml (100%) rename tests/{ => bench}/triangle-count/TriangleCount.sml (100%) rename tests/{ => bench}/triangle-count/main.sml (100%) rename tests/{ => bench}/triangle-count/triangle-count.mlb (100%) rename tests/{ => bench}/wc-opt/WC.sml (100%) rename tests/{ => bench}/wc-opt/main.sml (100%) rename tests/{ => bench}/wc-opt/wc-opt.mlb (100%) rename tests/{ => bench}/wc/MkWC.sml (100%) rename tests/{ => bench}/wc/main.sml (100%) rename tests/{ => bench}/wc/wc.mlb (100%) diff --git a/tests/bfs-delayed/MkBFS.sml b/tests/bench/bfs-delayed/MkBFS.sml similarity index 100% rename from tests/bfs-delayed/MkBFS.sml rename to tests/bench/bfs-delayed/MkBFS.sml diff --git a/tests/bfs-delayed/SerialBFS.sml b/tests/bench/bfs-delayed/SerialBFS.sml similarity index 100% rename from tests/bfs-delayed/SerialBFS.sml rename to tests/bench/bfs-delayed/SerialBFS.sml diff --git a/tests/bfs-delayed/bfs-delayed.mlb b/tests/bench/bfs-delayed/bfs-delayed.mlb similarity index 100% rename from tests/bfs-delayed/bfs-delayed.mlb rename to tests/bench/bfs-delayed/bfs-delayed.mlb diff --git a/tests/bfs-delayed/main.sml b/tests/bench/bfs-delayed/main.sml similarity index 100% rename from tests/bfs-delayed/main.sml rename to tests/bench/bfs-delayed/main.sml diff --git a/tests/bfs-det-dedup/Dedup.sml b/tests/bench/bfs-det-dedup/Dedup.sml similarity index 100% rename from tests/bfs-det-dedup/Dedup.sml rename to tests/bench/bfs-det-dedup/Dedup.sml diff --git a/tests/bfs-det-dedup/DedupBFS.sml b/tests/bench/bfs-det-dedup/DedupBFS.sml similarity index 100% rename from tests/bfs-det-dedup/DedupBFS.sml rename to tests/bench/bfs-det-dedup/DedupBFS.sml diff --git a/tests/bfs-det-dedup/OffsetSearch.sml b/tests/bench/bfs-det-dedup/OffsetSearch.sml similarity index 100% rename from tests/bfs-det-dedup/OffsetSearch.sml rename to tests/bench/bfs-det-dedup/OffsetSearch.sml diff --git a/tests/bfs-det-dedup/SerialBFS.sml b/tests/bench/bfs-det-dedup/SerialBFS.sml similarity index 100% rename from tests/bfs-det-dedup/SerialBFS.sml rename to tests/bench/bfs-det-dedup/SerialBFS.sml diff --git a/tests/bfs-det-dedup/bfs-det-dedup.mlb b/tests/bench/bfs-det-dedup/bfs-det-dedup.mlb similarity index 100% rename from tests/bfs-det-dedup/bfs-det-dedup.mlb rename to tests/bench/bfs-det-dedup/bfs-det-dedup.mlb diff --git a/tests/bfs-det-dedup/main.sml b/tests/bench/bfs-det-dedup/main.sml similarity index 100% rename from tests/bfs-det-dedup/main.sml rename to tests/bench/bfs-det-dedup/main.sml diff --git a/tests/bfs-det-priority/OffsetSearch.sml b/tests/bench/bfs-det-priority/OffsetSearch.sml similarity index 100% rename from tests/bfs-det-priority/OffsetSearch.sml rename to tests/bench/bfs-det-priority/OffsetSearch.sml diff --git a/tests/bfs-det-priority/PriorityBFS.sml b/tests/bench/bfs-det-priority/PriorityBFS.sml similarity index 100% rename from tests/bfs-det-priority/PriorityBFS.sml rename to tests/bench/bfs-det-priority/PriorityBFS.sml diff --git a/tests/bfs-det-priority/SerialBFS.sml b/tests/bench/bfs-det-priority/SerialBFS.sml similarity index 100% rename from tests/bfs-det-priority/SerialBFS.sml rename to tests/bench/bfs-det-priority/SerialBFS.sml diff --git a/tests/bfs-det-priority/bfs-det-priority.mlb b/tests/bench/bfs-det-priority/bfs-det-priority.mlb similarity index 100% rename from tests/bfs-det-priority/bfs-det-priority.mlb rename to tests/bench/bfs-det-priority/bfs-det-priority.mlb diff --git a/tests/bfs-det-priority/main.sml b/tests/bench/bfs-det-priority/main.sml similarity index 100% rename from tests/bfs-det-priority/main.sml rename to tests/bench/bfs-det-priority/main.sml diff --git a/tests/bfs-tree-entangled-fixed/NondetBFS.sml b/tests/bench/bfs-tree-entangled-fixed/NondetBFS.sml similarity index 100% rename from tests/bfs-tree-entangled-fixed/NondetBFS.sml rename to tests/bench/bfs-tree-entangled-fixed/NondetBFS.sml diff --git a/tests/bfs-tree-entangled-fixed/OffsetSearch.sml b/tests/bench/bfs-tree-entangled-fixed/OffsetSearch.sml similarity index 100% rename from tests/bfs-tree-entangled-fixed/OffsetSearch.sml rename to tests/bench/bfs-tree-entangled-fixed/OffsetSearch.sml diff --git a/tests/bfs-tree-entangled-fixed/SerialBFS.sml b/tests/bench/bfs-tree-entangled-fixed/SerialBFS.sml similarity index 100% rename from tests/bfs-tree-entangled-fixed/SerialBFS.sml rename to tests/bench/bfs-tree-entangled-fixed/SerialBFS.sml diff --git a/tests/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb b/tests/bench/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb similarity index 100% rename from tests/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb rename to tests/bench/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb diff --git a/tests/bfs-tree-entangled-fixed/main.sml b/tests/bench/bfs-tree-entangled-fixed/main.sml similarity index 100% rename from tests/bfs-tree-entangled-fixed/main.sml rename to tests/bench/bfs-tree-entangled-fixed/main.sml diff --git a/tests/bfs-tree-entangled/NondetBFS.sml b/tests/bench/bfs-tree-entangled/NondetBFS.sml similarity index 100% rename from tests/bfs-tree-entangled/NondetBFS.sml rename to tests/bench/bfs-tree-entangled/NondetBFS.sml diff --git a/tests/bfs-tree-entangled/OffsetSearch.sml b/tests/bench/bfs-tree-entangled/OffsetSearch.sml similarity index 100% rename from tests/bfs-tree-entangled/OffsetSearch.sml rename to tests/bench/bfs-tree-entangled/OffsetSearch.sml diff --git a/tests/bfs-tree-entangled/SerialBFS.sml b/tests/bench/bfs-tree-entangled/SerialBFS.sml similarity index 100% rename from tests/bfs-tree-entangled/SerialBFS.sml rename to tests/bench/bfs-tree-entangled/SerialBFS.sml diff --git a/tests/bfs-tree-entangled/bfs-tree-entangled.mlb b/tests/bench/bfs-tree-entangled/bfs-tree-entangled.mlb similarity index 100% rename from tests/bfs-tree-entangled/bfs-tree-entangled.mlb rename to tests/bench/bfs-tree-entangled/bfs-tree-entangled.mlb diff --git a/tests/bfs-tree-entangled/main.sml b/tests/bench/bfs-tree-entangled/main.sml similarity index 100% rename from tests/bfs-tree-entangled/main.sml rename to tests/bench/bfs-tree-entangled/main.sml diff --git a/tests/bfs/NondetBFS.sml b/tests/bench/bfs/NondetBFS.sml similarity index 100% rename from tests/bfs/NondetBFS.sml rename to tests/bench/bfs/NondetBFS.sml diff --git a/tests/bfs/OffsetSearch.sml b/tests/bench/bfs/OffsetSearch.sml similarity index 100% rename from tests/bfs/OffsetSearch.sml rename to tests/bench/bfs/OffsetSearch.sml diff --git a/tests/bfs/SerialBFS.sml b/tests/bench/bfs/SerialBFS.sml similarity index 100% rename from tests/bfs/SerialBFS.sml rename to tests/bench/bfs/SerialBFS.sml diff --git a/tests/bfs/bfs.mlb b/tests/bench/bfs/bfs.mlb similarity index 100% rename from tests/bfs/bfs.mlb rename to tests/bench/bfs/bfs.mlb diff --git a/tests/bfs/main.sml b/tests/bench/bfs/main.sml similarity index 100% rename from tests/bfs/main.sml rename to tests/bench/bfs/main.sml diff --git a/tests/bignum-add-opt/Add.sml b/tests/bench/bignum-add-opt/Add.sml similarity index 100% rename from tests/bignum-add-opt/Add.sml rename to tests/bench/bignum-add-opt/Add.sml diff --git a/tests/bignum-add-opt/bignum-add-opt.mlb b/tests/bench/bignum-add-opt/bignum-add-opt.mlb similarity index 100% rename from tests/bignum-add-opt/bignum-add-opt.mlb rename to tests/bench/bignum-add-opt/bignum-add-opt.mlb diff --git a/tests/bignum-add-opt/main.sml b/tests/bench/bignum-add-opt/main.sml similarity index 100% rename from tests/bignum-add-opt/main.sml rename to tests/bench/bignum-add-opt/main.sml diff --git a/tests/bignum-add/Bignum.sml b/tests/bench/bignum-add/Bignum.sml similarity index 100% rename from tests/bignum-add/Bignum.sml rename to tests/bench/bignum-add/Bignum.sml diff --git a/tests/bignum-add/MkAdd.sml b/tests/bench/bignum-add/MkAdd.sml similarity index 100% rename from tests/bignum-add/MkAdd.sml rename to tests/bench/bignum-add/MkAdd.sml diff --git a/tests/bignum-add/SequentialAdd.sml b/tests/bench/bignum-add/SequentialAdd.sml similarity index 100% rename from tests/bignum-add/SequentialAdd.sml rename to tests/bench/bignum-add/SequentialAdd.sml diff --git a/tests/bignum-add/bignum-add.mlb b/tests/bench/bignum-add/bignum-add.mlb similarity index 100% rename from tests/bignum-add/bignum-add.mlb rename to tests/bench/bignum-add/bignum-add.mlb diff --git a/tests/bignum-add/main.sml b/tests/bench/bignum-add/main.sml similarity index 100% rename from tests/bignum-add/main.sml rename to tests/bench/bignum-add/main.sml diff --git a/tests/centrality/BC.sml b/tests/bench/centrality/BC.sml similarity index 100% rename from tests/centrality/BC.sml rename to tests/bench/centrality/BC.sml diff --git a/tests/centrality/OffsetSearch.sml b/tests/bench/centrality/OffsetSearch.sml similarity index 100% rename from tests/centrality/OffsetSearch.sml rename to tests/bench/centrality/OffsetSearch.sml diff --git a/tests/centrality/centrality.mlb b/tests/bench/centrality/centrality.mlb similarity index 100% rename from tests/centrality/centrality.mlb rename to tests/bench/centrality/centrality.mlb diff --git a/tests/centrality/main.sml b/tests/bench/centrality/main.sml similarity index 100% rename from tests/centrality/main.sml rename to tests/bench/centrality/main.sml diff --git a/tests/collect/CollectHash.sml b/tests/bench/collect/CollectHash.sml similarity index 100% rename from tests/collect/CollectHash.sml rename to tests/bench/collect/CollectHash.sml diff --git a/tests/collect/CollectSort.sml b/tests/bench/collect/CollectSort.sml similarity index 100% rename from tests/collect/CollectSort.sml rename to tests/bench/collect/CollectSort.sml diff --git a/tests/collect/HashTable.sml b/tests/bench/collect/HashTable.sml similarity index 100% rename from tests/collect/HashTable.sml rename to tests/bench/collect/HashTable.sml diff --git a/tests/collect/KEY.sml b/tests/bench/collect/KEY.sml similarity index 100% rename from tests/collect/KEY.sml rename to tests/bench/collect/KEY.sml diff --git a/tests/collect/VALUE.sml b/tests/bench/collect/VALUE.sml similarity index 100% rename from tests/collect/VALUE.sml rename to tests/bench/collect/VALUE.sml diff --git a/tests/collect/collect.mlb b/tests/bench/collect/collect.mlb similarity index 100% rename from tests/collect/collect.mlb rename to tests/bench/collect/collect.mlb diff --git a/tests/collect/main.sml b/tests/bench/collect/main.sml similarity index 100% rename from tests/collect/main.sml rename to tests/bench/collect/main.sml diff --git a/tests/connectivity/Connectivity.sml b/tests/bench/connectivity/Connectivity.sml similarity index 100% rename from tests/connectivity/Connectivity.sml rename to tests/bench/connectivity/Connectivity.sml diff --git a/tests/connectivity/connectivity.mlb b/tests/bench/connectivity/connectivity.mlb similarity index 100% rename from tests/connectivity/connectivity.mlb rename to tests/bench/connectivity/connectivity.mlb diff --git a/tests/connectivity/main.sml b/tests/bench/connectivity/main.sml similarity index 100% rename from tests/connectivity/main.sml rename to tests/bench/connectivity/main.sml diff --git a/tests/dedup-entangled-fixed/NondetDedup.sml b/tests/bench/dedup-entangled-fixed/NondetDedup.sml similarity index 100% rename from tests/dedup-entangled-fixed/NondetDedup.sml rename to tests/bench/dedup-entangled-fixed/NondetDedup.sml diff --git a/tests/dedup-entangled-fixed/dedup-entangled-fixed.mlb b/tests/bench/dedup-entangled-fixed/dedup-entangled-fixed.mlb similarity index 100% rename from tests/dedup-entangled-fixed/dedup-entangled-fixed.mlb rename to tests/bench/dedup-entangled-fixed/dedup-entangled-fixed.mlb diff --git a/tests/dedup-entangled/NondetDedup.sml b/tests/bench/dedup-entangled/NondetDedup.sml similarity index 100% rename from tests/dedup-entangled/NondetDedup.sml rename to tests/bench/dedup-entangled/NondetDedup.sml diff --git a/tests/dedup-entangled/dedup-entangled.mlb b/tests/bench/dedup-entangled/dedup-entangled.mlb similarity index 100% rename from tests/dedup-entangled/dedup-entangled.mlb rename to tests/bench/dedup-entangled/dedup-entangled.mlb diff --git a/tests/dedup/dedup.mlb b/tests/bench/dedup/dedup.mlb similarity index 100% rename from tests/dedup/dedup.mlb rename to tests/bench/dedup/dedup.mlb diff --git a/tests/dedup/dedup.sml b/tests/bench/dedup/dedup.sml similarity index 100% rename from tests/dedup/dedup.sml rename to tests/bench/dedup/dedup.sml diff --git a/tests/delaunay-animation/DelaunayTriangulation.sml b/tests/bench/delaunay-animation/DelaunayTriangulation.sml similarity index 100% rename from tests/delaunay-animation/DelaunayTriangulation.sml rename to tests/bench/delaunay-animation/DelaunayTriangulation.sml diff --git a/tests/delaunay-animation/Split.sml b/tests/bench/delaunay-animation/Split.sml similarity index 100% rename from tests/delaunay-animation/Split.sml rename to tests/bench/delaunay-animation/Split.sml diff --git a/tests/delaunay-animation/delaunay-animation.mlb b/tests/bench/delaunay-animation/delaunay-animation.mlb similarity index 100% rename from tests/delaunay-animation/delaunay-animation.mlb rename to tests/bench/delaunay-animation/delaunay-animation.mlb diff --git a/tests/delaunay-animation/main.sml b/tests/bench/delaunay-animation/main.sml similarity index 100% rename from tests/delaunay-animation/main.sml rename to tests/bench/delaunay-animation/main.sml diff --git a/tests/delaunay-animation/test.sml b/tests/bench/delaunay-animation/test.sml similarity index 100% rename from tests/delaunay-animation/test.sml rename to tests/bench/delaunay-animation/test.sml diff --git a/tests/delaunay-mostly-pure/DelaunayTriangulation.sml b/tests/bench/delaunay-mostly-pure/DelaunayTriangulation.sml similarity index 100% rename from tests/delaunay-mostly-pure/DelaunayTriangulation.sml rename to tests/bench/delaunay-mostly-pure/DelaunayTriangulation.sml diff --git a/tests/delaunay-mostly-pure/delaunay-mostly-pure.mlb b/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb similarity index 100% rename from tests/delaunay-mostly-pure/delaunay-mostly-pure.mlb rename to tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb diff --git a/tests/delaunay-mostly-pure/main.sml b/tests/bench/delaunay-mostly-pure/main.sml similarity index 100% rename from tests/delaunay-mostly-pure/main.sml rename to tests/bench/delaunay-mostly-pure/main.sml diff --git a/tests/delaunay-mostly-pure/test.sml b/tests/bench/delaunay-mostly-pure/test.sml similarity index 100% rename from tests/delaunay-mostly-pure/test.sml rename to tests/bench/delaunay-mostly-pure/test.sml diff --git a/tests/delaunay-top-down/DelaunayTriangulationTopDown.sml b/tests/bench/delaunay-top-down/DelaunayTriangulationTopDown.sml similarity index 100% rename from tests/delaunay-top-down/DelaunayTriangulationTopDown.sml rename to tests/bench/delaunay-top-down/DelaunayTriangulationTopDown.sml diff --git a/tests/delaunay-top-down/HashTable.sml b/tests/bench/delaunay-top-down/HashTable.sml similarity index 100% rename from tests/delaunay-top-down/HashTable.sml rename to tests/bench/delaunay-top-down/HashTable.sml diff --git a/tests/delaunay-top-down/README.md b/tests/bench/delaunay-top-down/README.md similarity index 100% rename from tests/delaunay-top-down/README.md rename to tests/bench/delaunay-top-down/README.md diff --git a/tests/delaunay-top-down/delaunay-top-down.mlb b/tests/bench/delaunay-top-down/delaunay-top-down.mlb similarity index 100% rename from tests/delaunay-top-down/delaunay-top-down.mlb rename to tests/bench/delaunay-top-down/delaunay-top-down.mlb diff --git a/tests/delaunay-top-down/main.sml b/tests/bench/delaunay-top-down/main.sml similarity index 100% rename from tests/delaunay-top-down/main.sml rename to tests/bench/delaunay-top-down/main.sml diff --git a/tests/delaunay/DelaunayTriangulation.sml b/tests/bench/delaunay/DelaunayTriangulation.sml similarity index 100% rename from tests/delaunay/DelaunayTriangulation.sml rename to tests/bench/delaunay/DelaunayTriangulation.sml diff --git a/tests/delaunay/Split.sml b/tests/bench/delaunay/Split.sml similarity index 100% rename from tests/delaunay/Split.sml rename to tests/bench/delaunay/Split.sml diff --git a/tests/delaunay/delaunay.mlb b/tests/bench/delaunay/delaunay.mlb similarity index 100% rename from tests/delaunay/delaunay.mlb rename to tests/bench/delaunay/delaunay.mlb diff --git a/tests/delaunay/main.sml b/tests/bench/delaunay/main.sml similarity index 100% rename from tests/delaunay/main.sml rename to tests/bench/delaunay/main.sml diff --git a/tests/delaunay/test.sml b/tests/bench/delaunay/test.sml similarity index 100% rename from tests/delaunay/test.sml rename to tests/bench/delaunay/test.sml diff --git a/tests/dense-matmul/dense-matmul.mlb b/tests/bench/dense-matmul/dense-matmul.mlb similarity index 100% rename from tests/dense-matmul/dense-matmul.mlb rename to tests/bench/dense-matmul/dense-matmul.mlb diff --git a/tests/dense-matmul/main.sml b/tests/bench/dense-matmul/main.sml similarity index 100% rename from tests/dense-matmul/main.sml rename to tests/bench/dense-matmul/main.sml diff --git a/tests/fib/fib.mlb b/tests/bench/fib/fib.mlb similarity index 100% rename from tests/fib/fib.mlb rename to tests/bench/fib/fib.mlb diff --git a/tests/fib/fib.sml b/tests/bench/fib/fib.sml similarity index 100% rename from tests/fib/fib.sml rename to tests/bench/fib/fib.sml diff --git a/tests/flatten/AllBSFlatten.sml b/tests/bench/flatten/AllBSFlatten.sml similarity index 100% rename from tests/flatten/AllBSFlatten.sml rename to tests/bench/flatten/AllBSFlatten.sml diff --git a/tests/flatten/BinarySearch.sml b/tests/bench/flatten/BinarySearch.sml similarity index 100% rename from tests/flatten/BinarySearch.sml rename to tests/bench/flatten/BinarySearch.sml diff --git a/tests/flatten/BlockedAllBSFlatten.sml b/tests/bench/flatten/BlockedAllBSFlatten.sml similarity index 100% rename from tests/flatten/BlockedAllBSFlatten.sml rename to tests/bench/flatten/BlockedAllBSFlatten.sml diff --git a/tests/flatten/ExpandFlatten.sml b/tests/bench/flatten/ExpandFlatten.sml similarity index 100% rename from tests/flatten/ExpandFlatten.sml rename to tests/bench/flatten/ExpandFlatten.sml diff --git a/tests/flatten/FullExpandPow2Flatten.sml b/tests/bench/flatten/FullExpandPow2Flatten.sml similarity index 100% rename from tests/flatten/FullExpandPow2Flatten.sml rename to tests/bench/flatten/FullExpandPow2Flatten.sml diff --git a/tests/flatten/MultiBlockedBSFlatten.sml b/tests/bench/flatten/MultiBlockedBSFlatten.sml similarity index 100% rename from tests/flatten/MultiBlockedBSFlatten.sml rename to tests/bench/flatten/MultiBlockedBSFlatten.sml diff --git a/tests/flatten/SimpleBlockedFlatten.sml b/tests/bench/flatten/SimpleBlockedFlatten.sml similarity index 100% rename from tests/flatten/SimpleBlockedFlatten.sml rename to tests/bench/flatten/SimpleBlockedFlatten.sml diff --git a/tests/flatten/SimpleExpandFlatten.sml b/tests/bench/flatten/SimpleExpandFlatten.sml similarity index 100% rename from tests/flatten/SimpleExpandFlatten.sml rename to tests/bench/flatten/SimpleExpandFlatten.sml diff --git a/tests/flatten/flatten.mlb b/tests/bench/flatten/flatten.mlb similarity index 100% rename from tests/flatten/flatten.mlb rename to tests/bench/flatten/flatten.mlb diff --git a/tests/flatten/flatten.sml b/tests/bench/flatten/flatten.sml similarity index 100% rename from tests/flatten/flatten.sml rename to tests/bench/flatten/flatten.sml diff --git a/tests/gif-encode/main.sml b/tests/bench/gif-encode/main.sml similarity index 100% rename from tests/gif-encode/main.sml rename to tests/bench/gif-encode/main.sml diff --git a/tests/graphio/graphio.mlb b/tests/bench/graphio/graphio.mlb similarity index 100% rename from tests/graphio/graphio.mlb rename to tests/bench/graphio/graphio.mlb diff --git a/tests/graphio/main.sml b/tests/bench/graphio/main.sml similarity index 100% rename from tests/graphio/main.sml rename to tests/bench/graphio/main.sml diff --git a/tests/grep-old/Grep.sml b/tests/bench/grep-old/Grep.sml similarity index 100% rename from tests/grep-old/Grep.sml rename to tests/bench/grep-old/Grep.sml diff --git a/tests/grep-old/grep-old.mlb b/tests/bench/grep-old/grep-old.mlb similarity index 100% rename from tests/grep-old/grep-old.mlb rename to tests/bench/grep-old/grep-old.mlb diff --git a/tests/grep-old/main.sml b/tests/bench/grep-old/main.sml similarity index 100% rename from tests/grep-old/main.sml rename to tests/bench/grep-old/main.sml diff --git a/tests/grep/grep.mlb b/tests/bench/grep/grep.mlb similarity index 100% rename from tests/grep/grep.mlb rename to tests/bench/grep/grep.mlb diff --git a/tests/grep/main.sml b/tests/bench/grep/main.sml similarity index 100% rename from tests/grep/main.sml rename to tests/bench/grep/main.sml diff --git a/tests/high-frag/high-frag.mlb b/tests/bench/high-frag/high-frag.mlb similarity index 100% rename from tests/high-frag/high-frag.mlb rename to tests/bench/high-frag/high-frag.mlb diff --git a/tests/high-frag/main.sml b/tests/bench/high-frag/main.sml similarity index 100% rename from tests/high-frag/main.sml rename to tests/bench/high-frag/main.sml diff --git a/tests/integrate-opt/Integrate.sml b/tests/bench/integrate-opt/Integrate.sml similarity index 100% rename from tests/integrate-opt/Integrate.sml rename to tests/bench/integrate-opt/Integrate.sml diff --git a/tests/integrate-opt/integrate-opt.mlb b/tests/bench/integrate-opt/integrate-opt.mlb similarity index 100% rename from tests/integrate-opt/integrate-opt.mlb rename to tests/bench/integrate-opt/integrate-opt.mlb diff --git a/tests/integrate-opt/main.sml b/tests/bench/integrate-opt/main.sml similarity index 100% rename from tests/integrate-opt/main.sml rename to tests/bench/integrate-opt/main.sml diff --git a/tests/integrate/MkIntegrate.sml b/tests/bench/integrate/MkIntegrate.sml similarity index 100% rename from tests/integrate/MkIntegrate.sml rename to tests/bench/integrate/MkIntegrate.sml diff --git a/tests/integrate/integrate.mlb b/tests/bench/integrate/integrate.mlb similarity index 100% rename from tests/integrate/integrate.mlb rename to tests/bench/integrate/integrate.mlb diff --git a/tests/integrate/main.sml b/tests/bench/integrate/main.sml similarity index 100% rename from tests/integrate/main.sml rename to tests/bench/integrate/main.sml diff --git a/tests/interval-tree/IntervalTree.sml b/tests/bench/interval-tree/IntervalTree.sml similarity index 100% rename from tests/interval-tree/IntervalTree.sml rename to tests/bench/interval-tree/IntervalTree.sml diff --git a/tests/interval-tree/interval-tree.mlb b/tests/bench/interval-tree/interval-tree.mlb similarity index 100% rename from tests/interval-tree/interval-tree.mlb rename to tests/bench/interval-tree/interval-tree.mlb diff --git a/tests/interval-tree/main.sml b/tests/bench/interval-tree/main.sml similarity index 100% rename from tests/interval-tree/main.sml rename to tests/bench/interval-tree/main.sml diff --git a/tests/interval-tree/new-main.sml b/tests/bench/interval-tree/new-main.sml similarity index 100% rename from tests/interval-tree/new-main.sml rename to tests/bench/interval-tree/new-main.sml diff --git a/tests/linearrec-opt/LinearRec.sml b/tests/bench/linearrec-opt/LinearRec.sml similarity index 100% rename from tests/linearrec-opt/LinearRec.sml rename to tests/bench/linearrec-opt/LinearRec.sml diff --git a/tests/linearrec-opt/linearrec-opt.mlb b/tests/bench/linearrec-opt/linearrec-opt.mlb similarity index 100% rename from tests/linearrec-opt/linearrec-opt.mlb rename to tests/bench/linearrec-opt/linearrec-opt.mlb diff --git a/tests/linearrec-opt/main.sml b/tests/bench/linearrec-opt/main.sml similarity index 100% rename from tests/linearrec-opt/main.sml rename to tests/bench/linearrec-opt/main.sml diff --git a/tests/linearrec/MkLinearRec.sml b/tests/bench/linearrec/MkLinearRec.sml similarity index 100% rename from tests/linearrec/MkLinearRec.sml rename to tests/bench/linearrec/MkLinearRec.sml diff --git a/tests/linearrec/linearrec.mlb b/tests/bench/linearrec/linearrec.mlb similarity index 100% rename from tests/linearrec/linearrec.mlb rename to tests/bench/linearrec/linearrec.mlb diff --git a/tests/linearrec/main.sml b/tests/bench/linearrec/main.sml similarity index 100% rename from tests/linearrec/main.sml rename to tests/bench/linearrec/main.sml diff --git a/tests/linefit-opt/LineFit.sml b/tests/bench/linefit-opt/LineFit.sml similarity index 100% rename from tests/linefit-opt/LineFit.sml rename to tests/bench/linefit-opt/LineFit.sml diff --git a/tests/linefit-opt/linefit-opt.mlb b/tests/bench/linefit-opt/linefit-opt.mlb similarity index 100% rename from tests/linefit-opt/linefit-opt.mlb rename to tests/bench/linefit-opt/linefit-opt.mlb diff --git a/tests/linefit-opt/main.sml b/tests/bench/linefit-opt/main.sml similarity index 100% rename from tests/linefit-opt/main.sml rename to tests/bench/linefit-opt/main.sml diff --git a/tests/linefit/MkLineFit.sml b/tests/bench/linefit/MkLineFit.sml similarity index 100% rename from tests/linefit/MkLineFit.sml rename to tests/bench/linefit/MkLineFit.sml diff --git a/tests/linefit/linefit.mlb b/tests/bench/linefit/linefit.mlb similarity index 100% rename from tests/linefit/linefit.mlb rename to tests/bench/linefit/linefit.mlb diff --git a/tests/linefit/main.sml b/tests/bench/linefit/main.sml similarity index 100% rename from tests/linefit/main.sml rename to tests/bench/linefit/main.sml diff --git a/tests/low-d-decomp/LDD.sml b/tests/bench/low-d-decomp/LDD.sml similarity index 100% rename from tests/low-d-decomp/LDD.sml rename to tests/bench/low-d-decomp/LDD.sml diff --git a/tests/low-d-decomp/ldd-alt.sml b/tests/bench/low-d-decomp/ldd-alt.sml similarity index 100% rename from tests/low-d-decomp/ldd-alt.sml rename to tests/bench/low-d-decomp/ldd-alt.sml diff --git a/tests/low-d-decomp/low-d-decomp.mlb b/tests/bench/low-d-decomp/low-d-decomp.mlb similarity index 100% rename from tests/low-d-decomp/low-d-decomp.mlb rename to tests/bench/low-d-decomp/low-d-decomp.mlb diff --git a/tests/low-d-decomp/main.sml b/tests/bench/low-d-decomp/main.sml similarity index 100% rename from tests/low-d-decomp/main.sml rename to tests/bench/low-d-decomp/main.sml diff --git a/tests/max-indep-set/MIS.sml b/tests/bench/max-indep-set/MIS.sml similarity index 100% rename from tests/max-indep-set/MIS.sml rename to tests/bench/max-indep-set/MIS.sml diff --git a/tests/max-indep-set/faa.mlton.sml b/tests/bench/max-indep-set/faa.mlton.sml similarity index 100% rename from tests/max-indep-set/faa.mlton.sml rename to tests/bench/max-indep-set/faa.mlton.sml diff --git a/tests/max-indep-set/faa.mpl.sml b/tests/bench/max-indep-set/faa.mpl.sml similarity index 100% rename from tests/max-indep-set/faa.mpl.sml rename to tests/bench/max-indep-set/faa.mpl.sml diff --git a/tests/max-indep-set/main.sml b/tests/bench/max-indep-set/main.sml similarity index 100% rename from tests/max-indep-set/main.sml rename to tests/bench/max-indep-set/main.sml diff --git a/tests/max-indep-set/max-indep-set.mlb b/tests/bench/max-indep-set/max-indep-set.mlb similarity index 100% rename from tests/max-indep-set/max-indep-set.mlb rename to tests/bench/max-indep-set/max-indep-set.mlb diff --git a/tests/mcss-opt/MCSS.sml b/tests/bench/mcss-opt/MCSS.sml similarity index 100% rename from tests/mcss-opt/MCSS.sml rename to tests/bench/mcss-opt/MCSS.sml diff --git a/tests/mcss-opt/main.sml b/tests/bench/mcss-opt/main.sml similarity index 100% rename from tests/mcss-opt/main.sml rename to tests/bench/mcss-opt/main.sml diff --git a/tests/mcss-opt/mcss-opt.mlb b/tests/bench/mcss-opt/mcss-opt.mlb similarity index 100% rename from tests/mcss-opt/mcss-opt.mlb rename to tests/bench/mcss-opt/mcss-opt.mlb diff --git a/tests/mcss/MkMapReduceMCSS.sml b/tests/bench/mcss/MkMapReduceMCSS.sml similarity index 100% rename from tests/mcss/MkMapReduceMCSS.sml rename to tests/bench/mcss/MkMapReduceMCSS.sml diff --git a/tests/mcss/MkScanMCSS.sml b/tests/bench/mcss/MkScanMCSS.sml similarity index 100% rename from tests/mcss/MkScanMCSS.sml rename to tests/bench/mcss/MkScanMCSS.sml diff --git a/tests/mcss/main.sml b/tests/bench/mcss/main.sml similarity index 100% rename from tests/mcss/main.sml rename to tests/bench/mcss/main.sml diff --git a/tests/mcss/mcss.mlb b/tests/bench/mcss/mcss.mlb similarity index 100% rename from tests/mcss/mcss.mlb rename to tests/bench/mcss/mcss.mlb diff --git a/tests/msort-int32/msort-int32.mlb b/tests/bench/msort-int32/msort-int32.mlb similarity index 100% rename from tests/msort-int32/msort-int32.mlb rename to tests/bench/msort-int32/msort-int32.mlb diff --git a/tests/msort-int32/msort.sml b/tests/bench/msort-int32/msort.sml similarity index 100% rename from tests/msort-int32/msort.sml rename to tests/bench/msort-int32/msort.sml diff --git a/tests/msort-strings/msort-strings.mlb b/tests/bench/msort-strings/msort-strings.mlb similarity index 100% rename from tests/msort-strings/msort-strings.mlb rename to tests/bench/msort-strings/msort-strings.mlb diff --git a/tests/msort-strings/msort.sml b/tests/bench/msort-strings/msort.sml similarity index 100% rename from tests/msort-strings/msort.sml rename to tests/bench/msort-strings/msort.sml diff --git a/tests/msort/msort.mlb b/tests/bench/msort/msort.mlb similarity index 100% rename from tests/msort/msort.mlb rename to tests/bench/msort/msort.mlb diff --git a/tests/msort/msort.sml b/tests/bench/msort/msort.sml similarity index 100% rename from tests/msort/msort.sml rename to tests/bench/msort/msort.sml diff --git a/tests/nearest-nbrs/ParseFile.sml b/tests/bench/nearest-nbrs/ParseFile.sml similarity index 100% rename from tests/nearest-nbrs/ParseFile.sml rename to tests/bench/nearest-nbrs/ParseFile.sml diff --git a/tests/nearest-nbrs/main.sml b/tests/bench/nearest-nbrs/main.sml similarity index 100% rename from tests/nearest-nbrs/main.sml rename to tests/bench/nearest-nbrs/main.sml diff --git a/tests/nearest-nbrs/nearest-nbrs.mlb b/tests/bench/nearest-nbrs/nearest-nbrs.mlb similarity index 100% rename from tests/nearest-nbrs/nearest-nbrs.mlb rename to tests/bench/nearest-nbrs/nearest-nbrs.mlb diff --git a/tests/nqueens-simple/main.sml b/tests/bench/nqueens-simple/main.sml similarity index 100% rename from tests/nqueens-simple/main.sml rename to tests/bench/nqueens-simple/main.sml diff --git a/tests/nqueens-simple/nqueens-simple.mlb b/tests/bench/nqueens-simple/nqueens-simple.mlb similarity index 100% rename from tests/nqueens-simple/nqueens-simple.mlb rename to tests/bench/nqueens-simple/nqueens-simple.mlb diff --git a/tests/nqueens/nqueens.mlb b/tests/bench/nqueens/nqueens.mlb similarity index 100% rename from tests/nqueens/nqueens.mlb rename to tests/bench/nqueens/nqueens.mlb diff --git a/tests/nqueens/nqueens.sml b/tests/bench/nqueens/nqueens.sml similarity index 100% rename from tests/nqueens/nqueens.sml rename to tests/bench/nqueens/nqueens.sml diff --git a/tests/ocaml-binarytrees5/main.sml b/tests/bench/ocaml-binarytrees5/main.sml similarity index 100% rename from tests/ocaml-binarytrees5/main.sml rename to tests/bench/ocaml-binarytrees5/main.sml diff --git a/tests/ocaml-binarytrees5/ocaml-binarytrees5.mlb b/tests/bench/ocaml-binarytrees5/ocaml-binarytrees5.mlb similarity index 100% rename from tests/ocaml-binarytrees5/ocaml-binarytrees5.mlb rename to tests/bench/ocaml-binarytrees5/ocaml-binarytrees5.mlb diff --git a/tests/ocaml-binarytrees5/ocaml-source.ml b/tests/bench/ocaml-binarytrees5/ocaml-source.ml similarity index 100% rename from tests/ocaml-binarytrees5/ocaml-source.ml rename to tests/bench/ocaml-binarytrees5/ocaml-source.ml diff --git a/tests/ocaml-game-of-life-pure/main.sml b/tests/bench/ocaml-game-of-life-pure/main.sml similarity index 100% rename from tests/ocaml-game-of-life-pure/main.sml rename to tests/bench/ocaml-game-of-life-pure/main.sml diff --git a/tests/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb b/tests/bench/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb similarity index 100% rename from tests/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb rename to tests/bench/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb diff --git a/tests/ocaml-game-of-life/main.sml b/tests/bench/ocaml-game-of-life/main.sml similarity index 100% rename from tests/ocaml-game-of-life/main.sml rename to tests/bench/ocaml-game-of-life/main.sml diff --git a/tests/ocaml-game-of-life/ocaml-game-of-life.mlb b/tests/bench/ocaml-game-of-life/ocaml-game-of-life.mlb similarity index 100% rename from tests/ocaml-game-of-life/ocaml-game-of-life.mlb rename to tests/bench/ocaml-game-of-life/ocaml-game-of-life.mlb diff --git a/tests/ocaml-game-of-life/ocaml-source.ml b/tests/bench/ocaml-game-of-life/ocaml-source.ml similarity index 100% rename from tests/ocaml-game-of-life/ocaml-source.ml rename to tests/bench/ocaml-game-of-life/ocaml-source.ml diff --git a/tests/ocaml-lu-decomp/main.sml b/tests/bench/ocaml-lu-decomp/main.sml similarity index 100% rename from tests/ocaml-lu-decomp/main.sml rename to tests/bench/ocaml-lu-decomp/main.sml diff --git a/tests/ocaml-lu-decomp/ocaml-lu-decomp.mlb b/tests/bench/ocaml-lu-decomp/ocaml-lu-decomp.mlb similarity index 100% rename from tests/ocaml-lu-decomp/ocaml-lu-decomp.mlb rename to tests/bench/ocaml-lu-decomp/ocaml-lu-decomp.mlb diff --git a/tests/ocaml-lu-decomp/ocaml-source.ml b/tests/bench/ocaml-lu-decomp/ocaml-source.ml similarity index 100% rename from tests/ocaml-lu-decomp/ocaml-source.ml rename to tests/bench/ocaml-lu-decomp/ocaml-source.ml diff --git a/tests/ocaml-mandelbrot/main.sml b/tests/bench/ocaml-mandelbrot/main.sml similarity index 100% rename from tests/ocaml-mandelbrot/main.sml rename to tests/bench/ocaml-mandelbrot/main.sml diff --git a/tests/ocaml-mandelbrot/ocaml-mandelbrot.mlb b/tests/bench/ocaml-mandelbrot/ocaml-mandelbrot.mlb similarity index 100% rename from tests/ocaml-mandelbrot/ocaml-mandelbrot.mlb rename to tests/bench/ocaml-mandelbrot/ocaml-mandelbrot.mlb diff --git a/tests/ocaml-mandelbrot/ocaml-source.ml b/tests/bench/ocaml-mandelbrot/ocaml-source.ml similarity index 100% rename from tests/ocaml-mandelbrot/ocaml-source.ml rename to tests/bench/ocaml-mandelbrot/ocaml-source.ml diff --git a/tests/ocaml-nbody-imm/README b/tests/bench/ocaml-nbody-imm/README similarity index 100% rename from tests/ocaml-nbody-imm/README rename to tests/bench/ocaml-nbody-imm/README diff --git a/tests/ocaml-nbody-imm/main.sml b/tests/bench/ocaml-nbody-imm/main.sml similarity index 100% rename from tests/ocaml-nbody-imm/main.sml rename to tests/bench/ocaml-nbody-imm/main.sml diff --git a/tests/ocaml-nbody-imm/ocaml-nbody-imm.mlb b/tests/bench/ocaml-nbody-imm/ocaml-nbody-imm.mlb similarity index 100% rename from tests/ocaml-nbody-imm/ocaml-nbody-imm.mlb rename to tests/bench/ocaml-nbody-imm/ocaml-nbody-imm.mlb diff --git a/tests/ocaml-nbody-packed/README b/tests/bench/ocaml-nbody-packed/README similarity index 100% rename from tests/ocaml-nbody-packed/README rename to tests/bench/ocaml-nbody-packed/README diff --git a/tests/ocaml-nbody-packed/main.sml b/tests/bench/ocaml-nbody-packed/main.sml similarity index 100% rename from tests/ocaml-nbody-packed/main.sml rename to tests/bench/ocaml-nbody-packed/main.sml diff --git a/tests/ocaml-nbody-packed/ocaml-nbody-packed.mlb b/tests/bench/ocaml-nbody-packed/ocaml-nbody-packed.mlb similarity index 100% rename from tests/ocaml-nbody-packed/ocaml-nbody-packed.mlb rename to tests/bench/ocaml-nbody-packed/ocaml-nbody-packed.mlb diff --git a/tests/ocaml-nbody/main.sml b/tests/bench/ocaml-nbody/main.sml similarity index 100% rename from tests/ocaml-nbody/main.sml rename to tests/bench/ocaml-nbody/main.sml diff --git a/tests/ocaml-nbody/ocaml-nbody.mlb b/tests/bench/ocaml-nbody/ocaml-nbody.mlb similarity index 100% rename from tests/ocaml-nbody/ocaml-nbody.mlb rename to tests/bench/ocaml-nbody/ocaml-nbody.mlb diff --git a/tests/ocaml-nbody/ocaml-source.ml b/tests/bench/ocaml-nbody/ocaml-source.ml similarity index 100% rename from tests/ocaml-nbody/ocaml-source.ml rename to tests/bench/ocaml-nbody/ocaml-source.ml diff --git a/tests/palindrome/Pal.sml b/tests/bench/palindrome/Pal.sml similarity index 100% rename from tests/palindrome/Pal.sml rename to tests/bench/palindrome/Pal.sml diff --git a/tests/palindrome/main.sml b/tests/bench/palindrome/main.sml similarity index 100% rename from tests/palindrome/main.sml rename to tests/bench/palindrome/main.sml diff --git a/tests/palindrome/palindrome.mlb b/tests/bench/palindrome/palindrome.mlb similarity index 100% rename from tests/palindrome/palindrome.mlb rename to tests/bench/palindrome/palindrome.mlb diff --git a/tests/parens/MkParens.sml b/tests/bench/parens/MkParens.sml similarity index 100% rename from tests/parens/MkParens.sml rename to tests/bench/parens/MkParens.sml diff --git a/tests/parens/main.sml b/tests/bench/parens/main.sml similarity index 100% rename from tests/parens/main.sml rename to tests/bench/parens/main.sml diff --git a/tests/parens/parens.mlb b/tests/bench/parens/parens.mlb similarity index 100% rename from tests/parens/parens.mlb rename to tests/bench/parens/parens.mlb diff --git a/tests/primes-blocked/primes-blocked.mlb b/tests/bench/primes-blocked/primes-blocked.mlb similarity index 100% rename from tests/primes-blocked/primes-blocked.mlb rename to tests/bench/primes-blocked/primes-blocked.mlb diff --git a/tests/primes-blocked/primes-blocked.sml b/tests/bench/primes-blocked/primes-blocked.sml similarity index 100% rename from tests/primes-blocked/primes-blocked.sml rename to tests/bench/primes-blocked/primes-blocked.sml diff --git a/tests/primes-segmented/SegmentedPrimes.sml b/tests/bench/primes-segmented/SegmentedPrimes.sml similarity index 100% rename from tests/primes-segmented/SegmentedPrimes.sml rename to tests/bench/primes-segmented/SegmentedPrimes.sml diff --git a/tests/primes-segmented/TreeSeq.sml b/tests/bench/primes-segmented/TreeSeq.sml similarity index 100% rename from tests/primes-segmented/TreeSeq.sml rename to tests/bench/primes-segmented/TreeSeq.sml diff --git a/tests/primes-segmented/main.sml b/tests/bench/primes-segmented/main.sml similarity index 100% rename from tests/primes-segmented/main.sml rename to tests/bench/primes-segmented/main.sml diff --git a/tests/primes-segmented/primes-segmented.mlb b/tests/bench/primes-segmented/primes-segmented.mlb similarity index 100% rename from tests/primes-segmented/primes-segmented.mlb rename to tests/bench/primes-segmented/primes-segmented.mlb diff --git a/tests/primes/check-stack-dir/check-stack b/tests/bench/primes/check-stack-dir/check-stack similarity index 100% rename from tests/primes/check-stack-dir/check-stack rename to tests/bench/primes/check-stack-dir/check-stack diff --git a/tests/primes/check-stack-dir/primes.mlb b/tests/bench/primes/check-stack-dir/primes.mlb similarity index 100% rename from tests/primes/check-stack-dir/primes.mlb rename to tests/bench/primes/check-stack-dir/primes.mlb diff --git a/tests/primes/check-stack-dir/primes.sml b/tests/bench/primes/check-stack-dir/primes.sml similarity index 100% rename from tests/primes/check-stack-dir/primes.sml rename to tests/bench/primes/check-stack-dir/primes.sml diff --git a/tests/primes/primes.mlb b/tests/bench/primes/primes.mlb similarity index 100% rename from tests/primes/primes.mlb rename to tests/bench/primes/primes.mlb diff --git a/tests/primes/primes.sml b/tests/bench/primes/primes.sml similarity index 100% rename from tests/primes/primes.sml rename to tests/bench/primes/primes.sml diff --git a/tests/primes/safe/primes.mlb b/tests/bench/primes/safe/primes.mlb similarity index 100% rename from tests/primes/safe/primes.mlb rename to tests/bench/primes/safe/primes.mlb diff --git a/tests/primes/safe/primes.sml b/tests/bench/primes/safe/primes.sml similarity index 100% rename from tests/primes/safe/primes.sml rename to tests/bench/primes/safe/primes.sml diff --git a/tests/pure-msort-int32/msort.sml b/tests/bench/pure-msort-int32/msort.sml similarity index 100% rename from tests/pure-msort-int32/msort.sml rename to tests/bench/pure-msort-int32/msort.sml diff --git a/tests/pure-msort-int32/pure-msort-int32.mlb b/tests/bench/pure-msort-int32/pure-msort-int32.mlb similarity index 100% rename from tests/pure-msort-int32/pure-msort-int32.mlb rename to tests/bench/pure-msort-int32/pure-msort-int32.mlb diff --git a/tests/pure-msort-strings/msort.sml b/tests/bench/pure-msort-strings/msort.sml similarity index 100% rename from tests/pure-msort-strings/msort.sml rename to tests/bench/pure-msort-strings/msort.sml diff --git a/tests/pure-msort-strings/pure-msort-strings.mlb b/tests/bench/pure-msort-strings/pure-msort-strings.mlb similarity index 100% rename from tests/pure-msort-strings/pure-msort-strings.mlb rename to tests/bench/pure-msort-strings/pure-msort-strings.mlb diff --git a/tests/pure-msort/msort.sml b/tests/bench/pure-msort/msort.sml similarity index 100% rename from tests/pure-msort/msort.sml rename to tests/bench/pure-msort/msort.sml diff --git a/tests/pure-msort/pure-msort.mlb b/tests/bench/pure-msort/pure-msort.mlb similarity index 100% rename from tests/pure-msort/pure-msort.mlb rename to tests/bench/pure-msort/pure-msort.mlb diff --git a/tests/pure-nn/nn.sml b/tests/bench/pure-nn/nn.sml similarity index 100% rename from tests/pure-nn/nn.sml rename to tests/bench/pure-nn/nn.sml diff --git a/tests/pure-nn/pure-nn.mlb b/tests/bench/pure-nn/pure-nn.mlb similarity index 100% rename from tests/pure-nn/pure-nn.mlb rename to tests/bench/pure-nn/pure-nn.mlb diff --git a/tests/pure-quickhull/Quickhull.sml b/tests/bench/pure-quickhull/Quickhull.sml similarity index 100% rename from tests/pure-quickhull/Quickhull.sml rename to tests/bench/pure-quickhull/Quickhull.sml diff --git a/tests/pure-quickhull/Split.sml b/tests/bench/pure-quickhull/Split.sml similarity index 100% rename from tests/pure-quickhull/Split.sml rename to tests/bench/pure-quickhull/Split.sml diff --git a/tests/pure-quickhull/TreeSeq.sml b/tests/bench/pure-quickhull/TreeSeq.sml similarity index 100% rename from tests/pure-quickhull/TreeSeq.sml rename to tests/bench/pure-quickhull/TreeSeq.sml diff --git a/tests/pure-quickhull/main.sml b/tests/bench/pure-quickhull/main.sml similarity index 100% rename from tests/pure-quickhull/main.sml rename to tests/bench/pure-quickhull/main.sml diff --git a/tests/pure-quickhull/pure-quickhull.mlb b/tests/bench/pure-quickhull/pure-quickhull.mlb similarity index 100% rename from tests/pure-quickhull/pure-quickhull.mlb rename to tests/bench/pure-quickhull/pure-quickhull.mlb diff --git a/tests/pure-skyline/CityGen.sml b/tests/bench/pure-skyline/CityGen.sml similarity index 100% rename from tests/pure-skyline/CityGen.sml rename to tests/bench/pure-skyline/CityGen.sml diff --git a/tests/pure-skyline/FastHashRand.sml b/tests/bench/pure-skyline/FastHashRand.sml similarity index 100% rename from tests/pure-skyline/FastHashRand.sml rename to tests/bench/pure-skyline/FastHashRand.sml diff --git a/tests/pure-skyline/Skyline.sml b/tests/bench/pure-skyline/Skyline.sml similarity index 100% rename from tests/pure-skyline/Skyline.sml rename to tests/bench/pure-skyline/Skyline.sml diff --git a/tests/pure-skyline/main.sml b/tests/bench/pure-skyline/main.sml similarity index 100% rename from tests/pure-skyline/main.sml rename to tests/bench/pure-skyline/main.sml diff --git a/tests/pure-skyline/pure-skyline.mlb b/tests/bench/pure-skyline/pure-skyline.mlb similarity index 100% rename from tests/pure-skyline/pure-skyline.mlb rename to tests/bench/pure-skyline/pure-skyline.mlb diff --git a/tests/quickhull/MkOptSplit.sml b/tests/bench/quickhull/MkOptSplit.sml similarity index 100% rename from tests/quickhull/MkOptSplit.sml rename to tests/bench/quickhull/MkOptSplit.sml diff --git a/tests/quickhull/MkPurishSplit.sml b/tests/bench/quickhull/MkPurishSplit.sml similarity index 100% rename from tests/quickhull/MkPurishSplit.sml rename to tests/bench/quickhull/MkPurishSplit.sml diff --git a/tests/quickhull/MkQuickhull.sml b/tests/bench/quickhull/MkQuickhull.sml similarity index 100% rename from tests/quickhull/MkQuickhull.sml rename to tests/bench/quickhull/MkQuickhull.sml diff --git a/tests/quickhull/ParseFile.sml b/tests/bench/quickhull/ParseFile.sml similarity index 100% rename from tests/quickhull/ParseFile.sml rename to tests/bench/quickhull/ParseFile.sml diff --git a/tests/quickhull/Quickhull.sml b/tests/bench/quickhull/Quickhull.sml similarity index 100% rename from tests/quickhull/Quickhull.sml rename to tests/bench/quickhull/Quickhull.sml diff --git a/tests/quickhull/Split.sml b/tests/bench/quickhull/Split.sml similarity index 100% rename from tests/quickhull/Split.sml rename to tests/bench/quickhull/Split.sml diff --git a/tests/quickhull/TreeSeq.sml b/tests/bench/quickhull/TreeSeq.sml similarity index 100% rename from tests/quickhull/TreeSeq.sml rename to tests/bench/quickhull/TreeSeq.sml diff --git a/tests/quickhull/main.sml b/tests/bench/quickhull/main.sml similarity index 100% rename from tests/quickhull/main.sml rename to tests/bench/quickhull/main.sml diff --git a/tests/quickhull/quickhull.mlb b/tests/bench/quickhull/quickhull.mlb similarity index 100% rename from tests/quickhull/quickhull.mlb rename to tests/bench/quickhull/quickhull.mlb diff --git a/tests/random/random.mlb b/tests/bench/random/random.mlb similarity index 100% rename from tests/random/random.mlb rename to tests/bench/random/random.mlb diff --git a/tests/random/random.sml b/tests/bench/random/random.sml similarity index 100% rename from tests/random/random.sml rename to tests/bench/random/random.sml diff --git a/tests/range-tree/RangeTree.sml b/tests/bench/range-tree/RangeTree.sml similarity index 100% rename from tests/range-tree/RangeTree.sml rename to tests/bench/range-tree/RangeTree.sml diff --git a/tests/range-tree/main.sml b/tests/bench/range-tree/main.sml similarity index 100% rename from tests/range-tree/main.sml rename to tests/bench/range-tree/main.sml diff --git a/tests/range-tree/range-tree.mlb b/tests/bench/range-tree/range-tree.mlb similarity index 100% rename from tests/range-tree/range-tree.mlb rename to tests/bench/range-tree/range-tree.mlb diff --git a/tests/raytracer/main.sml b/tests/bench/raytracer/main.sml similarity index 100% rename from tests/raytracer/main.sml rename to tests/bench/raytracer/main.sml diff --git a/tests/raytracer/raytracer.mlb b/tests/bench/raytracer/raytracer.mlb similarity index 100% rename from tests/raytracer/raytracer.mlb rename to tests/bench/raytracer/raytracer.mlb diff --git a/tests/reverb/main.sml b/tests/bench/reverb/main.sml similarity index 100% rename from tests/reverb/main.sml rename to tests/bench/reverb/main.sml diff --git a/tests/reverb/reverb.mlb b/tests/bench/reverb/reverb.mlb similarity index 100% rename from tests/reverb/reverb.mlb rename to tests/bench/reverb/reverb.mlb diff --git a/tests/samplesort/main.sml b/tests/bench/samplesort/main.sml similarity index 100% rename from tests/samplesort/main.sml rename to tests/bench/samplesort/main.sml diff --git a/tests/samplesort/samplesort.mlb b/tests/bench/samplesort/samplesort.mlb similarity index 100% rename from tests/samplesort/samplesort.mlb rename to tests/bench/samplesort/samplesort.mlb diff --git a/tests/seam-carve-index/README b/tests/bench/seam-carve-index/README similarity index 100% rename from tests/seam-carve-index/README rename to tests/bench/seam-carve-index/README diff --git a/tests/seam-carve-index/SCI.sml b/tests/bench/seam-carve-index/SCI.sml similarity index 100% rename from tests/seam-carve-index/SCI.sml rename to tests/bench/seam-carve-index/SCI.sml diff --git a/tests/seam-carve-index/VerticalSeamIndexMap.sml b/tests/bench/seam-carve-index/VerticalSeamIndexMap.sml similarity index 100% rename from tests/seam-carve-index/VerticalSeamIndexMap.sml rename to tests/bench/seam-carve-index/VerticalSeamIndexMap.sml diff --git a/tests/seam-carve-index/main.sml b/tests/bench/seam-carve-index/main.sml similarity index 100% rename from tests/seam-carve-index/main.sml rename to tests/bench/seam-carve-index/main.sml diff --git a/tests/seam-carve-index/seam-carve-index.mlb b/tests/bench/seam-carve-index/seam-carve-index.mlb similarity index 100% rename from tests/seam-carve-index/seam-carve-index.mlb rename to tests/bench/seam-carve-index/seam-carve-index.mlb diff --git a/tests/seam-carve/SC.sml b/tests/bench/seam-carve/SC.sml similarity index 100% rename from tests/seam-carve/SC.sml rename to tests/bench/seam-carve/SC.sml diff --git a/tests/seam-carve/main.sml b/tests/bench/seam-carve/main.sml similarity index 100% rename from tests/seam-carve/main.sml rename to tests/bench/seam-carve/main.sml diff --git a/tests/seam-carve/seam-carve.mlb b/tests/bench/seam-carve/seam-carve.mlb similarity index 100% rename from tests/seam-carve/seam-carve.mlb rename to tests/bench/seam-carve/seam-carve.mlb diff --git a/tests/shuf/main.sml b/tests/bench/shuf/main.sml similarity index 100% rename from tests/shuf/main.sml rename to tests/bench/shuf/main.sml diff --git a/tests/shuf/shuf.mlb b/tests/bench/shuf/shuf.mlb similarity index 100% rename from tests/shuf/shuf.mlb rename to tests/bench/shuf/shuf.mlb diff --git a/tests/skyline/CityGen.sml b/tests/bench/skyline/CityGen.sml similarity index 100% rename from tests/skyline/CityGen.sml rename to tests/bench/skyline/CityGen.sml diff --git a/tests/skyline/FastHashRand.sml b/tests/bench/skyline/FastHashRand.sml similarity index 100% rename from tests/skyline/FastHashRand.sml rename to tests/bench/skyline/FastHashRand.sml diff --git a/tests/skyline/Skyline.sml b/tests/bench/skyline/Skyline.sml similarity index 100% rename from tests/skyline/Skyline.sml rename to tests/bench/skyline/Skyline.sml diff --git a/tests/skyline/main.sml b/tests/bench/skyline/main.sml similarity index 100% rename from tests/skyline/main.sml rename to tests/bench/skyline/main.sml diff --git a/tests/skyline/skyline.mlb b/tests/bench/skyline/skyline.mlb similarity index 100% rename from tests/skyline/skyline.mlb rename to tests/bench/skyline/skyline.mlb diff --git a/tests/spanner/Spanner.sml b/tests/bench/spanner/Spanner.sml similarity index 100% rename from tests/spanner/Spanner.sml rename to tests/bench/spanner/Spanner.sml diff --git a/tests/spanner/main.sml b/tests/bench/spanner/main.sml similarity index 100% rename from tests/spanner/main.sml rename to tests/bench/spanner/main.sml diff --git a/tests/spanner/spanner.mlb b/tests/bench/spanner/spanner.mlb similarity index 100% rename from tests/spanner/spanner.mlb rename to tests/bench/spanner/spanner.mlb diff --git a/tests/sparse-mxv-opt/SparseMxV.sml b/tests/bench/sparse-mxv-opt/SparseMxV.sml similarity index 100% rename from tests/sparse-mxv-opt/SparseMxV.sml rename to tests/bench/sparse-mxv-opt/SparseMxV.sml diff --git a/tests/sparse-mxv-opt/main.sml b/tests/bench/sparse-mxv-opt/main.sml similarity index 100% rename from tests/sparse-mxv-opt/main.sml rename to tests/bench/sparse-mxv-opt/main.sml diff --git a/tests/sparse-mxv-opt/sparse-mxv-opt.mlb b/tests/bench/sparse-mxv-opt/sparse-mxv-opt.mlb similarity index 100% rename from tests/sparse-mxv-opt/sparse-mxv-opt.mlb rename to tests/bench/sparse-mxv-opt/sparse-mxv-opt.mlb diff --git a/tests/sparse-mxv/MkMXV.sml b/tests/bench/sparse-mxv/MkMXV.sml similarity index 100% rename from tests/sparse-mxv/MkMXV.sml rename to tests/bench/sparse-mxv/MkMXV.sml diff --git a/tests/sparse-mxv/main.sml b/tests/bench/sparse-mxv/main.sml similarity index 100% rename from tests/sparse-mxv/main.sml rename to tests/bench/sparse-mxv/main.sml diff --git a/tests/sparse-mxv/sparse-mxv.mlb b/tests/bench/sparse-mxv/sparse-mxv.mlb similarity index 100% rename from tests/sparse-mxv/sparse-mxv.mlb rename to tests/bench/sparse-mxv/sparse-mxv.mlb diff --git a/tests/subset-sum/SubsetSumTiled.sml b/tests/bench/subset-sum/SubsetSumTiled.sml similarity index 100% rename from tests/subset-sum/SubsetSumTiled.sml rename to tests/bench/subset-sum/SubsetSumTiled.sml diff --git a/tests/subset-sum/main.sml b/tests/bench/subset-sum/main.sml similarity index 100% rename from tests/subset-sum/main.sml rename to tests/bench/subset-sum/main.sml diff --git a/tests/subset-sum/subset-sum.mlb b/tests/bench/subset-sum/subset-sum.mlb similarity index 100% rename from tests/subset-sum/subset-sum.mlb rename to tests/bench/subset-sum/subset-sum.mlb diff --git a/tests/suffix-array/AS.sml b/tests/bench/suffix-array/AS.sml similarity index 100% rename from tests/suffix-array/AS.sml rename to tests/bench/suffix-array/AS.sml diff --git a/tests/suffix-array/BruteForce.sml b/tests/bench/suffix-array/BruteForce.sml similarity index 100% rename from tests/suffix-array/BruteForce.sml rename to tests/bench/suffix-array/BruteForce.sml diff --git a/tests/suffix-array/PrefixDoubling.sml b/tests/bench/suffix-array/PrefixDoubling.sml similarity index 100% rename from tests/suffix-array/PrefixDoubling.sml rename to tests/bench/suffix-array/PrefixDoubling.sml diff --git a/tests/suffix-array/main.sml b/tests/bench/suffix-array/main.sml similarity index 100% rename from tests/suffix-array/main.sml rename to tests/bench/suffix-array/main.sml diff --git a/tests/suffix-array/suffix-array.mlb b/tests/bench/suffix-array/suffix-array.mlb similarity index 100% rename from tests/suffix-array/suffix-array.mlb rename to tests/bench/suffix-array/suffix-array.mlb diff --git a/tests/tape-delay/main.sml b/tests/bench/tape-delay/main.sml similarity index 100% rename from tests/tape-delay/main.sml rename to tests/bench/tape-delay/main.sml diff --git a/tests/tape-delay/tape-delay.mlb b/tests/bench/tape-delay/tape-delay.mlb similarity index 100% rename from tests/tape-delay/tape-delay.mlb rename to tests/bench/tape-delay/tape-delay.mlb diff --git a/tests/tinykaboom/TinyKaboom.sml b/tests/bench/tinykaboom/TinyKaboom.sml similarity index 100% rename from tests/tinykaboom/TinyKaboom.sml rename to tests/bench/tinykaboom/TinyKaboom.sml diff --git a/tests/tinykaboom/f32.sml b/tests/bench/tinykaboom/f32.sml similarity index 100% rename from tests/tinykaboom/f32.sml rename to tests/bench/tinykaboom/f32.sml diff --git a/tests/tinykaboom/main.sml b/tests/bench/tinykaboom/main.sml similarity index 100% rename from tests/tinykaboom/main.sml rename to tests/bench/tinykaboom/main.sml diff --git a/tests/tinykaboom/tinykaboom.mlb b/tests/bench/tinykaboom/tinykaboom.mlb similarity index 100% rename from tests/tinykaboom/tinykaboom.mlb rename to tests/bench/tinykaboom/tinykaboom.mlb diff --git a/tests/tinykaboom/vec3.sml b/tests/bench/tinykaboom/vec3.sml similarity index 100% rename from tests/tinykaboom/vec3.sml rename to tests/bench/tinykaboom/vec3.sml diff --git a/tests/to-gif/main.sml b/tests/bench/to-gif/main.sml similarity index 100% rename from tests/to-gif/main.sml rename to tests/bench/to-gif/main.sml diff --git a/tests/to-gif/to-gif.mlb b/tests/bench/to-gif/to-gif.mlb similarity index 100% rename from tests/to-gif/to-gif.mlb rename to tests/bench/to-gif/to-gif.mlb diff --git a/tests/tokens/tokens.mlb b/tests/bench/tokens/tokens.mlb similarity index 100% rename from tests/tokens/tokens.mlb rename to tests/bench/tokens/tokens.mlb diff --git a/tests/tokens/tokens.sml b/tests/bench/tokens/tokens.sml similarity index 100% rename from tests/tokens/tokens.sml rename to tests/bench/tokens/tokens.sml diff --git a/tests/triangle-count/TriangleCount.sml b/tests/bench/triangle-count/TriangleCount.sml similarity index 100% rename from tests/triangle-count/TriangleCount.sml rename to tests/bench/triangle-count/TriangleCount.sml diff --git a/tests/triangle-count/main.sml b/tests/bench/triangle-count/main.sml similarity index 100% rename from tests/triangle-count/main.sml rename to tests/bench/triangle-count/main.sml diff --git a/tests/triangle-count/triangle-count.mlb b/tests/bench/triangle-count/triangle-count.mlb similarity index 100% rename from tests/triangle-count/triangle-count.mlb rename to tests/bench/triangle-count/triangle-count.mlb diff --git a/tests/wc-opt/WC.sml b/tests/bench/wc-opt/WC.sml similarity index 100% rename from tests/wc-opt/WC.sml rename to tests/bench/wc-opt/WC.sml diff --git a/tests/wc-opt/main.sml b/tests/bench/wc-opt/main.sml similarity index 100% rename from tests/wc-opt/main.sml rename to tests/bench/wc-opt/main.sml diff --git a/tests/wc-opt/wc-opt.mlb b/tests/bench/wc-opt/wc-opt.mlb similarity index 100% rename from tests/wc-opt/wc-opt.mlb rename to tests/bench/wc-opt/wc-opt.mlb diff --git a/tests/wc/MkWC.sml b/tests/bench/wc/MkWC.sml similarity index 100% rename from tests/wc/MkWC.sml rename to tests/bench/wc/MkWC.sml diff --git a/tests/wc/main.sml b/tests/bench/wc/main.sml similarity index 100% rename from tests/wc/main.sml rename to tests/bench/wc/main.sml diff --git a/tests/wc/wc.mlb b/tests/bench/wc/wc.mlb similarity index 100% rename from tests/wc/wc.mlb rename to tests/bench/wc/wc.mlb From 25a76a5a30f51f5b10dfbe95e060c615db77bde5 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 14:57:09 -0400 Subject: [PATCH 06/18] fix mpllib imports --- tests/bench/bfs-delayed/bfs-delayed.mlb | 2 +- tests/bench/bfs-det-dedup/bfs-det-dedup.mlb | 2 +- tests/bench/bfs-det-priority/bfs-det-priority.mlb | 2 +- .../bench/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb | 2 +- tests/bench/bfs-tree-entangled/bfs-tree-entangled.mlb | 2 +- tests/bench/bfs/bfs.mlb | 2 +- tests/bench/bignum-add-opt/bignum-add-opt.mlb | 2 +- tests/bench/bignum-add/bignum-add.mlb | 2 +- tests/bench/centrality/centrality.mlb | 2 +- tests/bench/collect/collect.mlb | 2 +- tests/bench/connectivity/connectivity.mlb | 2 +- tests/bench/dedup-entangled-fixed/dedup-entangled-fixed.mlb | 2 +- tests/bench/dedup-entangled/dedup-entangled.mlb | 2 +- tests/bench/dedup/dedup.mlb | 2 +- tests/bench/delaunay-animation/delaunay-animation.mlb | 2 +- tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb | 2 +- tests/bench/delaunay-top-down/delaunay-top-down.mlb | 2 +- tests/bench/delaunay/delaunay.mlb | 2 +- tests/bench/dense-matmul/dense-matmul.mlb | 2 +- tests/bench/fib/fib.mlb | 2 +- tests/bench/flatten/flatten.mlb | 2 +- tests/bench/graphio/graphio.mlb | 2 +- tests/bench/grep-old/grep-old.mlb | 2 +- tests/bench/grep/grep.mlb | 2 +- tests/bench/high-frag/high-frag.mlb | 2 +- tests/bench/integrate-opt/integrate-opt.mlb | 2 +- tests/bench/integrate/integrate.mlb | 2 +- tests/bench/interval-tree/interval-tree.mlb | 2 +- tests/bench/linearrec-opt/linearrec-opt.mlb | 2 +- tests/bench/linearrec/linearrec.mlb | 2 +- tests/bench/linefit-opt/linefit-opt.mlb | 2 +- tests/bench/linefit/linefit.mlb | 2 +- tests/bench/low-d-decomp/low-d-decomp.mlb | 2 +- tests/bench/max-indep-set/max-indep-set.mlb | 2 +- tests/bench/mcss-opt/mcss-opt.mlb | 2 +- tests/bench/mcss/mcss.mlb | 2 +- tests/bench/msort-int32/msort-int32.mlb | 2 +- tests/bench/msort-strings/msort-strings.mlb | 2 +- tests/bench/msort/msort.mlb | 2 +- tests/bench/nearest-nbrs/nearest-nbrs.mlb | 2 +- tests/bench/nqueens-simple/nqueens-simple.mlb | 2 +- tests/bench/nqueens/nqueens.mlb | 2 +- tests/bench/ocaml-binarytrees5/ocaml-binarytrees5.mlb | 2 +- tests/bench/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb | 2 +- tests/bench/ocaml-game-of-life/ocaml-game-of-life.mlb | 2 +- tests/bench/ocaml-lu-decomp/ocaml-lu-decomp.mlb | 2 +- tests/bench/ocaml-mandelbrot/ocaml-mandelbrot.mlb | 2 +- tests/bench/ocaml-nbody-imm/ocaml-nbody-imm.mlb | 2 +- tests/bench/ocaml-nbody-packed/ocaml-nbody-packed.mlb | 2 +- tests/bench/ocaml-nbody/ocaml-nbody.mlb | 2 +- tests/bench/palindrome/palindrome.mlb | 2 +- tests/bench/parens/parens.mlb | 2 +- tests/bench/primes-blocked/primes-blocked.mlb | 2 +- tests/bench/primes-segmented/primes-segmented.mlb | 2 +- tests/bench/primes/check-stack-dir/primes.mlb | 2 +- tests/bench/primes/primes.mlb | 2 +- tests/bench/primes/safe/primes.mlb | 2 +- tests/bench/pure-msort-int32/pure-msort-int32.mlb | 2 +- tests/bench/pure-msort-strings/pure-msort-strings.mlb | 2 +- tests/bench/pure-msort/pure-msort.mlb | 2 +- tests/bench/pure-nn/pure-nn.mlb | 2 +- tests/bench/pure-quickhull/pure-quickhull.mlb | 2 +- tests/bench/pure-skyline/pure-skyline.mlb | 2 +- tests/bench/quickhull/quickhull.mlb | 2 +- tests/bench/random/random.mlb | 2 +- tests/bench/range-tree/range-tree.mlb | 2 +- tests/bench/raytracer/raytracer.mlb | 2 +- tests/bench/reverb/reverb.mlb | 2 +- tests/bench/samplesort/samplesort.mlb | 2 +- tests/bench/seam-carve-index/seam-carve-index.mlb | 2 +- tests/bench/seam-carve/seam-carve.mlb | 2 +- tests/bench/shuf/shuf.mlb | 2 +- tests/bench/skyline/skyline.mlb | 2 +- tests/bench/spanner/spanner.mlb | 2 +- tests/bench/sparse-mxv-opt/sparse-mxv-opt.mlb | 2 +- tests/bench/sparse-mxv/sparse-mxv.mlb | 2 +- tests/bench/subset-sum/subset-sum.mlb | 2 +- tests/bench/suffix-array/suffix-array.mlb | 2 +- tests/bench/tape-delay/tape-delay.mlb | 2 +- tests/bench/tinykaboom/tinykaboom.mlb | 2 +- tests/bench/to-gif/to-gif.mlb | 2 +- tests/bench/tokens/tokens.mlb | 2 +- tests/bench/triangle-count/triangle-count.mlb | 2 +- tests/bench/wc-opt/wc-opt.mlb | 2 +- tests/bench/wc/wc.mlb | 2 +- 85 files changed, 85 insertions(+), 85 deletions(-) diff --git a/tests/bench/bfs-delayed/bfs-delayed.mlb b/tests/bench/bfs-delayed/bfs-delayed.mlb index 80dfe0fe6..e71267437 100644 --- a/tests/bench/bfs-delayed/bfs-delayed.mlb +++ b/tests/bench/bfs-delayed/bfs-delayed.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SerialBFS.sml MkBFS.sml main.sml diff --git a/tests/bench/bfs-det-dedup/bfs-det-dedup.mlb b/tests/bench/bfs-det-dedup/bfs-det-dedup.mlb index 95aaeaa70..0a8df185f 100644 --- a/tests/bench/bfs-det-dedup/bfs-det-dedup.mlb +++ b/tests/bench/bfs-det-dedup/bfs-det-dedup.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb Dedup.sml SerialBFS.sml OffsetSearch.sml diff --git a/tests/bench/bfs-det-priority/bfs-det-priority.mlb b/tests/bench/bfs-det-priority/bfs-det-priority.mlb index e43ea8518..c7b968587 100644 --- a/tests/bench/bfs-det-priority/bfs-det-priority.mlb +++ b/tests/bench/bfs-det-priority/bfs-det-priority.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SerialBFS.sml OffsetSearch.sml PriorityBFS.sml diff --git a/tests/bench/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb b/tests/bench/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb index 320aea3a4..37e86d899 100644 --- a/tests/bench/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb +++ b/tests/bench/bfs-tree-entangled-fixed/bfs-tree-entangled-fixed.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SerialBFS.sml OffsetSearch.sml NondetBFS.sml diff --git a/tests/bench/bfs-tree-entangled/bfs-tree-entangled.mlb b/tests/bench/bfs-tree-entangled/bfs-tree-entangled.mlb index 320aea3a4..37e86d899 100644 --- a/tests/bench/bfs-tree-entangled/bfs-tree-entangled.mlb +++ b/tests/bench/bfs-tree-entangled/bfs-tree-entangled.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SerialBFS.sml OffsetSearch.sml NondetBFS.sml diff --git a/tests/bench/bfs/bfs.mlb b/tests/bench/bfs/bfs.mlb index 320aea3a4..37e86d899 100644 --- a/tests/bench/bfs/bfs.mlb +++ b/tests/bench/bfs/bfs.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SerialBFS.sml OffsetSearch.sml NondetBFS.sml diff --git a/tests/bench/bignum-add-opt/bignum-add-opt.mlb b/tests/bench/bignum-add-opt/bignum-add-opt.mlb index 4e38d2345..b01243dc3 100644 --- a/tests/bench/bignum-add-opt/bignum-add-opt.mlb +++ b/tests/bench/bignum-add-opt/bignum-add-opt.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb ../bignum-add/Bignum.sml Add.sml ../bignum-add/SequentialAdd.sml diff --git a/tests/bench/bignum-add/bignum-add.mlb b/tests/bench/bignum-add/bignum-add.mlb index 905631370..773234ca1 100644 --- a/tests/bench/bignum-add/bignum-add.mlb +++ b/tests/bench/bignum-add/bignum-add.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb Bignum.sml MkAdd.sml SequentialAdd.sml diff --git a/tests/bench/centrality/centrality.mlb b/tests/bench/centrality/centrality.mlb index ddddceb0f..8bddac8b8 100644 --- a/tests/bench/centrality/centrality.mlb +++ b/tests/bench/centrality/centrality.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb OffsetSearch.sml BC.sml main.sml diff --git a/tests/bench/collect/collect.mlb b/tests/bench/collect/collect.mlb index 41b672432..3532e2cdc 100644 --- a/tests/bench/collect/collect.mlb +++ b/tests/bench/collect/collect.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb KEY.sml VALUE.sml HashTable.sml diff --git a/tests/bench/connectivity/connectivity.mlb b/tests/bench/connectivity/connectivity.mlb index 16148c348..598d7df3f 100644 --- a/tests/bench/connectivity/connectivity.mlb +++ b/tests/bench/connectivity/connectivity.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb ../ldd/LDD.sml Connectivity.sml main.sml diff --git a/tests/bench/dedup-entangled-fixed/dedup-entangled-fixed.mlb b/tests/bench/dedup-entangled-fixed/dedup-entangled-fixed.mlb index f65fd3394..20bb8b2f1 100644 --- a/tests/bench/dedup-entangled-fixed/dedup-entangled-fixed.mlb +++ b/tests/bench/dedup-entangled-fixed/dedup-entangled-fixed.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb $(SML_LIB)/basis/mlton.mlb NondetDedup.sml diff --git a/tests/bench/dedup-entangled/dedup-entangled.mlb b/tests/bench/dedup-entangled/dedup-entangled.mlb index f65fd3394..20bb8b2f1 100644 --- a/tests/bench/dedup-entangled/dedup-entangled.mlb +++ b/tests/bench/dedup-entangled/dedup-entangled.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb $(SML_LIB)/basis/mlton.mlb NondetDedup.sml diff --git a/tests/bench/dedup/dedup.mlb b/tests/bench/dedup/dedup.mlb index d70b7e467..a1801a450 100644 --- a/tests/bench/dedup/dedup.mlb +++ b/tests/bench/dedup/dedup.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb dedup.sml diff --git a/tests/bench/delaunay-animation/delaunay-animation.mlb b/tests/bench/delaunay-animation/delaunay-animation.mlb index 295de819c..67d2cfe4e 100644 --- a/tests/bench/delaunay-animation/delaunay-animation.mlb +++ b/tests/bench/delaunay-animation/delaunay-animation.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb Split.sml DelaunayTriangulation.sml main.sml diff --git a/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb b/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb index dcf033eb3..167f0bd01 100644 --- a/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb +++ b/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb DelaunayTriangulation.sml main.sml diff --git a/tests/bench/delaunay-top-down/delaunay-top-down.mlb b/tests/bench/delaunay-top-down/delaunay-top-down.mlb index 890647c66..abed8a2d9 100644 --- a/tests/bench/delaunay-top-down/delaunay-top-down.mlb +++ b/tests/bench/delaunay-top-down/delaunay-top-down.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb HashTable.sml DelaunayTriangulationTopDown.sml main.sml diff --git a/tests/bench/delaunay/delaunay.mlb b/tests/bench/delaunay/delaunay.mlb index 295de819c..67d2cfe4e 100644 --- a/tests/bench/delaunay/delaunay.mlb +++ b/tests/bench/delaunay/delaunay.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb Split.sml DelaunayTriangulation.sml main.sml diff --git a/tests/bench/dense-matmul/dense-matmul.mlb b/tests/bench/dense-matmul/dense-matmul.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/dense-matmul/dense-matmul.mlb +++ b/tests/bench/dense-matmul/dense-matmul.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/fib/fib.mlb b/tests/bench/fib/fib.mlb index 32bdc20f1..9435124ee 100644 --- a/tests/bench/fib/fib.mlb +++ b/tests/bench/fib/fib.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb fib.sml diff --git a/tests/bench/flatten/flatten.mlb b/tests/bench/flatten/flatten.mlb index ffa7d8537..67c9017df 100644 --- a/tests/bench/flatten/flatten.mlb +++ b/tests/bench/flatten/flatten.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb BinarySearch.sml FullExpandPow2Flatten.sml AllBSFlatten.sml diff --git a/tests/bench/graphio/graphio.mlb b/tests/bench/graphio/graphio.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/graphio/graphio.mlb +++ b/tests/bench/graphio/graphio.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/grep-old/grep-old.mlb b/tests/bench/grep-old/grep-old.mlb index 29645d46c..a4cfd0cab 100644 --- a/tests/bench/grep-old/grep-old.mlb +++ b/tests/bench/grep-old/grep-old.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb Grep.sml main.sml diff --git a/tests/bench/grep/grep.mlb b/tests/bench/grep/grep.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/grep/grep.mlb +++ b/tests/bench/grep/grep.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/high-frag/high-frag.mlb b/tests/bench/high-frag/high-frag.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/high-frag/high-frag.mlb +++ b/tests/bench/high-frag/high-frag.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/integrate-opt/integrate-opt.mlb b/tests/bench/integrate-opt/integrate-opt.mlb index d732a9a4a..30864afc1 100644 --- a/tests/bench/integrate-opt/integrate-opt.mlb +++ b/tests/bench/integrate-opt/integrate-opt.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb Integrate.sml main.sml diff --git a/tests/bench/integrate/integrate.mlb b/tests/bench/integrate/integrate.mlb index f4abb6c79..f20a233f9 100644 --- a/tests/bench/integrate/integrate.mlb +++ b/tests/bench/integrate/integrate.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MkIntegrate.sml main.sml diff --git a/tests/bench/interval-tree/interval-tree.mlb b/tests/bench/interval-tree/interval-tree.mlb index 463c11a88..83f4bffdb 100644 --- a/tests/bench/interval-tree/interval-tree.mlb +++ b/tests/bench/interval-tree/interval-tree.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb IntervalTree.sml new-main.sml diff --git a/tests/bench/linearrec-opt/linearrec-opt.mlb b/tests/bench/linearrec-opt/linearrec-opt.mlb index bcb449aeb..cbef997d4 100644 --- a/tests/bench/linearrec-opt/linearrec-opt.mlb +++ b/tests/bench/linearrec-opt/linearrec-opt.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb LinearRec.sml main.sml diff --git a/tests/bench/linearrec/linearrec.mlb b/tests/bench/linearrec/linearrec.mlb index 6345c31e0..38ea89521 100644 --- a/tests/bench/linearrec/linearrec.mlb +++ b/tests/bench/linearrec/linearrec.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MkLinearRec.sml main.sml diff --git a/tests/bench/linefit-opt/linefit-opt.mlb b/tests/bench/linefit-opt/linefit-opt.mlb index 5eca75c27..1b44cc1c4 100644 --- a/tests/bench/linefit-opt/linefit-opt.mlb +++ b/tests/bench/linefit-opt/linefit-opt.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb LineFit.sml main.sml diff --git a/tests/bench/linefit/linefit.mlb b/tests/bench/linefit/linefit.mlb index e979f3232..1fdda8cbd 100644 --- a/tests/bench/linefit/linefit.mlb +++ b/tests/bench/linefit/linefit.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MkLineFit.sml main.sml diff --git a/tests/bench/low-d-decomp/low-d-decomp.mlb b/tests/bench/low-d-decomp/low-d-decomp.mlb index 3253cfe34..1116e5502 100644 --- a/tests/bench/low-d-decomp/low-d-decomp.mlb +++ b/tests/bench/low-d-decomp/low-d-decomp.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb ldd-alt.sml main.sml diff --git a/tests/bench/max-indep-set/max-indep-set.mlb b/tests/bench/max-indep-set/max-indep-set.mlb index ff8c5eb1b..24e29325c 100644 --- a/tests/bench/max-indep-set/max-indep-set.mlb +++ b/tests/bench/max-indep-set/max-indep-set.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb faa.$(COMPAT).sml MIS.sml main.sml diff --git a/tests/bench/mcss-opt/mcss-opt.mlb b/tests/bench/mcss-opt/mcss-opt.mlb index bc959d4e7..9a97da9bb 100644 --- a/tests/bench/mcss-opt/mcss-opt.mlb +++ b/tests/bench/mcss-opt/mcss-opt.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MCSS.sml main.sml diff --git a/tests/bench/mcss/mcss.mlb b/tests/bench/mcss/mcss.mlb index 80d1c2a88..1d654745f 100644 --- a/tests/bench/mcss/mcss.mlb +++ b/tests/bench/mcss/mcss.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MkMapReduceMCSS.sml main.sml diff --git a/tests/bench/msort-int32/msort-int32.mlb b/tests/bench/msort-int32/msort-int32.mlb index 394068b7b..e05edb8f7 100644 --- a/tests/bench/msort-int32/msort-int32.mlb +++ b/tests/bench/msort-int32/msort-int32.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb msort.sml diff --git a/tests/bench/msort-strings/msort-strings.mlb b/tests/bench/msort-strings/msort-strings.mlb index 394068b7b..e05edb8f7 100644 --- a/tests/bench/msort-strings/msort-strings.mlb +++ b/tests/bench/msort-strings/msort-strings.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb msort.sml diff --git a/tests/bench/msort/msort.mlb b/tests/bench/msort/msort.mlb index 394068b7b..e05edb8f7 100644 --- a/tests/bench/msort/msort.mlb +++ b/tests/bench/msort/msort.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb msort.sml diff --git a/tests/bench/nearest-nbrs/nearest-nbrs.mlb b/tests/bench/nearest-nbrs/nearest-nbrs.mlb index dcf87164b..2c6fe21d1 100644 --- a/tests/bench/nearest-nbrs/nearest-nbrs.mlb +++ b/tests/bench/nearest-nbrs/nearest-nbrs.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb ParseFile.sml main.sml diff --git a/tests/bench/nqueens-simple/nqueens-simple.mlb b/tests/bench/nqueens-simple/nqueens-simple.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/nqueens-simple/nqueens-simple.mlb +++ b/tests/bench/nqueens-simple/nqueens-simple.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/nqueens/nqueens.mlb b/tests/bench/nqueens/nqueens.mlb index 8a7897d39..889d11f07 100644 --- a/tests/bench/nqueens/nqueens.mlb +++ b/tests/bench/nqueens/nqueens.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb nqueens.sml diff --git a/tests/bench/ocaml-binarytrees5/ocaml-binarytrees5.mlb b/tests/bench/ocaml-binarytrees5/ocaml-binarytrees5.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/ocaml-binarytrees5/ocaml-binarytrees5.mlb +++ b/tests/bench/ocaml-binarytrees5/ocaml-binarytrees5.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb b/tests/bench/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb +++ b/tests/bench/ocaml-game-of-life-pure/ocaml-game-of-life-pure.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/ocaml-game-of-life/ocaml-game-of-life.mlb b/tests/bench/ocaml-game-of-life/ocaml-game-of-life.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/ocaml-game-of-life/ocaml-game-of-life.mlb +++ b/tests/bench/ocaml-game-of-life/ocaml-game-of-life.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/ocaml-lu-decomp/ocaml-lu-decomp.mlb b/tests/bench/ocaml-lu-decomp/ocaml-lu-decomp.mlb index d0c4aed76..4942dcdd5 100644 --- a/tests/bench/ocaml-lu-decomp/ocaml-lu-decomp.mlb +++ b/tests/bench/ocaml-lu-decomp/ocaml-lu-decomp.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb (*local $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb in diff --git a/tests/bench/ocaml-mandelbrot/ocaml-mandelbrot.mlb b/tests/bench/ocaml-mandelbrot/ocaml-mandelbrot.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/ocaml-mandelbrot/ocaml-mandelbrot.mlb +++ b/tests/bench/ocaml-mandelbrot/ocaml-mandelbrot.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/ocaml-nbody-imm/ocaml-nbody-imm.mlb b/tests/bench/ocaml-nbody-imm/ocaml-nbody-imm.mlb index c7b6ed5b0..4c9eb5f6a 100644 --- a/tests/bench/ocaml-nbody-imm/ocaml-nbody-imm.mlb +++ b/tests/bench/ocaml-nbody-imm/ocaml-nbody-imm.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb local $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb in diff --git a/tests/bench/ocaml-nbody-packed/ocaml-nbody-packed.mlb b/tests/bench/ocaml-nbody-packed/ocaml-nbody-packed.mlb index c7b6ed5b0..4c9eb5f6a 100644 --- a/tests/bench/ocaml-nbody-packed/ocaml-nbody-packed.mlb +++ b/tests/bench/ocaml-nbody-packed/ocaml-nbody-packed.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb local $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb in diff --git a/tests/bench/ocaml-nbody/ocaml-nbody.mlb b/tests/bench/ocaml-nbody/ocaml-nbody.mlb index c7b6ed5b0..4c9eb5f6a 100644 --- a/tests/bench/ocaml-nbody/ocaml-nbody.mlb +++ b/tests/bench/ocaml-nbody/ocaml-nbody.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb local $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb in diff --git a/tests/bench/palindrome/palindrome.mlb b/tests/bench/palindrome/palindrome.mlb index 799fb44ba..0c8ac80dd 100644 --- a/tests/bench/palindrome/palindrome.mlb +++ b/tests/bench/palindrome/palindrome.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb Pal.sml main.sml diff --git a/tests/bench/parens/parens.mlb b/tests/bench/parens/parens.mlb index 6ff0720ce..5f79a1b5e 100644 --- a/tests/bench/parens/parens.mlb +++ b/tests/bench/parens/parens.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MkParens.sml main.sml diff --git a/tests/bench/primes-blocked/primes-blocked.mlb b/tests/bench/primes-blocked/primes-blocked.mlb index e5c5b5763..51fd277a8 100644 --- a/tests/bench/primes-blocked/primes-blocked.mlb +++ b/tests/bench/primes-blocked/primes-blocked.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb primes-blocked.sml diff --git a/tests/bench/primes-segmented/primes-segmented.mlb b/tests/bench/primes-segmented/primes-segmented.mlb index e39bb30b2..e6746318a 100644 --- a/tests/bench/primes-segmented/primes-segmented.mlb +++ b/tests/bench/primes-segmented/primes-segmented.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb TreeSeq.sml SegmentedPrimes.sml main.sml diff --git a/tests/bench/primes/check-stack-dir/primes.mlb b/tests/bench/primes/check-stack-dir/primes.mlb index fb238e5ee..b0c9af4bd 100644 --- a/tests/bench/primes/check-stack-dir/primes.mlb +++ b/tests/bench/primes/check-stack-dir/primes.mlb @@ -1,5 +1,5 @@ $(SML_LIB)/basis/basis.mlb $(SML_LIB)/basis/fork-join.mlb -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb primes.sml diff --git a/tests/bench/primes/primes.mlb b/tests/bench/primes/primes.mlb index 72e5d0ac4..180294849 100644 --- a/tests/bench/primes/primes.mlb +++ b/tests/bench/primes/primes.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb primes.sml diff --git a/tests/bench/primes/safe/primes.mlb b/tests/bench/primes/safe/primes.mlb index 72e5d0ac4..180294849 100644 --- a/tests/bench/primes/safe/primes.mlb +++ b/tests/bench/primes/safe/primes.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb primes.sml diff --git a/tests/bench/pure-msort-int32/pure-msort-int32.mlb b/tests/bench/pure-msort-int32/pure-msort-int32.mlb index 394068b7b..e05edb8f7 100644 --- a/tests/bench/pure-msort-int32/pure-msort-int32.mlb +++ b/tests/bench/pure-msort-int32/pure-msort-int32.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb msort.sml diff --git a/tests/bench/pure-msort-strings/pure-msort-strings.mlb b/tests/bench/pure-msort-strings/pure-msort-strings.mlb index 394068b7b..e05edb8f7 100644 --- a/tests/bench/pure-msort-strings/pure-msort-strings.mlb +++ b/tests/bench/pure-msort-strings/pure-msort-strings.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb msort.sml diff --git a/tests/bench/pure-msort/pure-msort.mlb b/tests/bench/pure-msort/pure-msort.mlb index 394068b7b..e05edb8f7 100644 --- a/tests/bench/pure-msort/pure-msort.mlb +++ b/tests/bench/pure-msort/pure-msort.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb msort.sml diff --git a/tests/bench/pure-nn/pure-nn.mlb b/tests/bench/pure-nn/pure-nn.mlb index 520fc85c3..bfa54aec7 100644 --- a/tests/bench/pure-nn/pure-nn.mlb +++ b/tests/bench/pure-nn/pure-nn.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb nn.sml diff --git a/tests/bench/pure-quickhull/pure-quickhull.mlb b/tests/bench/pure-quickhull/pure-quickhull.mlb index fecef8c75..7c74996b1 100644 --- a/tests/bench/pure-quickhull/pure-quickhull.mlb +++ b/tests/bench/pure-quickhull/pure-quickhull.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb TreeSeq.sml Split.sml Quickhull.sml diff --git a/tests/bench/pure-skyline/pure-skyline.mlb b/tests/bench/pure-skyline/pure-skyline.mlb index 00f8c074a..20d3615dd 100644 --- a/tests/bench/pure-skyline/pure-skyline.mlb +++ b/tests/bench/pure-skyline/pure-skyline.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb FastHashRand.sml CityGen.sml Skyline.sml diff --git a/tests/bench/quickhull/quickhull.mlb b/tests/bench/quickhull/quickhull.mlb index 1f92f9fd9..599e6986b 100644 --- a/tests/bench/quickhull/quickhull.mlb +++ b/tests/bench/quickhull/quickhull.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb ParseFile.sml TreeSeq.sml diff --git a/tests/bench/random/random.mlb b/tests/bench/random/random.mlb index b85468778..00d163b4c 100644 --- a/tests/bench/random/random.mlb +++ b/tests/bench/random/random.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb random.sml diff --git a/tests/bench/range-tree/range-tree.mlb b/tests/bench/range-tree/range-tree.mlb index fa61df8c2..cb2f76121 100644 --- a/tests/bench/range-tree/range-tree.mlb +++ b/tests/bench/range-tree/range-tree.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb RangeTree.sml main.sml diff --git a/tests/bench/raytracer/raytracer.mlb b/tests/bench/raytracer/raytracer.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/raytracer/raytracer.mlb +++ b/tests/bench/raytracer/raytracer.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/reverb/reverb.mlb b/tests/bench/reverb/reverb.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/reverb/reverb.mlb +++ b/tests/bench/reverb/reverb.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/samplesort/samplesort.mlb b/tests/bench/samplesort/samplesort.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/samplesort/samplesort.mlb +++ b/tests/bench/samplesort/samplesort.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/seam-carve-index/seam-carve-index.mlb b/tests/bench/seam-carve-index/seam-carve-index.mlb index a7c6b4334..215968970 100644 --- a/tests/bench/seam-carve-index/seam-carve-index.mlb +++ b/tests/bench/seam-carve-index/seam-carve-index.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb VerticalSeamIndexMap.sml SCI.sml main.sml diff --git a/tests/bench/seam-carve/seam-carve.mlb b/tests/bench/seam-carve/seam-carve.mlb index b078b011a..8f9c5c603 100644 --- a/tests/bench/seam-carve/seam-carve.mlb +++ b/tests/bench/seam-carve/seam-carve.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SC.sml main.sml diff --git a/tests/bench/shuf/shuf.mlb b/tests/bench/shuf/shuf.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/shuf/shuf.mlb +++ b/tests/bench/shuf/shuf.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/skyline/skyline.mlb b/tests/bench/skyline/skyline.mlb index 00f8c074a..20d3615dd 100644 --- a/tests/bench/skyline/skyline.mlb +++ b/tests/bench/skyline/skyline.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb FastHashRand.sml CityGen.sml Skyline.sml diff --git a/tests/bench/spanner/spanner.mlb b/tests/bench/spanner/spanner.mlb index 66aa6e19e..54415e35a 100644 --- a/tests/bench/spanner/spanner.mlb +++ b/tests/bench/spanner/spanner.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb ../ldd/LDD.sml Spanner.sml main.sml diff --git a/tests/bench/sparse-mxv-opt/sparse-mxv-opt.mlb b/tests/bench/sparse-mxv-opt/sparse-mxv-opt.mlb index eded4f9db..551a98a1c 100644 --- a/tests/bench/sparse-mxv-opt/sparse-mxv-opt.mlb +++ b/tests/bench/sparse-mxv-opt/sparse-mxv-opt.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SparseMxV.sml main.sml diff --git a/tests/bench/sparse-mxv/sparse-mxv.mlb b/tests/bench/sparse-mxv/sparse-mxv.mlb index 6b60b1438..ed7e69797 100644 --- a/tests/bench/sparse-mxv/sparse-mxv.mlb +++ b/tests/bench/sparse-mxv/sparse-mxv.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MkMXV.sml main.sml diff --git a/tests/bench/subset-sum/subset-sum.mlb b/tests/bench/subset-sum/subset-sum.mlb index a96518275..cba3e06a8 100644 --- a/tests/bench/subset-sum/subset-sum.mlb +++ b/tests/bench/subset-sum/subset-sum.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb SubsetSumTiled.sml main.sml \ No newline at end of file diff --git a/tests/bench/suffix-array/suffix-array.mlb b/tests/bench/suffix-array/suffix-array.mlb index 91505ac40..49037ac80 100644 --- a/tests/bench/suffix-array/suffix-array.mlb +++ b/tests/bench/suffix-array/suffix-array.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb AS.sml BruteForce.sml PrefixDoubling.sml diff --git a/tests/bench/tape-delay/tape-delay.mlb b/tests/bench/tape-delay/tape-delay.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/tape-delay/tape-delay.mlb +++ b/tests/bench/tape-delay/tape-delay.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/tinykaboom/tinykaboom.mlb b/tests/bench/tinykaboom/tinykaboom.mlb index 5c55ee69c..6a0e4cd9d 100644 --- a/tests/bench/tinykaboom/tinykaboom.mlb +++ b/tests/bench/tinykaboom/tinykaboom.mlb @@ -1,4 +1,4 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb f32.sml vec3.sml TinyKaboom.sml diff --git a/tests/bench/to-gif/to-gif.mlb b/tests/bench/to-gif/to-gif.mlb index eb664c9d2..5f06290fb 100644 --- a/tests/bench/to-gif/to-gif.mlb +++ b/tests/bench/to-gif/to-gif.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb main.sml diff --git a/tests/bench/tokens/tokens.mlb b/tests/bench/tokens/tokens.mlb index b0046f5d6..01809d9b0 100644 --- a/tests/bench/tokens/tokens.mlb +++ b/tests/bench/tokens/tokens.mlb @@ -1,2 +1,2 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb tokens.sml diff --git a/tests/bench/triangle-count/triangle-count.mlb b/tests/bench/triangle-count/triangle-count.mlb index ffb73081d..e3cc8eb11 100644 --- a/tests/bench/triangle-count/triangle-count.mlb +++ b/tests/bench/triangle-count/triangle-count.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb TriangleCount.sml main.sml diff --git a/tests/bench/wc-opt/wc-opt.mlb b/tests/bench/wc-opt/wc-opt.mlb index 7ac8e8c4d..568dd94ff 100644 --- a/tests/bench/wc-opt/wc-opt.mlb +++ b/tests/bench/wc-opt/wc-opt.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb WC.sml main.sml diff --git a/tests/bench/wc/wc.mlb b/tests/bench/wc/wc.mlb index 2110c6e90..bc7303f32 100644 --- a/tests/bench/wc/wc.mlb +++ b/tests/bench/wc/wc.mlb @@ -1,3 +1,3 @@ -../mpllib/sources.$(COMPAT).mlb +../../mpllib/sources.$(COMPAT).mlb MkWC.sml main.sml From b0211a7d84651e4366ad6d563f51ce082cc4919c Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 15:05:51 -0400 Subject: [PATCH 07/18] ignore generated inputs (size is too large) --- tests/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/.gitignore diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 000000000..6b9a1e9dd --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +inputs/ From f1f6819e69cfc3973be2cde3fe1c0364a0c0a7d3 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 17:03:30 -0400 Subject: [PATCH 08/18] ignore pbbsbench --- tests/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/.gitignore b/tests/.gitignore index 6b9a1e9dd..a8ee02502 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1,2 @@ inputs/ +pbbsbench/ From 2d33f37ad78b9e3ab24911409d6385246e3806f6 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Wed, 10 Sep 2025 17:04:04 -0400 Subject: [PATCH 09/18] add makefiles for building tests & setting up inputs --- tests/Makefile | 46 ++++++++++++++++++++++++++++++++++++++++++++ tests/bench/Makefile | 20 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/Makefile create mode 100644 tests/bench/Makefile diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 000000000..6d0fd4fb9 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,46 @@ +PBBS_DIR?=pbbsbench +INPUT_DIR:=inputs + +RAND_PTS :=$(PBBS_DIR)/testData/geometryData/randPoints +RMAT_GRAPH :=$(PBBS_DIR)/testData/graphData/rMatGraph + +UC_INPUTS := $(INPUT_DIR)/uniform-circle-1M $(INPUT_DIR)/uniform-circle-20M +RMAT_INPUTS := $(INPUT_DIR)/rmat-1M-symm $(INPUT_DIR)/rmat-10M-symm +RMAT_BIN_INPUTS := $(patsubst %,%-bin,$(RMAT_INPUTS)) +ALL_INPUTS := $(UC_INPUTS) $(RMAT_INPUTS) $(RMAT_BIN_INPUTS) + +.PHONY: all clean deepclean + +all: inputs + +inputs: $(ALL_INPUTS) + +$(PBBS_DIR): + git clone https://github.com/cmuparlay/pbbsbench $@ + git -C $@ submodule update --init --recursive + +$(RAND_PTS) $(RMAT_GRAPH): $(PBBS_DIR) + $(MAKE) -C $(@D) $(@F) + +bench/graphio.bin: + $(MAKE) -C $(@D) SMLC=mlton $(@F) + +$(INPUT_DIR)/uniform-circle-%: $(RAND_PTS) + $< -s $(subst M,000000,$(*)) $@ + +$(INPUT_DIR)/rmat-%-symm: $(RMAT_GRAPH) + $< -s 15210 -o -j $(subst M,000000,$(*)) $@ + +$(INPUT_DIR)/rmat-%-symm-bin: bench/graphio.bin $(INPUT_DIR)/rmat-%-symm + $< $(word 2, $^) -outfile $@ + +$(INPUT_DIR): + mkdir -p $@ + +clean: + rm -rf $(INPUT_DIR) + rm -f bench/graphio.bin + +deepclean: clean + -$(MAKE) -C $(PBBS_DIR) clean 2>/dev/null || true + rm -rf $(PBBS_DIR) diff --git a/tests/bench/Makefile b/tests/bench/Makefile new file mode 100644 index 000000000..cba89b0a3 --- /dev/null +++ b/tests/bench/Makefile @@ -0,0 +1,20 @@ +DEFAULT_FLAGS?=-default-type int64 -default-type word64 +SMLC?=mpl + +COMPILER_NAME:=$(notdir $(SMLC)) + +SUBDIRS := $(sort $(dir $(wildcard */*.mlb))) +TARGETS := $(patsubst %/, %, $(SUBDIRS)) + +SUBDIRS := $(sort $(dir $(wildcard */*.mlb))) +TARGETS := $(patsubst %/, %.bin, $(SUBDIRS)) + +.PHONY: all clean + +all: $(TARGETS) + +%.bin: %/*.mlb + $(SMLC) -output $@ -mlb-path-var 'COMPAT $(COMPILER_NAME)' $(DEFAULT_FLAGS) $< + +clean: + rm -f $(TARGETS) From a23f7a4a8214974cefdee61b381d2b29e08d797f Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 14:26:51 -0400 Subject: [PATCH 10/18] ignore binaries --- tests/bench/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/bench/.gitignore diff --git a/tests/bench/.gitignore b/tests/bench/.gitignore new file mode 100644 index 000000000..a8a0dcec4 --- /dev/null +++ b/tests/bench/.gitignore @@ -0,0 +1 @@ +*.bin From a3a54cd4700e0c2e6d21f57a322d13f2d1f85e56 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 14:29:36 -0400 Subject: [PATCH 11/18] delete tests with invalid functions --- tests/bench/connectivity/Connectivity.sml | 28 --- tests/bench/connectivity/connectivity.mlb | 4 - tests/bench/connectivity/main.sml | 73 ------- .../DelaunayTriangulation.sml | 191 ------------------ .../delaunay-mostly-pure.mlb | 3 - tests/bench/delaunay-mostly-pure/main.sml | 126 ------------ tests/bench/delaunay-mostly-pure/test.sml | 125 ------------ tests/bench/spanner/Spanner.sml | 41 ---- tests/bench/spanner/main.sml | 73 ------- tests/bench/spanner/spanner.mlb | 4 - 10 files changed, 668 deletions(-) delete mode 100644 tests/bench/connectivity/Connectivity.sml delete mode 100644 tests/bench/connectivity/connectivity.mlb delete mode 100644 tests/bench/connectivity/main.sml delete mode 100644 tests/bench/delaunay-mostly-pure/DelaunayTriangulation.sml delete mode 100644 tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb delete mode 100644 tests/bench/delaunay-mostly-pure/main.sml delete mode 100644 tests/bench/delaunay-mostly-pure/test.sml delete mode 100644 tests/bench/spanner/Spanner.sml delete mode 100644 tests/bench/spanner/main.sml delete mode 100644 tests/bench/spanner/spanner.mlb diff --git a/tests/bench/connectivity/Connectivity.sml b/tests/bench/connectivity/Connectivity.sml deleted file mode 100644 index 2327c437c..000000000 --- a/tests/bench/connectivity/Connectivity.sml +++ /dev/null @@ -1,28 +0,0 @@ -structure Connectivity = -struct - type 'a seq = 'a Seq.t - - structure G = AdjacencyGraph(Int) - structure V = G.Vertex - structure AS = ArraySlice - - type vertex = G.vertex - - (* review this code *) - fun connectivity g b = - let - val n = G.numVertices g - val (clusters, _) = LDD.ldd g b - val (g', center_label) = AdjInt.contract clusters g - in - if (G.numEdges g') = 0 then clusters - else - let - val l' = connectivity g' b - (* val _ = print ("edges = " ^ (Int.toString (G.numEdges g')) ^ " vertices = " ^ (Int.toString (G.numVertices g')) ^ "\n") *) - fun label u = center_label (Seq.nth clusters u) - in - Seq.tabulate label n - end - end -end \ No newline at end of file diff --git a/tests/bench/connectivity/connectivity.mlb b/tests/bench/connectivity/connectivity.mlb deleted file mode 100644 index 598d7df3f..000000000 --- a/tests/bench/connectivity/connectivity.mlb +++ /dev/null @@ -1,4 +0,0 @@ -../../mpllib/sources.$(COMPAT).mlb -../ldd/LDD.sml -Connectivity.sml -main.sml diff --git a/tests/bench/connectivity/main.sml b/tests/bench/connectivity/main.sml deleted file mode 100644 index 468a02883..000000000 --- a/tests/bench/connectivity/main.sml +++ /dev/null @@ -1,73 +0,0 @@ -structure CLA = CommandLineArgs -structure G = AdjacencyGraph(Int) - -val source = CLA.parseInt "source" 0 -val doCheck = CLA.parseFlag "check" - -(* -val N = CLA.parseInt "N" 10000000 -val D = CLA.parseInt "D" 10 - -val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) -val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") -val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") -val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") -*) - -val filename = - case CLA.positional () of - [x] => x - | _ => Util.die "missing filename" - -val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) -val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") -val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") - -val (_, tm) = Util.getTime (fn _ => - if G.parityCheck graph then () - else TextIO.output (TextIO.stdErr, - "WARNING: parity check failed; graph might not be symmetric " ^ - "or might have duplicate- or self-edges\n")) -val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") - - -val b = (CommandLineArgs.parseReal "b" 0.3) - -val P = Benchmark.run "running connectivity: " (fn _ => Connectivity.connectivity graph b) -(* val _ = print ("num-triangles = " ^ (Int.toString P) ^ "\n") *) -(* val _ = LDD.check_ldd graph (#1 P) (#2 P) *) -(* val _ = Benchmark.run "running connectivity" (fn _ => LDD.connectivity graph b) *) -(* -val numVisited = - SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) - (fn i => if Seq.nth P i >= 0 then 1 else 0) -val _ = print ("visited " ^ Int.toString numVisited ^ "\n") - -fun numHops P hops v = - if hops > Seq.length P then ~2 - else if Seq.nth P v = ~1 then ~1 - else if Seq.nth P v = v then hops - else numHops P (hops+1) (Seq.nth P v) - -val maxHops = - SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) -val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") - -fun check () = - let - val (P', serialTime) = - Util.getTime (fn _ => SerialBFS.bfs graph source) - - val correct = - Seq.length P = Seq.length P' - andalso - SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) - (fn i => numHops P 0 i = numHops P' 0 i) - in - print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); - print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") - end - -val _ = if doCheck then check () else () - -val _ = GCStats.report () *) diff --git a/tests/bench/delaunay-mostly-pure/DelaunayTriangulation.sml b/tests/bench/delaunay-mostly-pure/DelaunayTriangulation.sml deleted file mode 100644 index 8f071b116..000000000 --- a/tests/bench/delaunay-mostly-pure/DelaunayTriangulation.sml +++ /dev/null @@ -1,191 +0,0 @@ -structure DelaunayTriangulation : -sig - val triangulate: Geometry2D.point Seq.t -> Topology2D.mesh -end = -struct - - val showDelaunayRoundStats = - CommandLineArgs.parseFlag "show-delaunay-round-stats" - - structure G = Geometry2D - structure T = Topology2D - structure NN = NearestNeighbors - structure A = Array - structure AS = ArraySlice - - type vertex = T.vertex - type simplex = T.simplex - - val BOUNDARY_SIZE = 10 - - fun generateBoundary pts = - let - val p0 = Seq.nth pts 0 - val minCorner = Seq.reduce G.Point.minCoords p0 pts - val maxCorner = Seq.reduce G.Point.maxCoords p0 pts - val diagonal = G.Point.sub (maxCorner, minCorner) - val size = G.Vector.length diagonal - val stretch = 10.0 - val radius = stretch*size - val center = G.Vector.add (minCorner, G.Vector.scaleBy 0.5 diagonal) - in - T.boundaryCircle {center=center, radius=radius, size=BOUNDARY_SIZE} - end - - - fun writeMax a i x = - let - fun loop old = - if x <= old then () else - let - val old' = Concurrency.casArray (a, i) (old, x) - in - if old' = old then () - else loop old' - end - in - loop (A.sub (a, i)) - end - - - fun reserveForInsert reserved mesh start (pt, id) = - let - val (cavity, perimeter) = - T.findCavityAndPerimeter mesh start pt - in - List.app (fn v => writeMax reserved v id) perimeter; - (cavity, perimeter) - end - - - fun resetAndCheckPerimeter reserved perimeter id = - List.foldl - (fn (v, allMine) => - if A.sub (reserved, v) = id then - (A.update (reserved, v, ~1); allMine) - else - false) - true - perimeter - - - fun triangulate inputPts = - let - val maxBatch = Util.ceilDiv (Seq.length inputPts) 100 - val totalNumVertices = BOUNDARY_SIZE + Seq.length inputPts - val reserved = - SeqBasis.tabulate 10000 (0, totalNumVertices) (fn _ => ~1) - - fun batchInsert mesh (nn: NN.t) pointsToInsert = - let - val n = T.numVertices mesh - val m = Seq.length pointsToInsert - - val attempts = - AS.full (SeqBasis.tabulate 100 (0, m) (fn i => - let - val pt = Seq.nth pointsToInsert i - val start: simplex = - (T.triangleOfVertex mesh (NN.nearestNeighbor nn pt), 0) - in - reserveForInsert reserved mesh start - (Seq.nth pointsToInsert i, i) - end)) - - val winnerFlags = - AS.full (SeqBasis.tabulate 1000 (0, m) (fn i => - let - val (_, perimeter) = Seq.nth attempts i - in - resetAndCheckPerimeter reserved perimeter i - end)) - - val winners = - AS.full (SeqBasis.tabFilter 1000 (0, m) (fn i => - if Seq.nth winnerFlags i then - SOME (#1 (Seq.nth attempts i), Seq.nth pointsToInsert i) - else - NONE)) - - val losers = - AS.full (SeqBasis.filter 1000 (0, m) - (Seq.nth pointsToInsert) - (not o Seq.nth winnerFlags)) - - val mesh' = T.ripAndTent winners mesh - in - (mesh', losers) - end - - val nnRebuildMultiplier = 10 - - fun shouldRebuild numNextRebuild totalRemaining = - let - val n = Seq.length inputPts - val numDone = n - totalRemaining - in - numDone >= numNextRebuild - andalso - numDone <= n div nnRebuildMultiplier - end - - - fun loop numRounds mesh (nn: NN.t) numNextRebuild losers remaining = - if Seq.length losers + Seq.length remaining = 0 then - (numRounds, mesh) - else if shouldRebuild numNextRebuild (Seq.length losers + Seq.length remaining) then - let - val nn = NN.makeTree 16 (T.getPoints mesh) - val numNextRebuild = numNextRebuild * nnRebuildMultiplier - in - if not showDelaunayRoundStats then () else - print ("rebuilt nn; next rebuild at " ^ Int.toString numNextRebuild ^ "\n"); - - loop numRounds mesh nn numNextRebuild losers remaining - end - else - let - val numRetry = Seq.length losers - val totalRemaining = numRetry + Seq.length remaining - val numDone = Seq.length inputPts - totalRemaining - val desiredSize = - Int.min (maxBatch, Int.min (1 + numDone div 50, Seq.length inputPts - numDone)) - val numAdditional = - Int.max (0, Int.min (desiredSize - numRetry, Seq.length remaining)) - val thisBatchSize = numAdditional + numRetry - - val newcomers = Seq.take remaining numAdditional - val remaining = Seq.drop remaining numAdditional - val (mesh, losers) = - batchInsert mesh nn (Seq.append (losers, newcomers)) - - val rate = - Real.round (100.0 * (Real.fromInt (thisBatchSize - Seq.length losers) - / Real.fromInt thisBatchSize)) - - val _ = - if not showDelaunayRoundStats then () else - print ("round " ^ Int.toString numRounds - ^ "\tdone " ^ Int.toString numDone - ^ "\tremaining " ^ Int.toString totalRemaining - ^ "\tdesired " ^ Int.toString desiredSize - ^ "\tretrying " ^ Int.toString numRetry - ^ "\tfresh " ^ Int.toString numAdditional - ^ "\tsuccess-rate " ^ Int.toString rate ^ "%\n") - in - loop (numRounds+1) mesh nn numNextRebuild losers remaining - end - - val initialMesh = generateBoundary inputPts - val initialNN = NN.makeTree 16 (T.getPoints initialMesh) - val numNextRebuild = 100 - - val (numRounds, finalMesh) = - loop 0 initialMesh initialNN numNextRebuild (Seq.empty()) inputPts - - val _ = print ("num rounds " ^ Int.toString numRounds ^ "\n") - in - finalMesh - end - -end diff --git a/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb b/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb deleted file mode 100644 index 167f0bd01..000000000 --- a/tests/bench/delaunay-mostly-pure/delaunay-mostly-pure.mlb +++ /dev/null @@ -1,3 +0,0 @@ -../../mpllib/sources.$(COMPAT).mlb -DelaunayTriangulation.sml -main.sml diff --git a/tests/bench/delaunay-mostly-pure/main.sml b/tests/bench/delaunay-mostly-pure/main.sml deleted file mode 100644 index 6fdaaef2e..000000000 --- a/tests/bench/delaunay-mostly-pure/main.sml +++ /dev/null @@ -1,126 +0,0 @@ -structure CLA = CommandLineArgs -structure T = Topology2D -structure DT = DelaunayTriangulation - -val n = CLA.parseInt "n" (1000 * 1000) -val seed = CLA.parseInt "seed" 15210 - -fun genReal i = - let - val x = Word64.fromInt (seed + i) - in - Real.fromInt (Word64.toInt (Word64.mod (Util.hash64 x, 0w1000000))) - / 1000000.0 - end - -fun genPoint i = (genReal (2*i), genReal (2*i + 1)) -val (input, tm) = Util.getTime (fn _ => Seq.tabulate genPoint n) -val _ = print ("generated input in " ^ Time.fmt 4 tm ^ "s\n") - -val mesh = Benchmark.run "delaunay" (fn _ => DT.triangulate input) - -(* val _ = - print ("\n" ^ T.toString mesh ^ "\n") *) - - -(* ========================================================================== - * output result image - * only works if all input points are in range [0,1) - *) - -val filename = CLA.parseString "output" "" -val _ = - if filename <> "" then () - else ( print ("to see output, use -output and -resolution arguments\n" ^ - "for example: delaunay -n 1000 -output result.ppm -resolution 1000\n") - ; OS.Process.exit OS.Process.success - ) - -val t0 = Time.now () - -val resolution = CLA.parseInt "resolution" 1000 -val width = resolution -val height = resolution - -val image = - { width = width - , height = height - , data = Seq.tabulate (fn _ => Color.white) (width*height) - } - -fun set (i, j) x = - if 0 <= i andalso i < height andalso - 0 <= j andalso j < width - then ArraySlice.update (#data image, i*width + j, x) - else () - -val r = Real.fromInt resolution -fun px x = Real.floor (x * r) -fun pos (x, y) = (resolution - px x - 1, px y) - -fun horizontalLine i (j0, j1) = - if j1 < j0 then horizontalLine i (j1, j0) - else Util.for (j0, j1) (fn j => set (i, j) Color.red) - -fun sign xx = - case Int.compare (xx, 0) of LESS => ~1 | EQUAL => 0 | GREATER => 1 - -(* Bresenham's line algorithm *) -fun line (x1, y1) (x2, y2) = - let - val w = x2 - x1 - val h = y2 - y1 - val dx1 = sign w - val dy1 = sign h - val (longest, shortest, dx2, dy2) = - if Int.abs w > Int.abs h then - (Int.abs w, Int.abs h, dx1, 0) - else - (Int.abs h, Int.abs w, 0, dy1) - - fun loop i numerator x y = - if i > longest then () else - let - val numerator = numerator + shortest; - in - set (x, y) Color.red; - if numerator >= longest then - loop (i+1) (numerator-longest) (x+dx1) (y+dy1) - else - loop (i+1) numerator (x+dx2) (y+dy2) - end - in - loop 0 (longest div 2) x1 y1 - end - -(* draw all triangle edges as straight red lines *) -val _ = ForkJoin.parfor 1000 (0, T.numTriangles mesh) (fn i => - let - fun vpos v = pos (T.vdata mesh v) - val T.Tri {vertices=(u,v,w), ...} = T.tdata mesh i - in - line (vpos u) (vpos v); - line (vpos v) (vpos w); - line (vpos w) (vpos u) - end) - -(* mark input points as a pixel *) -val _ = - ForkJoin.parfor 10000 (0, Seq.length input) (fn i => - let - val (x, y) = pos (Seq.nth input i) - fun b spot = set spot Color.black - in - b (x-1, y); - b (x, y-1); - b (x, y); - b (x, y+1); - b (x+1, y) - end) - -val t1 = Time.now () - -val _ = print ("generated image in " ^ Time.fmt 4 (Time.- (t1, t0)) ^ "s\n") - -val (_, tm) = Util.getTime (fn _ => PPM.write filename image) -val _ = print ("wrote to " ^ filename ^ " in " ^ Time.fmt 4 tm ^ "s\n") diff --git a/tests/bench/delaunay-mostly-pure/test.sml b/tests/bench/delaunay-mostly-pure/test.sml deleted file mode 100644 index a82638d33..000000000 --- a/tests/bench/delaunay-mostly-pure/test.sml +++ /dev/null @@ -1,125 +0,0 @@ -structure CLA = CommandLineArgs -structure T = Topology2D -structure DT = DelaunayTriangulation - -val (filename, testPtStr) = - case CLA.positional () of - [x, y] => (x, y) - | _ => Util.die "usage: ./foo " - -val testType = CLA.parseString "test" "split" - -val testPoint = - case List.mapPartial Real.fromString (String.tokens (fn c => c = #",") testPtStr) of - [x,y] => (x,y) - | _ => Util.die ("bad test point") - -val (mesh, tm) = Util.getTime (fn _ => T.parseFile filename) -val _ = print ("num vertices: " ^ Int.toString (T.numVertices mesh) ^ "\n") -val _ = print ("num edges: " ^ Int.toString (T.numTriangles mesh) ^ "\n") - -val _ = print ("\n" ^ T.toString mesh ^ "\n\n") - -val start: T.simplex = (0, 0) - -fun simpToString (t, i) = - "triangle " ^ Int.toString t ^ " orientation " ^ Int.toString i ^ ": " ^ - let - val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t - in - String.concatWith " " (List.map Int.toString - (if i = 0 then [a,b,c] - else if i = 1 then [b,c,a] - else [c,a,b])) - end - -fun triToString t = - "triangle " ^ Int.toString t ^ ": " ^ - let - val T.Tri {vertices=(a,b,c), ...} = T.tdata mesh t - in - String.concatWith " " (List.map Int.toString [a,b,c]) - end - -val _ = Util.for (0, T.numVertices mesh) (fn v => - let - val s = T.find mesh v start - in - print ("found " ^ Int.toString v ^ ": " ^ simpToString s ^ "\n") - end) - - -(* ======================================================================== *) - -fun testSplit () = - let - val _ = print ("===================================\nTESTING SPLIT\n") - - val ((center, tris), verts) = T.findCavityAndPerimeter mesh start testPoint - val _ = - print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") - val _ = - print ("CAVITY MEMBERS ARE:\n" - ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") - val _ = - print ("CAVITY PERIMETER VERTICES ARE:\n " - ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") - - - val mesh' = T.split mesh center testPoint - val _ = - print ("===================================\nAFTER SPLIT:\n" ^ T.toString mesh' ^ "\n") - in - () - end - -(* ======================================================================== *) - -fun testFlip () = - let - val _ = print ("===================================\nTESTING FLIP\n") - - val simp = T.findPoint mesh testPoint start - val _ = - print ("SIMPLEX CONTAINING POINT:\n " ^ simpToString simp ^ "\n") - - val mesh' = T.flip mesh simp - val _ = - print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") - in - () - end - -(* ======================================================================== *) - -fun testRipAndTent () = - let - val _ = print ("===================================\nTESTING RIP-AND-TENT\n") - - val (cavity as (center, tris), verts) = - T.findCavityAndPerimeter mesh start testPoint - - val _ = - print ("CAVITY CENTER IS:\n " ^ triToString center ^ "\n") - val _ = - print ("CAVITY MEMBERS ARE:\n" - ^ String.concatWith "\n" (List.map (fn x => " " ^ simpToString x) tris) ^ "\n") - val _ = - print ("CAVITY PERIMETER VERTICES ARE:\n " - ^ String.concatWith " " (List.map (fn x => Int.toString x) verts) ^ "\n") - - val mesh' = T.ripAndTentOne (cavity, testPoint) mesh - val _ = - print ("===================================\nAFTER FLIP:\n" ^ T.toString mesh' ^ "\n") - in - () - end - -(* ======================================================================== *) - -val _ = - case testType of - "split" => testSplit () - | "flip" => testFlip () - | "rip-and-tent" => testRipAndTent () - | _ => Util.die ("unknown test type") diff --git a/tests/bench/spanner/Spanner.sml b/tests/bench/spanner/Spanner.sml deleted file mode 100644 index 4b13b08ba..000000000 --- a/tests/bench/spanner/Spanner.sml +++ /dev/null @@ -1,41 +0,0 @@ -structure Spanner = -struct - type 'a seq = 'a Seq.t - structure G = AdjacencyGraph(Int) - structure V = G.Vertex - structure AS = ArraySlice - - type vertex = G.vertex - - fun spanner g k = - let - val n = G.numVertices g - val b = (Math.ln (Real.fromInt n))/(Real.fromInt (2 * k)) - val (clusters, parents) = LDD.ldd g b - fun is_center i = if i = (Seq.nth clusters i) then 1 else 0 - val compact_clusters = SeqBasis.scan 10000 Int.+ 0 (0, n) is_center - val num_clusters = Array.sub (compact_clusters, n) - val _ = print ("number of clusters = " ^ (Int.toString (num_clusters)) ^ "\n") - fun center i = Array.sub (compact_clusters, (Seq.nth clusters i)) - val intra_edges = Seq.tabulate (fn i => (i, Seq.nth parents i)) n - val hash_sim = Seq.tabulate (fn i => NONE) (num_clusters*num_clusters) - fun icu u = - let - val cu = center u - fun add_edge i = - let - val ci = center i - val indexi = cu*num_clusters + ci - in - if (cu < ci) then AS.update (hash_sim, indexi, SOME(u, i)) - else () - end - in - Seq.foreach (G.neighbors g u) (fn (i, si) => add_edge si) - end - val _ = ForkJoin.parfor 10000 (0, n) (fn i => icu i) - val inter_edges = AS.full(SeqBasis.tabFilter 10000 (0, num_clusters*num_clusters) (Seq.nth hash_sim)) - in - Seq.append (intra_edges, inter_edges) - end -end \ No newline at end of file diff --git a/tests/bench/spanner/main.sml b/tests/bench/spanner/main.sml deleted file mode 100644 index 2065354be..000000000 --- a/tests/bench/spanner/main.sml +++ /dev/null @@ -1,73 +0,0 @@ -structure CLA = CommandLineArgs -structure G = AdjacencyGraph(Int) - -val source = CLA.parseInt "source" 0 -val doCheck = CLA.parseFlag "check" - -(* -val N = CLA.parseInt "N" 10000000 -val D = CLA.parseInt "D" 10 - -val (graph, tm) = Util.getTime (fn _ => G.randSymmGraph N D) -val _ = print ("generated graph in " ^ Time.fmt 4 tm ^ "s\n") -val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") -val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") -*) - -val filename = - case CLA.positional () of - [x] => x - | _ => Util.die "missing filename" - -val (graph, tm) = Util.getTime (fn _ => G.parseFile filename) -val _ = print ("num vertices: " ^ Int.toString (G.numVertices graph) ^ "\n") -val _ = print ("num edges: " ^ Int.toString (G.numEdges graph) ^ "\n") - -val (_, tm) = Util.getTime (fn _ => - if G.parityCheck graph then () - else TextIO.output (TextIO.stdErr, - "WARNING: parity check failed; graph might not be symmetric " ^ - "or might have duplicate- or self-edges\n")) -val _ = print ("parity check in " ^ Time.fmt 4 tm ^ "s\n") - - -val k = (CommandLineArgs.parseInt "k" 3) - -val P = Benchmark.run "running spanner: " (fn _ => Spanner.spanner graph k) -(* val _ = print ("num-triangles = " ^ (Int.toString P) ^ "\n") *) -(* val _ = LDD.check_ldd graph (#1 P) (#2 P) *) -(* val _ = Benchmark.run "running connectivity" (fn _ => LDD.connectivity graph b) *) -(* -val numVisited = - SeqBasis.reduce 10000 op+ 0 (0, Seq.length P) - (fn i => if Seq.nth P i >= 0 then 1 else 0) -val _ = print ("visited " ^ Int.toString numVisited ^ "\n") - -fun numHops P hops v = - if hops > Seq.length P then ~2 - else if Seq.nth P v = ~1 then ~1 - else if Seq.nth P v = v then hops - else numHops P (hops+1) (Seq.nth P v) - -val maxHops = - SeqBasis.reduce 100 Int.max ~3 (0, G.numVertices graph) (numHops P 0) -val _ = print ("max dist " ^ Int.toString maxHops ^ "\n") - -fun check () = - let - val (P', serialTime) = - Util.getTime (fn _ => SerialBFS.bfs graph source) - - val correct = - Seq.length P = Seq.length P' - andalso - SeqBasis.reduce 10000 (fn (a, b) => a andalso b) true (0, Seq.length P) - (fn i => numHops P 0 i = numHops P' 0 i) - in - print ("serial finished in " ^ Time.fmt 4 serialTime ^ "s\n"); - print ("correct? " ^ (if correct then "yes" else "no") ^ "\n") - end - -val _ = if doCheck then check () else () - -val _ = GCStats.report () *) diff --git a/tests/bench/spanner/spanner.mlb b/tests/bench/spanner/spanner.mlb deleted file mode 100644 index 54415e35a..000000000 --- a/tests/bench/spanner/spanner.mlb +++ /dev/null @@ -1,4 +0,0 @@ -../../mpllib/sources.$(COMPAT).mlb -../ldd/LDD.sml -Spanner.sml -main.sml From 120a8a25b83c8c47a0f8bfb84196c1d3542bdafa Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 14:30:18 -0400 Subject: [PATCH 12/18] use compatibility layer instead of MLton.parallel --- tests/bench/collect/HashTable.sml | 2 +- tests/bench/delaunay-top-down/HashTable.sml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bench/collect/HashTable.sml b/tests/bench/collect/HashTable.sml index b467268be..0eadfa6df 100644 --- a/tests/bench/collect/HashTable.sml +++ b/tests/bench/collect/HashTable.sml @@ -56,7 +56,7 @@ struct else let val current' = - MLton.Parallel.arrayCompareAndSwap (arr, i) (current, desired) + Concurrency.casArray (arr, i) (current, desired) in if MLton.eq (current', current) then () else loop current' end diff --git a/tests/bench/delaunay-top-down/HashTable.sml b/tests/bench/delaunay-top-down/HashTable.sml index 1aa884596..26c162d1b 100644 --- a/tests/bench/delaunay-top-down/HashTable.sml +++ b/tests/bench/delaunay-top-down/HashTable.sml @@ -80,7 +80,7 @@ struct fun bcas (r, old, new) = - MLton.eq (old, MLton.Parallel.compareAndSwap r (old, new)) + MLton.eq (old, Concurrency.cas r (old, new)) (* fun atomic_combine_with (f: 'a * 'a -> 'a) (arr: 'a array, i) (x: 'a) = From 73e4846288287f8049af99782259af07b6399ff2 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 14:48:40 -0400 Subject: [PATCH 13/18] add rules for generating words input --- tests/Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/Makefile b/tests/Makefile index 6d0fd4fb9..2b9dad31f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -7,7 +7,8 @@ RMAT_GRAPH :=$(PBBS_DIR)/testData/graphData/rMatGraph UC_INPUTS := $(INPUT_DIR)/uniform-circle-1M $(INPUT_DIR)/uniform-circle-20M RMAT_INPUTS := $(INPUT_DIR)/rmat-1M-symm $(INPUT_DIR)/rmat-10M-symm RMAT_BIN_INPUTS := $(patsubst %,%-bin,$(RMAT_INPUTS)) -ALL_INPUTS := $(UC_INPUTS) $(RMAT_INPUTS) $(RMAT_BIN_INPUTS) +TEXT_INPUTS := $(INPUT_DIR)/words-8 $(INPUT_DIR)/words-32 +ALL_INPUTS := $(UC_INPUTS) $(RMAT_INPUTS) $(RMAT_BIN_INPUTS) $(TEXT_INPUTS) .PHONY: all clean deepclean @@ -34,6 +35,12 @@ $(INPUT_DIR)/rmat-%-symm: $(RMAT_GRAPH) $(INPUT_DIR)/rmat-%-symm-bin: bench/graphio.bin $(INPUT_DIR)/rmat-%-symm $< $(word 2, $^) -outfile $@ +$(INPUT_DIR)/words: + curl -L -o $@ https://raw.githubusercontent.com/dwyl/english-words/refs/heads/master/words_alpha.txt + +$(INPUT_DIR)/words-%: $(INPUT_DIR)/words + shuf -n $$(( $$(wc -l < $<) * $(*) )) -o $@ --repeat $< + $(INPUT_DIR): mkdir -p $@ From ce67e05c14720f83d8a3156abdcdbc8c18e7ad3f Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 14:54:00 -0400 Subject: [PATCH 14/18] document words-n file creation process --- tests/Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Makefile b/tests/Makefile index 2b9dad31f..a092c46e1 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -39,6 +39,12 @@ $(INPUT_DIR)/words: curl -L -o $@ https://raw.githubusercontent.com/dwyl/english-words/refs/heads/master/words_alpha.txt $(INPUT_DIR)/words-%: $(INPUT_DIR)/words + # Evil shell magic: + # 1. count the lines of the original words file + # 2. multiply that by n in words-n + # 3. shuffle with repetition that many times + # + # You can technically create words-n for any n, not just 8 or 32 shuf -n $$(( $$(wc -l < $<) * $(*) )) -o $@ --repeat $< $(INPUT_DIR): From 24958703c6faea743e82441e8d514b02535eac0f Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 17:27:56 -0400 Subject: [PATCH 15/18] use consistent flag names --- tests/bench/dense-matmul/main.sml | 2 +- tests/bench/msort/msort.sml | 2 +- tests/bench/nqueens/nqueens.sml | 4 ++-- tests/bench/ocaml-lu-decomp/main.sml | 4 ++-- tests/bench/palindrome/main.sml | 2 +- tests/bench/primes/primes.sml | 2 +- tests/bench/quickhull/main.sml | 2 +- tests/bench/suffix-array/main.sml | 2 +- tests/bench/wc-opt/main.sml | 10 ++++------ 9 files changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/bench/dense-matmul/main.sml b/tests/bench/dense-matmul/main.sml index aed5ec288..05e906b65 100644 --- a/tests/bench/dense-matmul/main.sml +++ b/tests/bench/dense-matmul/main.sml @@ -1,6 +1,6 @@ structure CLA = CommandLineArgs -val n = CLA.parseInt "N" 1024 +val n = CLA.parseInt "n" 1024 val _ = if Util.boundPow2 n = n then () else Util.die "sidelength N must be a power of two" diff --git a/tests/bench/msort/msort.sml b/tests/bench/msort/msort.sml index 292204fcb..7f3ab9e0d 100644 --- a/tests/bench/msort/msort.sml +++ b/tests/bench/msort/msort.sml @@ -1,6 +1,6 @@ structure CLA = CommandLineArgs -val n = CLA.parseInt "N" (100 * 1000 * 1000) +val n = CLA.parseInt "n" (100 * 1000 * 1000) val _ = print ("N " ^ Int.toString n ^ "\n") val _ = print ("generating " ^ Int.toString n ^ " random integers\n") diff --git a/tests/bench/nqueens/nqueens.sml b/tests/bench/nqueens/nqueens.sml index e309daa94..765b0c13c 100644 --- a/tests/bench/nqueens/nqueens.sml +++ b/tests/bench/nqueens/nqueens.sml @@ -27,8 +27,8 @@ fun countSol n = search 0 [] end -val n = CommandLineArgs.parseInt "N" 13 -val _ = print ("N " ^ Int.toString n ^ "\n") +val n = CommandLineArgs.parseInt "n" 13 +val _ = print ("n " ^ Int.toString n ^ "\n") val msg = "counting number of " ^ Int.toString n ^ "x" ^ Int.toString n ^ " solutions" diff --git a/tests/bench/ocaml-lu-decomp/main.sml b/tests/bench/ocaml-lu-decomp/main.sml index 9f2af65d0..72728bbe7 100644 --- a/tests/bench/ocaml-lu-decomp/main.sml +++ b/tests/bench/ocaml-lu-decomp/main.sml @@ -7,9 +7,9 @@ *) structure CLA = CommandLineArgs -val mat_size = CLA.parseInt "mat_size" 1200 +val mat_size = CLA.parseInt "n" 1200 val chunk_size = CLA.parseInt "chunk_size" 16 -val _ = print ("mat_size " ^ Int.toString mat_size ^ "\n") +val _ = print ("n" ^ Int.toString mat_size ^ "\n") val _ = print ("chunk_size " ^ Int.toString chunk_size ^ "\n") (* ocaml source: diff --git a/tests/bench/palindrome/main.sml b/tests/bench/palindrome/main.sml index bfb649ee4..c49a3c8cc 100644 --- a/tests/bench/palindrome/main.sml +++ b/tests/bench/palindrome/main.sml @@ -1,7 +1,7 @@ structure CLA = CommandLineArgs structure P = Pal -val n = CLA.parseInt "N" (1000 * 1000) +val n = CLA.parseInt "n" (1000 * 1000) (* makes the sequence `ababab...` *) fun gen i = if i mod 2 = 0 then #"a" else #"b" diff --git a/tests/bench/primes/primes.sml b/tests/bench/primes/primes.sml index 3e8308e23..e2dd7cc5f 100644 --- a/tests/bench/primes/primes.sml +++ b/tests/bench/primes/primes.sml @@ -35,7 +35,7 @@ fun primes n = * parse command-line arguments and run *) -val n = CLA.parseInt "N" (100 * 1000 * 1000) +val n = CLA.parseInt "n" (100 * 1000 * 1000) val msg = "generating primes up to " ^ Int.toString n val result = Benchmark.run msg (fn _ => primes n) diff --git a/tests/bench/quickhull/main.sml b/tests/bench/quickhull/main.sml index f007de715..fa2edac0a 100644 --- a/tests/bench/quickhull/main.sml +++ b/tests/bench/quickhull/main.sml @@ -15,7 +15,7 @@ fun randPt seed = (1.0 + r * Math.cos(theta), 1.0 + r * Math.sin(theta)) end -val filename = CLA.parseString "infile" "" +val filename = CLA.parseString "input" "" val outfile = CLA.parseString "outfile" "" val n = CLA.parseInt "n" (1000 * 1000 * 100) val doCheck = CLA.parseFlag "check" diff --git a/tests/bench/suffix-array/main.sml b/tests/bench/suffix-array/main.sml index fb08bfaa3..17e17ab4d 100644 --- a/tests/bench/suffix-array/main.sml +++ b/tests/bench/suffix-array/main.sml @@ -5,7 +5,7 @@ val str = CLA.parseString "str" "" (* val algo = CLA.parseString "algo" "" *) val check = CLA.parseFlag "check" val benchmark = CLA.parseFlag "benchmark" -val benchSize = CLA.parseInt "N" 10000000 +val benchSize = CLA.parseInt "n" 10000000 val printResult = CLA.parseFlag "print" val filename = CLA.parseString "file" "" val rep = case (Int.fromString (CLA.parseString "repeat" "1")) of diff --git a/tests/bench/wc-opt/main.sml b/tests/bench/wc-opt/main.sml index 46cb3a2f2..51fb6c080 100644 --- a/tests/bench/wc-opt/main.sml +++ b/tests/bench/wc-opt/main.sml @@ -2,19 +2,17 @@ structure CLA = CommandLineArgs structure Seq = ArraySequence val n = CLA.parseInt "n" (1000 * 1000 * 100) -val filePath = CLA.parseString "infile" "" - val source = - if filePath = "" then - (*Seq.tabulate (fn _ => #" ") n*) - Seq.tabulate (fn i => Char.chr (Util.hash i mod 255)) n - else + case CLA.positional () of + [filePath] => let val (source, tm) = Util.getTime (fn _ => ReadFile.contentsSeq filePath) val _ = print ("loadtime " ^ Time.fmt 3 tm ^ "\n") in source end + | _ => + Seq.tabulate (fn i => Char.chr (Util.hash i mod 255)) n fun task () = WC.wc source From f9d87c684a9618edd546f864b3278de1cc3df265 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 17:28:47 -0400 Subject: [PATCH 16/18] ignore tests/bin --- tests/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/.gitignore b/tests/.gitignore index a8ee02502..818ad71b8 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,2 +1,3 @@ inputs/ pbbsbench/ +bin/ From 765dd16ca58fa01772709e4d7c60cad2d7c1e488 Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Thu, 11 Sep 2025 17:53:06 -0400 Subject: [PATCH 17/18] push button execution --- tests/Makefile | 124 ++++++++++++++++++++++++++++++++++++----- tests/bench/.gitignore | 1 - tests/bench/Makefile | 20 ------- 3 files changed, 111 insertions(+), 34 deletions(-) delete mode 100644 tests/bench/.gitignore delete mode 100644 tests/bench/Makefile diff --git a/tests/Makefile b/tests/Makefile index a092c46e1..281a27a8f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,8 +1,11 @@ -PBBS_DIR?=pbbsbench -INPUT_DIR:=inputs +PBBS_DIR ?= pbbsbench +INPUT_DIR := inputs +BIN_DIR := bin -RAND_PTS :=$(PBBS_DIR)/testData/geometryData/randPoints -RMAT_GRAPH :=$(PBBS_DIR)/testData/graphData/rMatGraph + +#=================inputs================= +RAND_PTS := $(PBBS_DIR)/testData/geometryData/randPoints +RMAT_GRAPH := $(PBBS_DIR)/testData/graphData/rMatGraph UC_INPUTS := $(INPUT_DIR)/uniform-circle-1M $(INPUT_DIR)/uniform-circle-20M RMAT_INPUTS := $(INPUT_DIR)/rmat-1M-symm $(INPUT_DIR)/rmat-10M-symm @@ -12,9 +15,9 @@ ALL_INPUTS := $(UC_INPUTS) $(RMAT_INPUTS) $(RMAT_BIN_INPUTS) $(TEXT_INPUTS) .PHONY: all clean deepclean -all: inputs +all: input tests -inputs: $(ALL_INPUTS) +input: $(ALL_INPUTS) $(PBBS_DIR): git clone https://github.com/cmuparlay/pbbsbench $@ @@ -23,16 +26,13 @@ $(PBBS_DIR): $(RAND_PTS) $(RMAT_GRAPH): $(PBBS_DIR) $(MAKE) -C $(@D) $(@F) -bench/graphio.bin: - $(MAKE) -C $(@D) SMLC=mlton $(@F) - $(INPUT_DIR)/uniform-circle-%: $(RAND_PTS) $< -s $(subst M,000000,$(*)) $@ $(INPUT_DIR)/rmat-%-symm: $(RMAT_GRAPH) $< -s 15210 -o -j $(subst M,000000,$(*)) $@ -$(INPUT_DIR)/rmat-%-symm-bin: bench/graphio.bin $(INPUT_DIR)/rmat-%-symm +$(INPUT_DIR)/rmat-%-symm-bin: $(BIN_DIR)/graphio.mlton $(INPUT_DIR)/rmat-%-symm $(INPUT_DIR) $< $(word 2, $^) -outfile $@ $(INPUT_DIR)/words: @@ -47,13 +47,111 @@ $(INPUT_DIR)/words-%: $(INPUT_DIR)/words # You can technically create words-n for any n, not just 8 or 32 shuf -n $$(( $$(wc -l < $<) * $(*) )) -o $@ --repeat $< -$(INPUT_DIR): - mkdir -p $@ +$(ALL_INPUTS): | $(INPUT_DIR)/ + +#=================binaries================= +DEFAULT_FLAGS ?= -default-type int64 -default-type word64 +SMLC ?= mlton + +COMPILER_NAME := $(notdir $(SMLC)) +TESTS_NAME := $(sort $(basename $(notdir $(wildcard bench/*/*.mlb)))) +BINARIES := $(addprefix $(BIN_DIR)/,$(TESTS_NAME)) + +$(BIN_DIR)/%.$(COMPILER_NAME): bench/%/*.mlb $(BIN_DIR)/ + $(SMLC) -output $@ -mlb-path-var 'COMPAT $(COMPILER_NAME)' $(DEFAULT_FLAGS) $< + +#=================default parameters for tests================= +primes_N := 100000000 +dense-matmul_N := 1024 +msort_N := 20000000 +suffix-array_N := 1000000 +palindrome_N := 1000000 +nqueens_N := 13 +linefit-opt_N := 500000000 +linearrec_N := 200000000 +bignum-add-opt_N := 500000000 +integrate-opt_N := 500000000 +sparse-mxv-opt_N := 200000000 +mcss-opt_N := 500000000 +ocaml-lu-decomp_N := 1024 +ocaml-binarytrees5_N := 19 + +dedup_W := $(INPUT_DIR)/words-32 +grep_W := $(INPUT_DIR)/words-32 +tokens_W := $(INPUT_DIR)/words-32 +msort-strings_W := $(INPUT_DIR)/words-8 + +delaunay_C := 1M +nearest-nbrs_C := 1M +quickhull_C := 20M + +dedup_PRE := --verbose --no-output +grep_PRE := EE +tokens_PRE := --verbose --no-output +bfs_PRE := --no-dir-opt + +NUMERICAL_TESTS := primes dense-matmul msort suffix-array palindrome \ + nqueens linefit-opt linearrec bignum-add-opt integrate-opt \ + sparse-mxv-opt mcss-opt ocaml-lu-decomp ocaml-binarytrees5 +WORDS_TESTS := dedup grep tokens msort-strings +CIRC_TESTS := delaunay nearest-nbrs quickhull +RMAT_TESTS := bfs centrality low-d-decomp max-indep-set triangle-count wc-opt +MISC_TESTS := tinykaboom reverb seam-carve range-tree raytracer ocaml-nbody-imm + +ALL_TESTS := $(NUMERICAL_TESTS) $(WORDS_TESTS) $(CIRC_TESTS) $(RMAT_TESTS) $(MISC_TESTS) +$(ALL_TESTS): %: $(BIN_DIR)/%.$(COMPILER_NAME) + +tests: $(ALL_TESTS) + +$(NUMERICAL_TESTS): +ifdef N + $(BIN_DIR)/$@.$(COMPILER_NAME) -n $(N) +else + $(BIN_DIR)/$@.$(COMPILER_NAME) -n $($@_N) +endif + +ifdef WORDS +$(WORDS_TESTS): + $(BIN_DIR)/$@.$(COMPILER_NAME) $($@_PRE) $(WORDS) +else +$(WORDS_TESTS): $(%_W) + $(BIN_DIR)/$@.$(COMPILER_NAME) $($@_PRE) $($@_W) +endif + +$(CIRC_TESTS): $(INPUT_DIR)/uniform-circle-$(%_C) + $(BIN_DIR)/$@.$(COMPILER_NAME) -input $< + +$(RMAT_TESTS): $(INPUT_DIR)/rmat-10M-symm-bin + $(BIN_DIR)/$@.$(COMPILER_NAME) $($@_PRE) $< + +tinykaboom: + $(BIN_DIR)/$@.$(COMPILER_NAME) -width 100 -height 100 -frames 10 -fps 1 + +reverb: + $(BIN_DIR)/$@.$(COMPILER_NAME) $(INPUT_DIR)/mangore-waltz.wav + +seam-carve: + $(BIN_DIR)/$@.$(COMPILER_NAME) $(INPUT_DIR)/pano.ppm -num-seams 100 + +range-tree: + $(BIN_DIR)/$@.$(COMPILER_NAME) -n 1000000 -q 1000000 + +raytracer: + $(BIN_DIR)/$@.$(COMPILER_NAME) -n 1000 -m 1000 + +game-of-life: + $(BIN_DIR)/$@.$(COMPILER_NAME) -n_times 100 -board_size 1024 + +ocaml-nbody-imm: + $(BIN_DIR)/$@.$(COMPILER_NAME) -n 500 -num_bodies 1024 clean: rm -rf $(INPUT_DIR) - rm -f bench/graphio.bin + rm -rf $(BIN_DIR) deepclean: clean -$(MAKE) -C $(PBBS_DIR) clean 2>/dev/null || true rm -rf $(PBBS_DIR) + +$(INPUT_DIR)/ $(BIN_DIR)/: + mkdir -p $@ diff --git a/tests/bench/.gitignore b/tests/bench/.gitignore deleted file mode 100644 index a8a0dcec4..000000000 --- a/tests/bench/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.bin diff --git a/tests/bench/Makefile b/tests/bench/Makefile deleted file mode 100644 index cba89b0a3..000000000 --- a/tests/bench/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -DEFAULT_FLAGS?=-default-type int64 -default-type word64 -SMLC?=mpl - -COMPILER_NAME:=$(notdir $(SMLC)) - -SUBDIRS := $(sort $(dir $(wildcard */*.mlb))) -TARGETS := $(patsubst %/, %, $(SUBDIRS)) - -SUBDIRS := $(sort $(dir $(wildcard */*.mlb))) -TARGETS := $(patsubst %/, %.bin, $(SUBDIRS)) - -.PHONY: all clean - -all: $(TARGETS) - -%.bin: %/*.mlb - $(SMLC) -output $@ -mlb-path-var 'COMPAT $(COMPILER_NAME)' $(DEFAULT_FLAGS) $< - -clean: - rm -f $(TARGETS) From 6652537509aa44758b9729db4685c17c067d305b Mon Sep 17 00:00:00 2001 From: Seong-Heon Jung Date: Tue, 16 Sep 2025 21:58:15 -0400 Subject: [PATCH 18/18] fix makefile --- tests/Makefile | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/Makefile b/tests/Makefile index 281a27a8f..13ea052be 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -11,7 +11,8 @@ UC_INPUTS := $(INPUT_DIR)/uniform-circle-1M $(INPUT_DIR)/uniform-circle-20 RMAT_INPUTS := $(INPUT_DIR)/rmat-1M-symm $(INPUT_DIR)/rmat-10M-symm RMAT_BIN_INPUTS := $(patsubst %,%-bin,$(RMAT_INPUTS)) TEXT_INPUTS := $(INPUT_DIR)/words-8 $(INPUT_DIR)/words-32 -ALL_INPUTS := $(UC_INPUTS) $(RMAT_INPUTS) $(RMAT_BIN_INPUTS) $(TEXT_INPUTS) +BIN_INPUTS := $(INPUT_DIR)/mangore-waltz.wav $(INPUT_DIR)/pano.ppm $(INPUT_DIR)/moon-landing.wav +ALL_INPUTS := $(UC_INPUTS) $(RMAT_INPUTS) $(RMAT_BIN_INPUTS) $(TEXT_INPUTS) $(BIN_INPUTS) .PHONY: all clean deepclean @@ -26,6 +27,9 @@ $(PBBS_DIR): $(RAND_PTS) $(RMAT_GRAPH): $(PBBS_DIR) $(MAKE) -C $(@D) $(@F) +$(BIN_INPUTS): + curl -L https://raw.githubusercontent.com/MPLLang/parallel-ml-bench/refs/heads/main/inputs/$(notdir $@) -o $@ + $(INPUT_DIR)/uniform-circle-%: $(RAND_PTS) $< -s $(subst M,000000,$(*)) $@ @@ -101,7 +105,7 @@ MISC_TESTS := tinykaboom reverb seam-carve range-tree raytracer ocaml-nbod ALL_TESTS := $(NUMERICAL_TESTS) $(WORDS_TESTS) $(CIRC_TESTS) $(RMAT_TESTS) $(MISC_TESTS) $(ALL_TESTS): %: $(BIN_DIR)/%.$(COMPILER_NAME) -tests: $(ALL_TESTS) +test: $(ALL_TESTS) $(NUMERICAL_TESTS): ifdef N @@ -110,15 +114,18 @@ else $(BIN_DIR)/$@.$(COMPILER_NAME) -n $($@_N) endif +.SECONDEXPANSION: + ifdef WORDS $(WORDS_TESTS): $(BIN_DIR)/$@.$(COMPILER_NAME) $($@_PRE) $(WORDS) else -$(WORDS_TESTS): $(%_W) - $(BIN_DIR)/$@.$(COMPILER_NAME) $($@_PRE) $($@_W) +$(WORDS_TESTS): $$($$@_W) + $(BIN_DIR)/$@.$(COMPILER_NAME) $($@_PRE) $< endif -$(CIRC_TESTS): $(INPUT_DIR)/uniform-circle-$(%_C) + +$(CIRC_TESTS): $(INPUT_DIR)/uniform-circle-$$($$@_C) $(BIN_DIR)/$@.$(COMPILER_NAME) -input $< $(RMAT_TESTS): $(INPUT_DIR)/rmat-10M-symm-bin @@ -127,10 +134,10 @@ $(RMAT_TESTS): $(INPUT_DIR)/rmat-10M-symm-bin tinykaboom: $(BIN_DIR)/$@.$(COMPILER_NAME) -width 100 -height 100 -frames 10 -fps 1 -reverb: +reverb: $(INPUT_DIR)/mangore-waltz.wav $(BIN_DIR)/$@.$(COMPILER_NAME) $(INPUT_DIR)/mangore-waltz.wav -seam-carve: +seam-carve: $(INPUT_DIR)/pano.ppm $(BIN_DIR)/$@.$(COMPILER_NAME) $(INPUT_DIR)/pano.ppm -num-seams 100 range-tree: