From 9d177acccd84ca780ef622de7fd1abdcb6b94fde Mon Sep 17 00:00:00 2001 From: Kamil Jakubus Date: Wed, 30 Sep 2026 00:50:21 +0200 Subject: [PATCH] Speed up local variable analysis by reusing previous results Save completed searches in LocalGraph and reuse them when finding which assignments reach a local variable read. Stop searching earlier blocks when a saved result is available and no assignment in the block overrides it. Save results only after the search finishes so loops cannot reuse incomplete results. Apply this to both eager and lazy local variable analysis. Add tests for loops, overwritten locals, initial values, unreachable code, and different query orders. --- src/ir/LocalGraph.cpp | 28 ++++++++++--- test/gtest/local-graph.cpp | 84 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/ir/LocalGraph.cpp b/src/ir/LocalGraph.cpp index b059fb6fd34..f2c54900681 100644 --- a/src/ir/LocalGraph.cpp +++ b/src/ir/LocalGraph.cpp @@ -118,10 +118,12 @@ struct LocalGraphFlower // scanning them linearly is efficient, avoiding hash computations (while // in Info, it's convenient to have a map so we can assign them easily, // where the last one seen overwrites the previous; and, we do that O(1)). - // TODO: If we also stored gets here then we could use the sets for a get - // we already computed, for a get that we are computing, and stop that - // part of the flow. std::vector> lastSets; + + // Completed queries at the start of this block. If the block has no set + // for an index, these also describe its outgoing value. Reuse them instead + // of repeatedly traversing the same predecessors for later queries. + std::vector> computedGets; }; // All the flow blocks. @@ -285,12 +287,23 @@ struct LocalGraphFlower }); if (lastSet != pred->lastSets.end()) { // There is a set here, apply it, and stop the flow. - // TODO: If we find a computed get, apply its sets and stop? That - // could help but it requires more info on FlowBlock. for (auto* get : gets) { getSetsMap[get].insert(lastSet->second); } } else { + auto computed = std::find_if( + pred->computedGets.begin(), + pred->computedGets.end(), + [&](const auto& value) { return value.first == index; }); + if (computed != pred->computedGets.end()) { + const auto& sets = getSetsMap.at(computed->second); + for (auto* get : gets) { + for (auto* set : sets) { + getSetsMap[get].insert(set); + } + } + continue; + } // Keep on flowing. work.push_back(pred); } @@ -300,6 +313,11 @@ struct LocalGraphFlower // Bump the current iteration for the next time we are called. currentIteration++; + + // Publish only after the entire search has finished: a loop may revisit + // this block while its query is still incomplete. + getSetsMap.try_emplace(gets[0]); + block->computedGets.emplace_back(index, gets[0]); } // When the LocalGraph is in lazy mode we do not compute all of getSetsMap diff --git a/test/gtest/local-graph.cpp b/test/gtest/local-graph.cpp index d9ec57bebab..17b4cc149f2 100644 --- a/test/gtest/local-graph.cpp +++ b/test/gtest/local-graph.cpp @@ -1,4 +1,5 @@ #include "ir/local-graph.h" +#include "ir/find_all.h" #include "parser/wat-parser.h" #include "wasm.h" @@ -8,6 +9,89 @@ using LocalGraphTest = ::testing::Test; using namespace wasm; +TEST_F(LocalGraphTest, ReuseCompletedCrossBlockQueries) { + Module wasm; + ASSERT_FALSE(WATParser::parseModule(wasm, R"wasm( + (module + (func $overwritten (param $c i32) (local $x i32) + (local.set $x (i32.const 10)) + (if (local.get $c) (then (drop (local.get $x)))) + (if (local.get $c) (then (drop (local.get $x)))) + (local.set $x (i32.const 20)) + (if (local.get $c) (then (drop (local.get $x)))) + (drop (local.get $x))) + (func $loop (param $c i32) (local $x i32) + (local.set $x (i32.const 10)) + (loop $again + (drop (local.get $x)) + (if (local.get $c) + (then (drop (local.get $x))) + (else (local.set $x (i32.const 20)))) + (drop (local.get $x)) + (br_if $again (local.get $c))) + (drop (local.get $x))) + (func $initial (param $c i32) (local $x i32) + (loop $again + (drop (local.get $x)) + (if (local.get $c) + (then (drop (local.get $x))) + (else (local.set $x (i32.const 20)))) + (drop (local.get $x)) + (br_if $again (local.get $c))) + (drop (local.get $x))) + (func $unreachable (param $c i32) (local $x i32) + (local.set $x (i32.const 10)) + (unreachable) + (if (local.get $c) (then (drop (local.get $x)))) + (drop (local.get $x))) + ) + )wasm") + .getErr()); + + for (auto& func : wasm.functions) { + SCOPED_TRACE(func->name.toString()); + FindAll sets(func->body); + FindAll allGets(func->body); + std::vector gets; + for (auto* get : allGets.list) { + if (get->index == 1) { + gets.push_back(get); + } + } + auto check = [&](const LocalGraphBase::Sets& actual, size_t i) { + SCOPED_TRACE(i); + if (func->name == "overwritten") { + ASSERT_EQ(actual.size(), 1U); + EXPECT_TRUE(actual.contains(sets.list[i < 2 ? 0 : 1])); + } else if (func->name == "loop") { + ASSERT_EQ(actual.size(), 2U); + EXPECT_TRUE(actual.contains(sets.list[0])); + EXPECT_TRUE(actual.contains(sets.list[1])); + } else if (func->name == "initial") { + ASSERT_EQ(actual.size(), 2U); + EXPECT_TRUE(actual.contains(nullptr)); + EXPECT_TRUE(actual.contains(sets.list[0])); + } else { + EXPECT_TRUE(actual.empty()); + } + }; + + LocalGraph eager(func.get(), &wasm); + for (size_t i = 0; i < gets.size(); ++i) { + check(eager.getSets(gets[i]), i); + } + // Lazy queries must give the same answers regardless of query order, + // including when a query traverses a loop before earlier gets are queried. + for (bool reverse : {false, true}) { + LazyLocalGraph lazy(func.get(), &wasm); + for (size_t j = 0; j < gets.size(); ++j) { + size_t i = reverse ? gets.size() - 1 - j : j; + check(lazy.getSets(gets[i]), i); + } + } + } +} + TEST_F(LocalGraphTest, ObstacleBasics) { auto moduleText = R"wasm( (module