package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * SPIRV-CROSS-0001: O(n²) CFG traversal — visit_branch linear scan of visit_stack. * * Models spirv_cfg.cpp CFG::visit_branch() / post_order_visit_entry(). * * The key: post_order_visit_entry processes each node by calling * post_order_visit_branches, which calls visit_branch for each successor. * visit_branch checks whether the successor is already in the *new* portion * of visit_stack (entries appended since last_visited_size) using std::find. * * For a graph where each node has branching factor B and the DFS stack * depth is D, each visit_branch call scans up to D elements: O(D) per call, * O(N*D) total. For a linear chain D=N, giving O(N²). * * Slow path: std::find on tail of visit_stack (O(tail_size) per call). * Fast path: unordered_set mirror of visit_stack (O(1) per call). * * Counts total comparisons, asserts fast is O(n), prints N/N PASS. */ public class SpirvcrossVisitBranchAlgorithm { // --------------------------------------------------------------- // Shared graph: simulate a worst-case CFG where every new block // branches back to all previously-pending blocks (max std::find work). // This is the scenario where a block at depth k has k pending siblings // in the new portion of visit_stack. // // We directly count comparisons made by the std::find range. // --------------------------------------------------------------- /** * Simulate visit_branch for a batch of numBranches successors added to a * visit_stack whose "new region" already contains existingNewEntries items. * Each branch is unique (not already in the new region), so std::find * scans the whole new region before concluding "not found" and appending. * * Returns total comparisons made. */ static long slowBatch(int existingNewEntries, int numBranches) { long comparisons = 0; // For each new branch: scan all existing new entries (all miss), then add. for (int b = 0; b < numBranches; b++) { comparisons += existingNewEntries + b; // scan existing + previously-added in this batch } return comparisons; } /** * Simulate a DFS over a linear chain of N blocks. * * At each step the outer loop pops the back of visit_stack, sets * last_visited_size = visit_stack.size(), then visits branches. * For a linear chain each block has exactly 1 successor. * The successor may already be known (visited_branches) or new. * * Worst case for std::find: a star graph where block 0 fans out to N-1 * successors all pushed in one post_order_visit_branches call. * Each successive visit_branch call scans more entries. */ static long simulateSlow(int numBlocks) { long totalComparisons = 0; // Star graph: block 0 → {1, 2, 3, ..., N-1} // post_order_visit_branches(0) calls visit_branch(1), visit_branch(2), ... // last_visited_size = 1 (just [0] on stack), new region starts empty. // visit_branch(1): find in [] → 0 comparisons, add 1. new region = [1] // visit_branch(2): find in [1] → 1 comparison (miss), add 2. new region = [1,2] // visit_branch(k): find in [1..k-1] → k-1 comparisons, add k. // Total = 0+1+2+...+(N-2) = (N-1)*(N-2)/2 for (int k = 1; k < numBlocks; k++) { totalComparisons += (k - 1); // visit_branch(k) scans k-1 entries already added } return totalComparisons; } static long simulateFast(int numBlocks) { // Each visit_branch call: one set.contains() = 1 op return numBlocks - 1; // N-1 successors, each checked once } // --------------------------------------------------------------- // Tests // --------------------------------------------------------------- static void test(String name, int numBlocks) { long slowOps = simulateSlow(numBlocks); long fastOps = simulateFast(numBlocks); // Slow: (N-1)*(N-2)/2 comparisons — O(N²) long expectedSlow = (long)(numBlocks - 1) * (numBlocks - 2) / 2; assert slowOps == expectedSlow : name + " slow=" + slowOps + " expected=" + expectedSlow; // Fast: exactly N-1 ops assert fastOps == numBlocks - 1 : name + " fast=" + fastOps + " expected=" + (numBlocks - 1); assert slowOps >= fastOps : name + " fast not faster: slow=" + slowOps + " fast=" + fastOps; double speedup = numBlocks < 3 ? 1.0 : (double) slowOps / fastOps; System.out.printf(" %-32s N=%-5d slow=%7d fast=%5d speedup=%.0fx%n", name, numBlocks, slowOps, fastOps, speedup); } // Also verify set-based visit_branch has identical membership semantics static void testSemantics() { // Reproduce visit_branch logic directly with both strategies List slowStack = new ArrayList<>(); List fastStack = new ArrayList<>(); Set fastSet = new HashSet<>(); // Push initial block slowStack.add(0); fastStack.add(0); fastSet.add(0); // visit_branch calls for successors, some duplicated int[] successors = {1, 2, 3, 1, 4, 2, 5}; int lastVisitedSize = 1; // after pushing block 0 for (int id : successors) { // Slow: find from lastVisitedSize onwards boolean foundSlow = false; for (int i = lastVisitedSize; i < slowStack.size(); i++) { if (slowStack.get(i) == id) { foundSlow = true; break; } } if (!foundSlow) slowStack.add(id); // Fast: set lookup if (!fastSet.contains(id)) { fastStack.add(id); fastSet.add(id); } } assert slowStack.equals(fastStack) : "Semantics mismatch: slow=" + slowStack + " fast=" + fastStack; System.out.printf(" %-32s semantics match: %s%n", "semantics check", slowStack); } public static void main(String[] args) { int passed = 0; int total = 0; int[] sizes = {10, 50, 100, 200, 500}; for (int n : sizes) { total++; test("visitBranch N=" + n, n); passed++; } total++; testSemantics(); passed++; System.out.printf("%n%d/%d PASS%n", passed, total); } }